1.2.4 - Object-oriented programming principles

1.2.4 - Object-oriented programming principles

Object-oriented programming groups state with the operations that use it. You will distinguish a class from its objects, protect an object's state through methods, and follow how inheritance and overriding affect a method call in OCR pseudocode and Python.

A class defines behavior while each object holds state

Object-oriented code separates a reusable definition from each particular entity created from it.

Class and instance

A class defines a kind of object's attributes and methods. An object is an instance created from that class, with its own instance state.

An attribute stores state; a method is a subroutine belonging to the object. A Session class might have a minutes attribute and an extend method; two Session objects can hold different minutes values while using the same method definition.

[DIAGRAM: One Session class instantiates morning and evening objects with independent minutes values; asset_slug: session-class-objects; caption: A shared class definition creates separate objects, each with its own state.]
A shared class definition creates separate objects, each with its own state.

Creating morning with 20 minutes and evening with 45 minutes does not create one shared minutes variable. Extending morning by 5 should change morning to 25 and leave evening at 45. The class is a definition; the object is the particular entity on which a call such as morning.extend(5) acts. A method's parameter supplies extra input to that operation, rather than creating another object automatically.

Object-oriented design can keep related state and operations together so callers need not manage every internal update. That benefit depends on a sensible design: using classes does not itself make a program faster, safer or easier to maintain. A class should represent a meaningful responsibility, not merely rename a collection of unrelated functions.

Constructors and access methods protect valid state

A constructor initialises a new object. OCR pseudocode names this procedure new; new Session(20) creates an instance and passes 20 to its constructor. A public member can be used by outside code; a private member is accessible through the class's implementation rather than direct outside access. OCR's printed pseudocode assumes members are public unless explicitly marked otherwise, and its methods are instance methods.

Parameter, argument and receiver

A parameter is a name in a method definition; an argument supplies its value at a call. In morning.extend(5), morning is the receiving object and 5 is the argument supplied to amount.

Encapsulation combines state with operations and provides a controlled interface. Marking an attribute private stops callers bypassing the intended operations. A getter returns state; a setter or another update method can enforce an invariant, such as minutes never becoming negative. Encapsulation requires appropriate method behavior, not simply a getter and setter with no checks.

Pseudocode
class Session
    private minutes
    public procedure new(givenMinutes)
        minutes = givenMinutes
    endprocedure
    public function getMinutes()
        return minutes
    endfunction
    public function extend(amount)
        if amount < 0 then
            return false
        endif
        minutes = minutes + amount
        return true
    endfunction
    public function describe()
        return "Session"
    endfunction
endclass
morning = new Session(20)
accepted = morning.extend(5)
print(morning.getMinutes())

The constructor's givenMinutes has a precondition of being a nonnegative integer. The last call prints 25. Calling extend with −3 returns false before changing minutes; zero is accepted and leaves it unchanged. Outside code should use getMinutes() rather than assign to morning.minutes. A getter returns a value and is therefore a function; a procedure without a returned value is not a substitute.

Inheritance shares behavior and constructor work

Inheritance defines a subclass from a superclass. The subclass receives the superclass's available behavior and may add or specialise features. Use it for a meaningful 'is a kind of' relationship: a PaidSession is a kind of Session. A booking system containing a Session is a 'has a' relationship and does not by itself justify inheritance.

OCR writes inherits in the class declaration and super.new(...) to call the superclass constructor. super.method(...) calls a superclass method. These calls reuse an existing implementation rather than duplicate its state updates.

Pseudocode
class PaidSession inherits Session
    private rate
    public procedure new(givenMinutes, givenRate)
        super.new(givenMinutes)
        rate = givenRate
    endprocedure
    public function cost()
        return getMinutes() * rate
    endfunction
    public function describe()
        return super.describe() + ": paid"
    endfunction
endclass
evening = new PaidSession(45, 2)
print(evening.cost())

This uses the preceding Session definition. The rate is an integer price per minute for this example; givenMinutes and givenRate are nonnegative integers. The superclass constructor sets minutes to 45 and the subclass constructor sets rate to 2, so cost returns 90. PaidSession can use inherited extend and getMinutes. It should not directly access Session's private minutes; the getter is the defined interface.

Calling super.describe() explicitly gets the superclass's "Session" result, to which the subclass adds ": paid". This demonstrates reuse even when a method is specialised. Calling the superclass constructor matters because inherited state must be initialised too. Inheritance can reduce duplication, but an unsuitable hierarchy can couple classes so that a superclass change has unintended effects in its subclasses.

The object's class selects an overridden method

Overriding supplies a subclass implementation of an inherited method with the same interface. Polymorphism lets the same kind of call use different appropriate implementations for different objects. The call is not chosen merely from a variable's name; it depends on the object receiving it.

Session and PaidSession both provide describe() with no explicit parameters. Calling morning.describe() uses Session's implementation and returns "Session". Calling evening.describe() uses PaidSession's override and returns "Session: paid". Client code can call describe on either object without writing a separate type test for each class.

For example, a display routine receives one of these objects and calls its describe method. Adding a new subclass with its own compatible describe method can add another display behavior without changing the routine. The shared interface must still mean something consistent to callers; a method with a different name or incompatible parameters is not this override. Overriding is also distinct from merely giving several unrelated functions different names.

Polymorphism does not imply every inherited method changes. PaidSession's getMinutes and extend still use the inherited implementations, while describe is specialised.

Encapsulation controls access to state. Inheritance establishes a reuse relationship. Polymorphism selects the appropriate behavior through a common call on the receiving object.

Follow and amend equivalent pseudocode and Python

Programming principles are independent of one language's punctuation, but a working implementation must respect its language's conventions. Python constructors use __init__; self identifies the current object. Python's double-underscore attributes are name-mangled to reduce accidental access and collisions, rather than providing an absolute security boundary. This is an implementation aid to the encapsulation design, not identical enforcement to OCR's private keyword.

The following complete Python 3 sandbox implements the same interface and behavior. Its constructors reject negative initial values with ValueError; inputs are integers. No extra libraries are needed. This is ungraded exploration: predict before Run, then change the receiver or argument and observe which object's state and method behavior changes. Run neither submits an answer nor awards marks.

Explore object state and method dispatch

{"exercise_id":"h446-1-2-4-session-state-dispatch","task_version":"1","runtime_profile":"python-console-v1","original_filename":"sessions.py","file_mode":"fresh","code":"class Session:\n def init(self, minutes):\n if minutes < 0:\n raise ValueError("minutes must be nonnegative")\n self.__minutes = minutes\n\n def get_minutes(self):\n return self.__minutes\n\n def extend(self, amount):\n if amount < 0:\n return False\n self.__minutes += amount\n return True\n\n def describe(self):\n return "Session"\n\n\nclass PaidSession(Session):\n def init(self, minutes, rate):\n super().init(minutes)\n if rate < 0:\n raise ValueError("rate must be nonnegative")\n self.__rate = rate\n\n def cost(self):\n return self.get_minutes() * self.__rate\n\n def describe(self):\n return super().describe() + ": paid"\n\n\nmorning = Session(20)\nevening = PaidSession(45, 2)\nprint(morning.extend(5))\nprint(evening.extend(-3))\nprint(morning.get_minutes(), evening.get_minutes())\nprint(evening.cost())\nprint(morning.describe(), evening.describe())\n","instructions":"Before Run, predict both return values, both objects' minutes, the cost and the two descriptions. Run and compare. Next change evening.extend(-3) to evening.extend(0), then to evening.extend(5), predicting the return/state/cost each time. Reset those calls, then replace morning = Session(20) with morning = PaidSession(20, 3): predict which description changes and whether evening's minutes changes. Finally reset and try evening = PaidSession(-1, 2); confirm the constructor rejects the invalid starting state. Each run creates fresh objects."}

Trace attributes per object: the failed evening extension does not alter its minutes or rate. Check initial zero and extend(0), a successful positive extension, a rejected negative extension, and negative constructor values. A faulty extension that updates state before testing amount would violate the invariant even if it eventually returned False. Move the rejection before the assignment; changing only the returned Boolean does not undo state corruption.

For writing practice, construct a small class with a private state attribute, a constructor, a getter and one validated update method in an ordinary local Python IDE or OCR-style pseudocode editor. Test each public operation against its stated contract. NovaLearn's standalone pseudocode source editor preserves source for marking but does not run it; Python console execution is available only for deliberately runnable tasks. Writing a complete inherited solution and broader design trade-offs are developed further in the later implementation lesson.