The Object-Oriented Paradigm

Designing and building software from interacting objects — using classes, encapsulation, inheritance, polymorphism, and abstraction to keep code modular and reusable.

Year 11 — Preliminary ~10 weeks Outcomes: SE-11-01 to 04, 06 to 09
11Dot points
2Parts
8Outcomes
~10Weeks
Syllabus Part 1 Understanding OOP

Key Features of an OOP Language

🎯 (SE-11-01, SE-11-02, SE-11-08)

📌 Apply the key features of an object-oriented programming (OOP) language

  • objects
  • classes
  • encapsulation
  • abstraction
  • inheritance
  • generalisation
  • polymorphism

This section uses one running example, a small lending system for Riverbend Library. The library lends books and DVDs to members. Every code block is real Python 3 that you can paste into an editor and run; the output shown was produced by running it. The same classes come back in later sections, where they are tested, refactored and drawn as a class diagram. NESA's syllabus glossary defines object-oriented programming as "a paradigm based on the concept of 'objects' that can contain data and code in the form of procedures". The Course Specifications add that students are expected to be able to code in Python, so the examples use it.

Key featureWhat it meansWhere you will see it at Riverbend Library
ClassA blueprint that defines the attributes and methods every object of that type has.Book, DVD, Member, Library
ObjectOne instance of a class, holding its own attribute values.The book "River Road" and the DVD "Star Coast"
EncapsulationBundling data with the methods that use it, and controlling access to the data.A member's loans can only change through borrow() and give_back()
AbstractionShowing only what a user of the class needs and hiding the rest.Code calls item.loan_days() without knowing how each item type decides
InheritanceA subclass reuses the attributes and methods of a superclass.Book and DVD inherit from LibraryItem
GeneralisationFinding what several classes share and moving it up into one superclass.Title, id and loan status pulled out of Book and DVD into LibraryItem
PolymorphismOne message, many forms: different objects respond to the same method call in their own way.describe() gives a different result for a book and a DVD

Objects and Classes

A class is a blueprint. It lists the attributes (the data each object holds) and the methods (the behaviour each object can perform). An object is one instance built from that blueprint while the program runs. Two objects of the same class have the same attributes and methods, but each keeps its own values. The constructor (__init__ in Python) runs when an object is created and sets its starting state. Inside a method, self means "the object this method was called on".

Python — a class and two objects
class Book:
    def __init__(self, title, author):   # constructor: runs when an object is created
        self.title = title               # attributes: this object's data
        self.author = author
        self.on_loan = False

    def describe(self):                  # method: behaviour
        status = "on loan" if self.on_loan else "available"
        return f"{self.title} by {self.author} [{status}]"


book1 = Book("River Road", "A. Nguyen")  # object 1
book2 = Book("Salt Country", "M. Okafor")  # object 2
book1.on_loan = True                     # changes book1 only

print(book1.describe())
print(book2.describe())
print(book1 is book2)                    # two separate objects
Output
River Road by A. Nguyen [on loan]
Salt Country by M. Okafor [available]
False

Attributes set inside __init__ belong to each object (instance attributes). A value written directly in the class body, such as MAX_LOANS = 3 in the full program later, belongs to the class and is shared by every object (a class attribute). Use a class attribute for a rule that is the same for everyone and an instance attribute for anything that differs between objects.

Encapsulation

Encapsulation bundles an object's data with the methods that operate on it, and controls how outside code can reach that data. The aim is that an object can never be left in an invalid state by accident: every change goes through a method that can check it first. In Python, a leading underscore (_name) is a convention meaning "internal, please don't touch", and a double underscore (__loans) triggers name mangling, which renames the attribute to _Member__loans so it is awkward to reach by accident. Python does not have a true private keyword as Java and C# do, so private here is a strong signal rather than a lock. What matters for the HSC is the design intent: keep data hidden and expose a small, controlled interface.

A getter reads a value and a setter changes it, usually after validating it. Python's @property decorator lets you write them so that outside code still uses simple attribute syntax.

Python — private data, a getter and a validating setter
class Member:
    def __init__(self, name, email):
        self._name = name
        self.email = email              # goes through the setter below
        self.__loans = []               # private list of borrowed items

    @property
    def email(self):                    # getter
        return self._email

    @email.setter
    def email(self, value):             # setter with validation
        if "@" not in value:
            raise ValueError("email must contain @")
        self._email = value

    @property
    def loan_count(self):               # read-only: there is no setter
        return len(self.__loans)


m = Member("Priya", "priya@example.com")
m.email = "priya.s@example.com"         # allowed: passes validation
print(m.email, m.loan_count)

try:
    m.email = "not-an-email"            # rejected by the setter
except ValueError as err:
    print("Rejected:", err)

try:
    m.loan_count = 5                    # no setter, so this is refused
except AttributeError as err:
    print("Refused:", err)

print(hasattr(m, "__loans"), hasattr(m, "_Member__loans"))   # name mangling in action
Output
priya.s@example.com 0
Rejected: email must contain @
Refused: property 'loan_count' of 'Member' object has no setter
False True

Python's exact error wording differs slightly between versions (for example, the abstract-class message below), but the type of error is the same.

Encapsulation is not the same as abstraction Encapsulation is about protecting data: bundling it with its methods and restricting access. Abstraction is about simplifying the view: showing only what a user of the class needs. A car's locked bonnet is encapsulation; the steering wheel and pedals that let you drive without understanding the engine are abstraction. Students often swap the two, so write the difference into your answer.

Concept diagram

Encapsulation: what outside code can reach

Outside code uses the public methods; the private data can only change through them.

What to notice
  • The only route to the private data is through methods that can check the request.
  • This is why a bank account can never be given a negative balance by mistake.

Try thisWhat would go wrong if __balance were public?

Try it

Objects and encapsulation: the Member class

Send messages to Member objects and see why encapsulation protects the rules of a class. Enable JavaScript to use this tool. In short: borrow() checks the loan limit before changing the private list of loans, and code that reaches inside can break the rule.

Abstraction

Abstraction means modelling only the details that matter for the problem and hiding the rest behind a simple interface. A Member class for a library needs a name and an email address; it does not need height or favourite colour. A user of the class calls borrow(item) without knowing how the loan list is stored, and the developer can change the storage from a list to a database later without breaking any calling code. This is why abstraction reduces complexity and allows the internal implementation to change without affecting the parts of the program that use it.

An abstract class takes this further. It defines a method that every subclass must provide, but does not say how, and it cannot be instantiated itself. Python provides this through the abc module.

Python — an abstract class defines the interface, subclasses supply the detail
from abc import ABC, abstractmethod


class LibraryItem(ABC):
    def __init__(self, title):
        self._title = title

    @abstractmethod
    def loan_days(self):                # a promise: every item type must answer this
        ...


class Book(LibraryItem):
    def loan_days(self):
        return 14


class DVD(LibraryItem):
    def loan_days(self):
        return 7


print(Book("River Road").loan_days(), DVD("Star Coast").loan_days())

try:
    LibraryItem("Something")            # an abstract idea, not a real item
except TypeError as err:
    print("TypeError:", err)
Output
14 7
TypeError: Can't instantiate abstract class LibraryItem with abstract method loan_days

Inheritance

Inheritance lets a new class (the subclass or child) take on the attributes and methods of an existing class (the superclass or parent) and then add or change behaviour. It models an is-a relationship: a Book is a library item. A subclass can add new attributes and methods (a Book has an author), reuse inherited ones unchanged (check_out()), or override a method by defining one with the same name. Inside an overriding method, super() calls the parent's version so the subclass can extend it rather than repeat it.

Python — a subclass reusing, extending and overriding
class LibraryItem:
    def __init__(self, item_id, title):
        self._item_id = item_id
        self._title = title
        self._on_loan = False

    def check_out(self):
        if self._on_loan:
            return False
        self._on_loan = True
        return True

    def describe(self):
        status = "on loan" if self._on_loan else "available"
        return f"{self._title} [{status}]"


class Book(LibraryItem):
    def __init__(self, item_id, title, author):
        super().__init__(item_id, title)   # reuse the parent's constructor
        self._author = author              # add something new

    def describe(self):                    # override, but extend using super()
        return f"{super().describe()} by {self._author}"


class ReferenceBook(Book):
    def check_out(self):                   # override completely: never lendable
        return False


atlas = ReferenceBook("R1", "Atlas of Australia", "P. Smith")
print(atlas.check_out())                   # ReferenceBook's own version
print(atlas.describe())                    # Book's version, which calls LibraryItem's
print(isinstance(atlas, Book), isinstance(atlas, LibraryItem))
Output
False
Atlas of Australia [available] by P. Smith
True True

Use inheritance only when the is-a test is honest. A Member is not a kind of LibraryItem, so it must not inherit from it. When the relationship is has-a (a library has items; a member has loans), use composition: hold the other object in an attribute. Deep chains of inheritance are harder to read than shallow ones, so two or three levels is plenty for a school project.

Class diagram

Class diagram: a vehicle hierarchy

Car, Motorcycle and Truck inherit from Vehicle and add their own attributes and methods.

What to notice
  • Each class box has three parts: name, attributes, methods.
  • The filled arrowhead points to the parent class (inheritance), as in the NESA Course Specifications.
  • The plain arrow with 1 and 1 is a relationship: each vehicle has exactly one engine.

Try thisAdd an ElectricCar class. Which class should it inherit from, and what attribute would it add?

Generalisation

Generalisation is the design activity that produces a superclass. You notice that several classes repeat the same attributes and methods, so you move the shared parts up into a new parent class and leave only the differences in each child. Inheritance is the mechanism; generalisation is the reasoning that decides what to inherit. The opposite direction, adding a more specific subclass to an existing class, is specialisation.

Python — before: Book and DVD repeat the same code
class Book:
    def __init__(self, item_id, title, author):
        self._item_id = item_id          # duplicated
        self._title = title              # duplicated
        self._on_loan = False            # duplicated
        self._author = author

class DVD:
    def __init__(self, item_id, title, minutes):
        self._item_id = item_id          # duplicated
        self._title = title              # duplicated
        self._on_loan = False            # duplicated
        self._minutes = minutes
Python — after: the shared parts are generalised into LibraryItem
class LibraryItem:                       # the generalisation
    def __init__(self, item_id, title):
        self._item_id = item_id
        self._title = title
        self._on_loan = False

class Book(LibraryItem):                 # only what makes a Book different
    def __init__(self, item_id, title, author):
        super().__init__(item_id, title)
        self._author = author

class DVD(LibraryItem):                  # only what makes a DVD different
    def __init__(self, item_id, title, minutes):
        super().__init__(item_id, title)
        self._minutes = minutes

The benefit is that a rule such as "an item cannot be checked out twice" is written once. Fix a bug in LibraryItem and every subclass is fixed as well.

Polymorphism

Polymorphism means "many forms". The same message (method call) can be sent to objects of different classes, and each responds in the way that suits its own class. The calling code does not need to check what type it has. Python gives you polymorphism in two ways:

  • Overriding (inheritance-based polymorphism): subclasses provide their own version of a method that the superclass defines. Python decides which version to run when the call happens, by looking at the class of the object receiving the message.
  • Duck typing: if an object has the method being called, it works, whatever its class. "If it walks like a duck and quacks like a duck, it is treated as a duck." The classes do not need a shared parent.
Python — one loop, many forms
class LibraryItem:
    def __init__(self, title):
        self._title = title

    def describe(self):
        return f"{self._title}"

    def loan_days(self):
        return 0


class Book(LibraryItem):
    def loan_days(self):                   # override
        return 14

    def describe(self):
        return f"Book: {self._title}"


class DVD(LibraryItem):
    def loan_days(self):                   # override
        return 7

    def describe(self):
        return f"DVD: {self._title}"


class MobileLibraryVan:                    # NOT a LibraryItem: no shared parent
    def describe(self):
        return "Van: Riverbend Mobile Library"

    def loan_days(self):
        return 3


things = [Book("River Road"), DVD("Star Coast"), MobileLibraryVan()]
for thing in things:                       # the loop never checks the type
    print(thing.describe(), "->", thing.loan_days(), "days")
Output
Book: River Road -> 14 days
DVD: Star Coast -> 7 days
Van: Riverbend Mobile Library -> 3 days

The MobileLibraryVan works in the loop purely by duck typing: it has describe() and loan_days(), so the loop accepts it. The benefit is that new types can be added without changing existing code that sends the messages. Note that Python does not support method overloading (several methods with one name but different parameter lists) in the way Java does. The last definition wins, and you use default arguments instead. On the HSC, "polymorphism" almost always means overriding.

Method lookup explorer

Pick an object and a message, then trace which class's method actually runs. Switch overrides off to see Python fall back to the parent.

Exam tip: "Apply" An apply question wants you to use the feature on a given situation, not just define it. Name the feature, point to where it appears in the scenario or code, and say what benefit it gives there. For example, for "explain the benefit of encapsulation in the Member class": "Encapsulation keeps the loan list private so it can only change through borrow() and give_back(). Those methods enforce the three-item limit, so no other code can give a member four loans by mistake, and the list could be replaced by a database without changing any calling code." A one-line definition alone earns few marks. Common errors: swapping encapsulation and abstraction, describing inheritance as "copying code", and saying polymorphism "means many classes".
▶ Watch & Learn

A fast, beginner-friendly tour of the four pillars — encapsulation, abstraction, inheritance, and polymorphism — with simple real-world analogies.

▶ Watch & Learn

A clear, hands-on introduction to classes and objects — the core idea behind object-oriented programming.

Try it

Which feature of OOP?

Practise naming encapsulation, abstraction, inheritance, polymorphism and generalisation. Enable JavaScript to use this tool. In short: encapsulation hides data behind methods, abstraction shows only what is needed, inheritance reuses a parent class, polymorphism answers one message in different ways, and generalisation moves shared features into a parent.


Comparing Procedural Programming with OOP

🎯 (SE-11-02)

📌 Compare procedural programming with OOP

Both paradigms can solve the same problem. The difference is how the code is organised: procedural programs are a sequence of functions that act on data passed between them, while OOP programs are made of objects that keep their data and the methods that use it together. Procedural programming is one style of the imperative paradigm (statements that change state, organised into subroutines), so the two are related rather than opposites: an OOP method body is itself imperative code.

Aspect Procedural Programming Object-Oriented Programming
Core Focus Step-by-step logic and function calls. Interaction between modular objects.
Data Handling Data is kept apart from the functions, passed as parameters or shared as global variables. Data and methods are encapsulated together; access can be restricted.
Reusability Reuse through functions and modules; supporting a new kind of data usually means editing or copying functions. Reuse through classes, inheritance and composition; a new kind of object can be added as a subclass.
Maintenance A change to how data is stored can ripple through every function that touches it. Changes to internal details stay inside the class as long as its interface stays the same.
Modelling Models the process: what happens, in what order. Models the things in the problem and what each is responsible for.
Best suited to Short scripts, calculations and pipelines with a clear start-to-finish flow. Larger systems with several kinds of entity that have their own data and behaviour.

Concept diagram

Procedural and object-oriented code

The same goals, organised differently.

What to notice
  • Procedural code keeps data separate from the functions that change it.
  • OOP keeps each object's data with its own methods.

Try thisWould you use procedural or OOP for a one-off script that renames files? Why?

The same task, two ways

Python — procedural
balance = 100

def withdraw(balance, amount):
    return balance - amount

balance = withdraw(balance, 30)   # any code can change balance directly
Python — object-oriented
class Account:
    def __init__(self, balance):
        self.__balance = balance       # hidden inside the object

    def withdraw(self, amount):
        if amount <= self.__balance:     # the object protects its own data
            self.__balance -= amount

acc = Account(100)
acc.withdraw(30)

Notice that the procedural withdraw() happily returns a negative balance, and nothing stops other code from writing balance = -500. In the OOP version the rule lives beside the data it protects.

What changes when a new type is added?

The real difference shows up when the program has to grow. At Riverbend Library, books are lent for 14 days. The library now adds DVDs, lent for 7 days. Compare the two designs.

Python — procedural: every function must know about every item type
def loan_days(item):
    if item["type"] == "book":
        return 14
    elif item["type"] == "dvd":            # edited to add DVDs
        return 7

def describe(item):
    if item["type"] == "book":
        return f"Book: {item['title']}"
    elif item["type"] == "dvd":            # edited again
        return f"DVD: {item['title']} ({item['minutes']} min)"

items = [{"type": "book", "title": "River Road"},
         {"type": "dvd", "title": "Star Coast", "minutes": 95}]
for item in items:
    print(describe(item), loan_days(item))
Output
Book: River Road 14
DVD: Star Coast (95 min) 7
Python — object-oriented: a new type is a new class, existing code is untouched
class Book:
    def __init__(self, title):
        self.title = title
    def loan_days(self):
        return 14
    def describe(self):
        return f"Book: {self.title}"

class DVD:                                 # the only new code
    def __init__(self, title, minutes):
        self.title = title
        self.minutes = minutes
    def loan_days(self):
        return 7
    def describe(self):
        return f"DVD: {self.title} ({self.minutes} min)"

items = [Book("River Road"), DVD("Star Coast", 95)]
for item in items:                         # this loop did not change
    print(item.describe(), item.loan_days())
Output
Book: River Road 14
DVD: Star Coast (95 min) 7

In the procedural version, adding a third type (say, an audio book) means finding and editing every function that has an if on type, and forgetting one causes a bug. In the OOP version it means writing one new class. That is the practical meaning of "easier to extend and maintain".

Other paradigms in the Course Specifications

The Course Specifications name four paradigms. You are expected to know what each involves, and this course asks you to compare procedural code with OOP specifically. In one line each:

ParadigmOrganised aroundTypical building blocks
Imperative / proceduralCommands that change state, grouped into subroutinesVariables, assignment, control structures, subroutines
Object-orientedObjects that hold state and respond to messagesClasses, objects, inheritance, polymorphism, encapsulation, message passing
FunctionalFunctions applied to values, avoiding shared changing stateFunction calls, recursion, functions as first-class values, collections
LogicFacts and rules from which the system works out answersFacts, rules, queries

Python supports several of these in one program, so a real project often mixes them: OOP for the overall structure, with small functional-style helpers inside methods.

HSC writing sample "Procedural programming suits a short script that converts a file once, because a few functions are quicker to write than classes. OOP suits a school management system, because classes such as Student and Teacher keep each type of data with the methods that change it, making the system easier to extend and maintain as it grows."
Exam tip: "Compare" A compare question wants similarities and differences on the same criteria, not two separate descriptions. Choose two or three criteria (how data is handled, how new features are added, how easy it is to maintain), then write one sentence per criterion that uses a linking word such as whereas or in contrast. Add a concrete example from the scenario. Both paradigms can solve the problem, so avoid saying OOP is "better": say which suits the situation and why. A common mistake is listing OOP features without saying what the procedural version does.

Representing a System: DFDs, Structure Charts and Class Diagrams

🎯 (SE-11-01, SE-11-06)

📌 Use data flow diagrams, structure charts and class diagrams to represent a system

The Course Specifications give you three modelling tools. Each shows a different view of the same system, so choosing the right one is part of the skill. All three use NESA's notation, summarised on the Course Tools & Specs page.

ToolQuestion it answersUse it in OOP to show
Class diagram (Course Specifications p. 9)What are the classes and how are they related?Classes with their attributes and methods, inheritance, and relationships with multiplicities. The primary tool for OOP.
Data flow diagram (p. 5)Where does data come from, where does it go and how is it changed?How data moves between external entities, processes and data stores, before you decide which classes will do the work.
Structure chart (p. 7)How is the program broken into modules and what do they pass to each other?The calls and parameters between modules or methods, such as a mainline calling subroutines. Common in top-down design.

Class diagrams: NESA notation

A class diagram models classes, their attributes and methods, and the relationships between classes. It shows the structure of the system, not the order in which things happen. The symbols in the Course Specifications are:

SymbolMeaning
Box in three partsClass name at the top, attributes in the middle, methods at the bottom.
Line with a closed triangle headInheritance. The triangle touches the parent class; the line starts at the child.
Line with an open arrow head, and a labelA relationship between two classes, named with a verb such as "borrows".
1..1   0..*   1..*   0..1   m..nMultiplicity: only one; zero or more; one or more; zero or only one; at least m and at most n.

Class diagram

Class diagram: the Riverbend Library system

The running example from this page: three item classes, a member and the library.

What to notice
  • Book and DVD point to LibraryItem with the triangle at the parent, and ReferenceBook points to Book: a three-level hierarchy.
  • Only the differences appear in the child boxes. Book does not repeat itemId or title, because it inherits them.
  • 0..1 at the Member end and 0..3 at the LibraryItem end together say: an item is on loan to at most one member, and a member has up to three items.

Try thisAdd a Fine class. A member can have zero or more fines, and each fine belongs to exactly one member. Which multiplicity goes at which end?

Reading multiplicities

A multiplicity is written at the end of the line next to the class it counts, and answers "for one object at the other end, how many objects of this class?". Read each line in both directions and say it as a sentence. That is also how to check your own diagram.

On the diagramRead as
Library 1..1 holds 0..* LibraryItemOne library holds zero or more items. Each item belongs to exactly one library.
Member 0..1 borrows 0..3 LibraryItemA member borrows between zero and three items. An item is borrowed by zero or one member (zero when it is on the shelf).
Student 0..* studies 1..* Subject (Course Specifications example)A student must study one or more subjects. A subject can have zero or more students enrolled.

From diagram to code

A class diagram is a plan for code, and the mapping is direct. Each box becomes a class, each attribute becomes an instance attribute, each method becomes a method, an inheritance arrow becomes the parent in brackets, and a relationship with a multiplicity becomes an attribute that holds the other object or a collection of them. The upper limit of a multiplicity (the 3 in 0..3) becomes a check in the code.

Python — the class diagram as a code skeleton
from abc import ABC, abstractmethod

class LibraryItem(ABC):                    # box: LibraryItem
    def __init__(self, item_id, title):
        self._item_id = item_id            # attribute: itemId
        self._title = title                # attribute: title
        self._on_loan = False              # attribute: onLoan

    @abstractmethod
    def loan_days(self): ...               # method: loanDays()

class Book(LibraryItem):                   # inheritance arrow: Book -> LibraryItem
    def __init__(self, item_id, title, author):
        super().__init__(item_id, title)
        self._author = author              # attribute only Book has

    def loan_days(self):
        return 14

class ReferenceBook(Book):                 # inheritance arrow: ReferenceBook -> Book
    pass

class Member:
    MAX_LOANS = 3                          # multiplicity 0..3 becomes a limit
    def __init__(self):
        self.__loans = []                  # relationship "borrows": zero or more items

print([c.__name__ for c in ReferenceBook.__mro__])   # inheritance chain, child first (ABC and object are Python's own)
Output
['ReferenceBook', 'Book', 'LibraryItem', 'ABC', 'object']

Data flow diagrams and structure charts in an OOP project

  • Data flow diagram (Course Specifications p. 5): useful early, for showing how data moves between external entities (a member), processes (Check out item) and data stores (Loans). It shows the transformation of data and does not mention classes, so it helps you discover what the classes must do.
  • Structure chart (Course Specifications p. 7): a hierarchy of modules with the data and flags passed between them. In OOP it is a good way to plan the calls inside one operation, for example a checkout() method that calls find_member(), find_item() and borrow().

Structure chart

Structure chart: student management

The modules of the system and what they pass to each other.

What to notice
  • The diamond means only one of the three main tasks runs each time.
  • Enrol student sends Details down and gets a Valid flag back.

Try thisDraw the structure chart for Produce report, refining it into smaller modules.

Data flow diagram

Data flow diagram: canteen ordering (Level 1)

How data moves between people, processes and stored data.

What to notice
  • Processes are circles, data stores are open-ended rectangles, and external entities are rectangles.
  • Every arrow is labelled with the data it carries.

Try thisAdd a process 3 Take payment. Which entity and data store would it connect to?

Online Tool Suggestion: Try Lucidchart, Draw.io, or Mermaid.js to create professional UML Class Diagrams and DFDs. Tools may add UML extras (visibility signs, diamonds for composition) that are not in the Course Specifications; in an assessment, stick to NESA's symbols.

Which diagram for which question?

SituationBest toolWhy
"Show how the library's classes are related."Class diagramStructure: classes, inheritance and multiplicities.
"Show how a loan request flows from the member to the records."Data flow diagramMovement of data between entities, processes and stores.
"Show how the checkout task is divided into modules."Structure chartModule hierarchy with the data passed between modules.
"Show the order in which two objects exchange messages."Sequence diagram (used on this page for message passing; not one of the three required tools)Time order of calls between objects.
Exam tip: "Use" and drawing a class diagram A use question asks you to draw or complete the diagram for a described system. Work in this order: (1) underline the nouns in the scenario and pick the classes; (2) draw each as a three-part box; (3) put in only the attributes and methods the scenario needs, with types on attributes; (4) draw an inheritance arrow from each child to its parent with the triangle at the parent; (5) draw and label a relationship line for every other link and put a multiplicity at both ends. Check each relationship by reading it aloud in both directions. Common mistakes: the inheritance arrow pointing the wrong way, repeating the parent's attributes in the child box, a relationship line with no label or with a multiplicity at one end only, and using "many" instead of 0..* or 1..*. NESA's published 2025 HSC marking guidelines (the only published exam so far, so treat it as one example, not a promise) included a 3-mark class diagram question about inheritance and removing redundant attributes, and marker feedback asked for "appropriate symbols for class diagrams".

The Design Process in OOP

🎯 (SE-11-01)

📌 Describe the process of design used to develop code in an OOP language

  • task definition
  • top-down and bottom-up
  • facade pattern
  • agility

Design is what happens between "we have a need" and "we start typing code". In OOP the process is about deciding which classes exist, what each is responsible for and how they work together. The four parts below are usually used together, not one after another.

Task Definition

The initial phase where engineers formally outline the problem scope and define the specific objects and classes required to solve it. This involves understanding the requirements, identifying the key entities (objects) in the problem domain, and determining their relationships and responsibilities. A useful technique is to write the need as a short statement and then read it for nouns (candidate classes or attributes) and verbs (candidate methods).

Task definition: Riverbend Library "Riverbend Library lends books and DVDs to members. A member can borrow up to three items at once. Books can be kept for 14 days and DVDs for 7. Reference books can be read in the library but never borrowed. Staff need to check items out, check them in and print a list of what is on the shelves."
Found in the statementDesign decision
Nouns: library, member, item, book, DVD, reference bookCandidate classes. "Book", "DVD" and "reference book" are kinds of item, so LibraryItem is a generalisation and Book, DVD and ReferenceBook are subclasses.
Nouns that describe a noun: title, author, minutes, emailAttributes of those classes (title belongs to every item; author only to a book).
Verbs: lends, borrow, check out, check in, printMethods: borrow(), check_out(), check_in(), report().
Rules: up to three items; 14 days; 7 days; never borrowedA limit in Member, and overriding of loan_days() and check_out() in the subclasses.
Inputs and outputs: member id and item id in; a message and a report outThe parameters and return values of Library.checkout() and Library.report().

The result of task definition is a written list of requirements, a first list of classes with responsibilities, and a draft class diagram (see the previous section).

Top-down and Bottom-up Design

Top-down design starts with high-level system requirements and breaks them down into smaller sub-systems or classes. This approach defines the overall structure first, then fills in details for each class. Bottom-up design involves building and testing robust base classes first, then combining them to create the final application. Effective OOP engineering often uses a hybrid approach, ensuring individual objects are functional (bottom-up) while meeting the overall system architecture (top-down).

Top-downBottom-up
Starts withThe whole system and its main taskThe smallest reusable classes
Riverbend exampleWrite Library.checkout() first, calling classes that do not exist yetWrite and test LibraryItem and Book first, then Member, then Library
NeedsStubs for the parts not yet writtenA test for each class before it is combined
StrengthThe overall structure is clear early; teams can work in parallel against agreed interfacesEvery finished part is already tested and reusable
RiskDetails discovered later may force the top to changeThe parts may not fit together into what the user needs

A top-down start looks like this. The top-level behaviour is written and can already be run, while each lower-level class is only a stub (a placeholder that returns a fixed answer) until it is developed properly.

Python — top-down: the top-level logic first, stubs underneath
class Member:                               # stub: real rules not written yet
    def borrow(self, item):
        return True                         # always says yes for now

class Book:                                 # stub
    def __init__(self, title):
        self.title = title


class Library:                              # the top of the design
    def checkout(self, member, item):
        if member.borrow(item):
            return f"Checked out {item.title}"
        return f"Cannot lend {item.title}"


print(Library().checkout(Member(), Book("River Road")))
Output
Checked out River Road

The stubs let you run and test the shape of the program on day one. Later you replace each stub with the real class (Member with the three-item limit, Book inheriting from LibraryItem) and the same Library.checkout() keeps working.

Facade Pattern

A design pattern is a proven, reusable solution to a common design problem. The facade pattern provides one simplified interface to a complex set of classes, hiding the internal complexity from the user. A single facade class coordinates the internal classes, so the rest of the program deals only with the facade. This reduces coupling: the client no longer depends on every subsystem class, and the subsystems can change without the client noticing.

Checking out a book involves three subsystems: checking the member's card, finding the item in the catalogue and recording the loan. A LibraryDesk facade lets the client do all three with one call.

Python — a facade over three subsystems
class MemberRegistry:                       # subsystem 1
    def __init__(self):
        self._cards = {"M1": "Priya", "M2": "Tom"}

    def is_valid(self, member_id):
        return member_id in self._cards


class Catalogue:                            # subsystem 2
    def __init__(self):
        self._titles = {"B1": "River Road", "B2": "Salt Country"}

    def find(self, item_id):
        return self._titles.get(item_id)


class LoanRecorder:                         # subsystem 3
    def __init__(self):
        self._loans = []

    def record(self, member_id, item_id):
        self._loans.append((member_id, item_id))


class LibraryDesk:                          # the FACADE: one simple interface
    def __init__(self):
        self._registry = MemberRegistry()
        self._catalogue = Catalogue()
        self._recorder = LoanRecorder()

    def borrow(self, member_id, item_id):
        if not self._registry.is_valid(member_id):
            return "Invalid member card"
        title = self._catalogue.find(item_id)
        if title is None:
            return "No such item"
        self._recorder.record(member_id, item_id)
        return f"Loan recorded: {title}"


desk = LibraryDesk()                        # the client only ever meets the facade
print(desk.borrow("M1", "B2"))
print(desk.borrow("M9", "B1"))
print(desk.borrow("M2", "B7"))
Output
Loan recorded: Salt Country
Invalid member card
No such item

The client never creates or calls MemberRegistry, Catalogue or LoanRecorder. If the catalogue is later moved from a dictionary to a database, only LibraryDesk changes. Use a facade when a group of classes is complicated to use directly; do not add one when a single class would do, because an unnecessary layer is extra code to maintain. The Library.checkout() method in the full program later on this page is a small facade of the same kind.

Agility

Agility in OOP design refers to implementing an iterative development cycle where code is regularly reviewed and updated based on testing, user feedback, and changing requirements. Agile approaches favour frequent refactoring, continuous improvement, and adaptive design rather than rigid upfront planning. This flexibility allows software to evolve as understanding of the problem deepens.

OOP suits an agile approach because well-separated classes can be changed one at a time. A possible sequence for the library:

  1. Iteration 1: a Book class that can be checked out and returned, with tests. Show it to the librarian.
  2. Iteration 2: feedback says DVDs are needed. Duplicated code appears, so generalise it into LibraryItem and re-run the tests to prove nothing broke.
  3. Iteration 3: feedback asks for a loan limit per member. Add it inside Member.borrow(); no other class changes.

Each pass is short, is tested and is checked with the client, so wrong assumptions are found early and cheaply. Agile does not mean "no design": the class diagram is still drawn, but it is updated as the design changes. See the SDLC Guide for how agile compares with a plan-driven approach.

Exam tip: "Describe" A describe question wants the features of the process in a sensible order, not an evaluation. Name each stage (task definition, top-down and bottom-up, facade, agility), say what it involves in one sentence and tie it to an example from the scenario, for instance "top-down: write Library.checkout() first using stubs". If a question names a pattern, say what problem it solves and how the classes are arranged: for a facade, "one class gives a simple interface to several subsystem classes". Do not confuse top-down and bottom-up, and do not describe a facade as inheritance.

Assessing the Effectiveness of Code

🎯 (SE-11-07)

📌 Assess the effectiveness of programming code developed to implement an algorithm

To assess code, you make a judgement about how well it implements the algorithm, backed by evidence from testing and reading the code. Use the criteria below, and say which ones matter most for the task.

Evaluation Criteria

  • Correctness: Does the code produce the expected output for all valid inputs? Does it correctly implement the algorithm's logic?
  • Efficiency: Does the code run efficiently in terms of time complexity (execution speed) and space complexity (memory usage)? Can it handle large datasets?
  • Maintainability: Is the code well-structured, clearly commented, and easy for other developers to understand and modify? Does it follow OOP principles?
  • Robustness: Does the code handle edge cases and errors gracefully without crashing?
  • Scalability: Can the code be extended with new features or adapted to larger problems without major restructuring?

In OOP, effectiveness is enhanced through proper use of classes, inheritance, and encapsulation. A well-designed class hierarchy makes code more testable, reusable, and maintainable. Poor design, such as overly deep inheritance chains or god objects (classes that do too much), reduces effectiveness.

Where the evidence comes from

CriterionEvidence you can gather
CorrectnessA test table of normal, boundary and abnormal inputs with expected and actual results (see the testing section).
EfficiencyCount the steps the algorithm takes for a small and a large input, or time it, and say how it grows.
MaintainabilityRead the code: clear names, one task per method, no duplicated blocks, private data behind methods, comments that explain why.
RobustnessTry invalid input (empty, negative, wrong type, missing item) and see whether the program fails safely with a useful message.
ScalabilityAsk what it would take to add a new item type or ten thousand records. If the answer is "edit five methods", the design does not scale.

Worked assessment: two ways to find a member

Riverbend Library needs a method that finds a member by id. Two developers implement it. Both pass the same tests, so both are correct. The step counts below are measured by the program itself, so they are the same every time it is run.

Python — comparing two implementations by counting steps
class Member:
    def __init__(self, member_id, name):
        self.member_id = member_id
        self.name = name


def find_in_list(members, member_id):          # Version A: scan the list
    steps = 0
    for m in members:
        steps += 1
        if m.member_id == member_id:
            return m, steps
    return None, steps


def find_in_dict(index, member_id):            # Version B: look up in a dictionary
    return index.get(member_id), 1


for size in (10, 1000):
    members = [Member(f"M{i}", f"Person {i}") for i in range(size)]
    index = {m.member_id: m for m in members}
    last = f"M{size - 1}"                      # worst case for the list
    _, a = find_in_list(members, last)
    _, b = find_in_dict(index, last)
    print(f"{size} members: list needs {a} steps, dictionary needs {b}")

print("empty library:", find_in_list([], "M1")[0], find_in_dict({}, "M1")[0])   # robustness
Output
10 members: list needs 10 steps, dictionary needs 1
1000 members: list needs 1000 steps, dictionary needs 1
empty library: None None

An assessment of these two might read: Both versions are correct and both handle an empty library by returning None (robustness). Maintainability is similar, because each is short and clearly named. Efficiency differs: the list version's steps grow with the number of members, whereas the dictionary version takes one step however large the library gets. Because a public library could have thousands of members, Version B is the more effective implementation, at the cost of keeping the dictionary in step with the list if members are added or removed. Notice the pattern: a claim, evidence, and a weighed judgement that includes a trade-off.

Design problems that reduce effectiveness in OOP

ProblemWhat it looks likeEffectFix
God objectOne Library class that stores data, validates input, prints reports and sends emailsHard to test and to change; every edit risks breaking something unrelatedSplit into classes with one responsibility each
Public dataOther code writes member.loans = [] directlyRules such as the three-item limit can be bypassedEncapsulate; change state only through methods
Duplicated code in subclassesBook and DVD both copy the same check-out logicA fix has to be made in several placesGeneralise into a superclass
Wrong or very deep inheritanceA Member inheriting from LibraryItem to reuse one methodConfusing design; changes to the parent break unrelated childrenUse inheritance only for a true is-a relationship; otherwise use composition
Tight couplingA class reaches into another class's internal attributesChanging one class forces changes in the otherSend messages through public methods
Scenario Two students implement "find a student by ID". Version A loops through a list of 2,000 students every time. Version B stores students in a dictionary keyed by ID. Both are correct, but B is far more efficient (one lookup instead of up to 2,000 comparisons) and just as readable. B is the more effective implementation.
HSC writing sample "The code is effective because it produced the expected output for all 12 test cases, including boundary values. However, its efficiency is limited: the search method is O(n), and replacing the list with a dictionary would make lookups constant-time for large class sizes."
Exam tip: "Assess" An assess question wants a judgement about how well the code works, supported by evidence. Pick the criteria that matter for the scenario (usually correctness first, then one or two others), give a piece of evidence for each (a test result, a step count, a design feature) and finish with a clear overall judgement such as "effective, but limited by...". Marks are lost for a list of criteria with no verdict, for judging efficiency without evidence, and for saying code is "good" without naming what makes it so.

Message-Passing Between Objects

🎯 (SE-11-08)

📌 Investigate how OOP languages handle message-passing between objects

In an object-oriented program, objects work by asking each other to do things. When Object A calls a method on Object B, Object A is sending a message to Object B requesting it to perform a specific action. The syntax looks like objectB.methodName(arguments). This allows for decoupling: Object A needs to know what Object B can do (its interface), but not how it does it (its implementation).

Anatomy of a message

PartIn account.withdraw(500)Role
ReceiveraccountThe object that receives the message and decides how to respond.
Method namewithdrawNames the request. Together with the parameters it forms the receiver's interface.
Arguments500Extra information the receiver needs (parameter passing).
Replythe returned True or FalseWhat comes back to the sender. A method with no return replies with None.
Python — one object sends a message to another
class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        # Message received: "withdraw this amount from me"
        if amount <= self.balance:
            self.balance -= amount
            return True
        return False


account = BankAccount(800)
result = account.withdraw(500)     # the caller sends the message and gets a reply
print(result, account.balance)
print(account.withdraw(500), account.balance)   # same message, different reply
Output
True 300
False 300

How Python handles a message

Python decides what a message means at run time. When the line item.describe() runs, Python looks at the class of the object that item refers to, searches that class and then its parent classes for a method called describe, and runs the first one it finds. This is called dynamic dispatch. It is the mechanism that makes polymorphism work: the same message goes to different receivers and each finds its own method. If no class in the chain has the method, Python raises an AttributeError, which is the receiver saying "I do not understand that message".

Python — the same message, different receivers, and one that cannot answer
class Book:
    def describe(self):
        return "a book"

class DVD:
    def describe(self):
        return "a DVD"

class Chair:                       # no describe() method at all
    pass

for receiver in (Book(), DVD(), Chair()):
    try:
        print(receiver.describe())
    except AttributeError as err:
        print("Not understood:", err)
Output
a book
a DVD
Not understood: 'Chair' object has no attribute 'describe'

Objects collaborating through messages

A real operation is a chain of messages. When the librarian asks the library to check out an item, the library asks the member to borrow it, the member asks the item to check itself out, and each replies to whoever called it. No object reaches inside another to change its data. This trace uses the classes from the full program in the design section:

StepSenderMessageReceiverReply
1main programcheckout("M1", "B1")Librarya message string (at the end)
2Libraryborrow(item)MemberTrue or False
3Membercheck_out()LibraryItemTrue (item was free) or False (already on loan)
4Memberadds the item to its own loan list, then replies to the Library True
5Libraryloan_days() and titleLibraryItem14 and "River Road", used to build the reply for step 1

Tell, don't ask

Good message passing tells an object to do something rather than asking for its data and then making the decision for it. The first version below breaks encapsulation, because the caller has to know how the loan list works and repeats the rule; the second sends one message and leaves the rule inside Member.

Python — asking versus telling
class Member:
    MAX_LOANS = 3
    def __init__(self):
        self.loans = []                        # public: anyone can reach in

    def borrow(self, item):                    # the rule lives here
        if len(self.loans) >= Member.MAX_LOANS:
            return False
        self.loans.append(item)
        return True


m = Member()
for title in ("A", "B", "C"):
    m.borrow(title)

# ASK: the caller looks inside and repeats the rule, but writes <= instead of <
if len(m.loans) <= 3:
    m.loans.append("D")
print("asking version:", len(m.loans), "loans")     # the limit has been broken

# TELL: one message; Member applies its own rule.
print("telling version:", m.borrow("E"))
Output
asking version: 4 loans
telling version: False

The "asking" version copied the rule into the caller and got it slightly wrong, so the member ended up with four items. Every place that copies a rule is another place for that mistake. In the "telling" version the rule exists once, inside borrow(), and the request was correctly refused. Making loans private as well would stop code from bypassing borrow() altogether.

Benefits and limits

This investigated feature enables several OOP benefits:

  • Modularity: Each object is independent and can be tested, modified, or replaced without affecting other objects that use its interface.
  • Flexibility: Polymorphism allows different objects to respond to the same message in different ways based on their class type.
  • Scalability: New classes can be added without modifying existing code that sends messages through a common interface.
  • Maintainability: Changes to an object's internal implementation don't affect other objects as long as the interface (method signatures) remains the same.

There are limits and costs. Each message is a function call, so a design with very many tiny objects sending long chains of messages can be harder to follow and slightly slower. A change to a method's name or parameters is a change to the contract, and every sender must be updated. In a dynamically typed language such as Python, a misspelt message is only discovered when that line runs, which is one more reason for tests. Weigh these against the gains: for most systems the clarity of small, well-defined interfaces is worth far more than the cost of the calls.

Sequence diagram

Objects passing messages

The client asks the account to withdraw; the account checks with a validator and replies.

What to notice
  • Each arrow is a message: one object calling another object's method.
  • The client never sees how the balance is checked, only the result.

Try thisAdd a message that records the withdrawal in a TransactionLog object.

Exam tip: "Investigate" An investigate question wants you to examine how something works and what follows from it. Define message passing accurately (one object calls a method on another, which handles it and may reply), show it with a specific call from the scenario such as account.withdraw(500), explain how the language handles it (the method is found in the receiver's class at run time, which is why the same message can produce different results), and give at least one benefit and one limitation. A frequent mistake is describing message passing as "sending data" without saying that the receiver runs a method.

Code Optimisation

🎯 (SE-11-08)

📌 Explain code optimisation in software engineering

Code optimisation means changing a program so that it uses fewer resources (time, memory, storage or network) while producing exactly the same results. Optimised code performs better on the target hardware, consumes fewer resources, and provides a better user experience. The important word is same: if the behaviour changes, that is a new feature or a bug, not an optimisation.

Measure first

Programmers are notoriously poor at guessing which part of a program is slow. The professional approach is to profile the code, meaning to measure where the time or memory actually goes, and then to optimise only that part. Python has the time module for simple timing and cProfile for a full breakdown by function.

Python — time the two lookups before choosing one
import time

names = [f"Person {i}" for i in range(20000)]
lookup = {name: True for name in names}
target = "Person 19999"                          # worst case for the list

start = time.perf_counter()
for _ in range(200):
    target in names                              # scans the list
list_time = time.perf_counter() - start

start = time.perf_counter()
for _ in range(200):
    target in lookup                             # one hash lookup
dict_time = time.perf_counter() - start

print("dictionary faster:", dict_time < list_time)   # exact times vary between computers
Output
dictionary faster: True

The exact times differ from computer to computer, so a good report compares ratios or shows the timing method rather than quoting one number.

Optimisation Techniques in OOP

  • Algorithm Refinement: Improving the efficiency of algorithms within methods by choosing faster data structures or reducing iterations. Replacing a list scan with a dictionary lookup, as in the worked assessment earlier, is the classic case.
  • Lazy Instantiation: Creating objects only when needed, rather than upfront, to reduce memory overhead and start-up time.
  • Caching: Storing results of expensive operations to avoid recalculating them.
  • Avoiding repeated work: Moving a calculation that gives the same answer every time out of a loop, or storing a value in a variable instead of asking for it again.
  • Inheritance Hierarchy Optimisation: Keeping inheritance chains as shallow as the design allows. In Python a call walks up the chain until it finds the method, which costs very little, so the main gain from a shallow hierarchy is that the code is simpler to read and change.
  • Code Profiling: Identifying bottlenecks (slow parts) in code using profiling tools to target optimisation efforts where they matter most.
Python — lazy instantiation: build the expensive object only if it is needed
class ReportGenerator:
    def __init__(self):
        print("  (building the report generator: slow, uses memory)")

    def make(self, title):
        return f"Report: {title}"


class Library:
    def __init__(self):
        self._reports = None                   # nothing built yet

    @property
    def reports(self):
        if self._reports is None:              # build on first use only
            self._reports = ReportGenerator()
        return self._reports


lib = Library()
print("Library created")                       # no ReportGenerator yet
print(lib.reports.make("Overdue items"))       # built now
print(lib.reports.make("New members"))         # reused, not rebuilt
Output
Library created
  (building the report generator: slow, uses memory)
Report: Overdue items
Report: New members

Before and after

Python — caching an expensive result
class Report:
    def __init__(self, marks):
        self.marks = marks
        self._average = None                 # not calculated yet

    def average(self):
        if self._average is None:           # calculate once…
            self._average = sum(self.marks) / len(self.marks)
        return self._average                 # …then reuse it

If a web page calls average() 50 times, the original version adds up every mark 50 times; the optimised version does it once. The output is the same, but the program does less work. The cost is that the stored value goes out of date if marks changes, so a cached class must reset _average whenever the data it depends on changes. Python also provides functools.lru_cache to cache the results of a function automatically.

Python — the same idea using functools.lru_cache
from functools import lru_cache

calls = 0

@lru_cache(maxsize=None)
def loan_fee(days_late):
    global calls
    calls += 1                                   # counts real calculations
    return round(0.25 * days_late, 2)


for _ in range(1000):
    loan_fee(8)                                  # 1000 requests for the same answer

print(loan_fee(8), "calculated", calls, "time(s)")
Output
2.0 calculated 1 time(s)

Structure supports optimisation

Well-structured code is easier to optimise. Because each method does one task, a profiler points at one small method rather than at a 200-line function, and because callers use only the interface, the method's insides can be replaced without touching anything else.

Python — readable structure: small methods and a private helper
class SystemManager:
    def __init__(self):
        self.active = True # Initialise state

    def run_task(self): # One logical task per subroutine
        if self.active:
            self.__log_event() # Modular call

    def __log_event(self): # Private helper
        pass # Ease of maintenance

The trade-off: speed against readability

Optimisation has costs: more code, more complexity and often more bugs. Premature optimisation, tuning code before you know it is slow, wastes effort and makes code harder to understand without providing meaningful performance gains. A sensible order is: make it correct, make it clear, measure, and only then make it faster where the measurements say it matters. Keep the tests from earlier so that you can prove each optimisation left the results unchanged.

Exam tip: "Explain" An explain question wants each idea linked to its reason. Name a technique, say what it does, and say why it improves performance, using a short example such as "caching stores the average after the first calculation, so fifty calls do the addition once instead of fifty times". Mention profiling as the way to find where to optimise, and note the trade-off with readability. A common mistake is to say optimisation makes code "better" without stating which resource (time or memory) is saved.

OOP Features That Support Collaboration

🎯 (SE-11-01, SE-11-06)

📌 Outline the features of OOP that support collaborative code development

  • consistency
  • code commenting
  • version control
  • feedback

Large programs are written by teams, and even a solo student project is read by a teacher, a partner and your own future self. OOP helps because a program is divided into classes with clear interfaces, so different people can build different classes at the same time and agree only on the interface. The four supports below are the ones the syllabus names.

Consistency

Using classes and standard naming conventions ensures the codebase is predictable for all team members. A team agrees the conventions once and everyone follows them, so that any class looks familiar even if someone else wrote it. Python's community style guide (PEP 8) is a common choice. Consistency also means the same structure for every class (constructor first, then properties, then methods), one class per file in a larger project, and the same way of handling errors.

ItemConventionExample
Class namesPascalCase, a nounLibraryItem, Member
Method and variable namessnake_case, methods are verbscheck_out(), loan_count
ConstantsUPPER_SNAKE_CASEMAX_LOANS
Internal attributesLeading underscore (or two for name mangling)_title, __loans

The class diagram is also a consistency tool: it is the team's agreed contract. If the diagram says Member has borrow(item), the person writing Library can call it before it exists, using a stub (see the implementation section).

Code Commenting

Professional documentation within classes (docstrings) explains the purpose of methods to other developers. Comments describe why the code does something and anything that is not obvious; they should not repeat what the code plainly says. A docstring is a string placed first inside a class or method. Python stores it, so tools and other programmers can read it without opening the source file.

Python — a docstring that helps teammates
class Booking:
    """A single room booking.

    Attributes:
        room (str): room code, e.g. "L204"
        hours (int): length of the booking in whole hours
    """

    def __init__(self, room, hours):
        self.room = room
        self.hours = hours

    def cost(self, rate):
        """Return the cost in dollars: hours multiplied by the hourly rate."""
        return self.hours * rate


help(Booking.cost)                       # a teammate can read this without the source
print(Booking("L204", 3).cost(12.5))
Output
Help on function cost in module __main__:

cost(self, rate)
    Return the cost in dollars: hours multiplied by the hourly rate.

37.5

Version Control

Tools like Git allow multiple engineers to work on different classes simultaneously and merge their work into a single repository. Every change is saved as a commit with a message, the full history is kept, and any earlier version can be restored. Because OOP puts each class in its own file, two people editing different classes rarely change the same lines, which keeps merge conflicts rare.

Git commandWhat it does
git clone <url>Copies the shared repository to your computer.
git switch -c add-dvd-classCreates a branch so your work does not disturb others.
git add dvd.py then git commit -m "Add DVD subclass with 7 day loan"Saves a snapshot with a clear message.
git pushShares your branch. A pull request then asks a teammate to review it before it is merged.
git pullBrings teammates' merged changes to your computer.

Feedback

Peer reviews of class structures and logic ensure that code is modular, reusable, and free of logic errors. Feedback also comes from test results, from the client trying an iteration, and from pair programming (two people at one computer, one typing and one reviewing). Good feedback is specific and about the code, not the person. A short checklist keeps a review of OOP code focused:

  • Does each class have one clear responsibility, and is its name a noun that says what it is?
  • Is data private, with changes made only through methods?
  • Does inheritance pass the is-a test, and is duplicated code generalised?
  • Do methods do one task and have names that are verbs?
  • Are there docstrings and tests, and do the tests pass?
Exam tip: "Outline" An outline question wants the main points briefly, so give each feature a heading word and one sentence that ties it to OOP specifically, then an example. For instance: "Version control: Git lets team members edit different classes on separate branches and merge them, so nobody overwrites another person's work." Do not just define the term. Marks are lost when an answer describes teamwork in general (communication, meetings) instead of features of the code and its tools.
Syllabus Part 2 Programming in OOP

Designing and Implementing OOP Programs

🎯 (SE-11-02)

📌 Design and implement computer programs involving branching, iteration and functions in an OOP language for an identified need or opportunity

The Course Specifications expect you to define classes, objects, attributes and methods, use inheritance, polymorphism and encapsulation, perform message passing, and use control structures and variables. This section puts all of that together in one complete, runnable program for an identified need: the Riverbend Library statement from the design section.

Program Structure with Control Flow

Effective OOP programs combine three fundamental control structures:

  • Branching: Using if/elif/else (or match in modern Python) to make decisions based on object state or input. For example, Member.borrow() checks whether the member is at their limit and whether the item is free.
  • Iteration: Using for and while loops to process collections of objects or repeat operations. For example, looping through every item to print a report.
  • Functions (Methods): Encapsulating logic in methods that accept parameters and return results. Each method should perform one logical task.

Real-World Application

When designing OOP programs for actual needs, engineers must:

  1. Identify the objects in the problem domain (e.g., Student, Course, Enrolment for a school system).
  2. Define classes with appropriate attributes and methods.
  3. Design relationships between classes (inheritance, composition).
  4. Implement logic using branching and iteration within methods.
  5. Test each class independently before integration.

Library Management System — Class Diagram

A Library Management System is an ideal HSC project because it has multiple entity types with clear IS-A and HAS-A relationships. The first-cut diagram below shows how Book, Member, and Library interact. It uses + and - visibility marks, which come from UML; the Course Specifications notation leaves them out.

Class diagram

Class diagram: a library system

The classes of a library system and how they relate.

What to notice
  • Multiplicities such as 1 and 0..* show how many objects take part in a relationship.
  • Inheritance arrows point from the child class to the parent class.

Try thisAdd a Loan class between Member and Book. What multiplicities would it have?

Writing the code exposed something the first cut missed: books and DVDs share most of their data and behaviour. So the design was generalised, and the refined diagram from the modelling section (Book, DVD and ReferenceBook inheriting from LibraryItem) is the one implemented below. Revising a diagram after learning something is a normal part of design.

Python Implementation: the complete program

The classes below use encapsulation (private data, properties), abstraction (an abstract LibraryItem), inheritance (Book, DVD and ReferenceBook), polymorphism (each subclass answers loan_days() and describe() in its own way) and message passing (the library asks the member, who asks the item). The mainline at the bottom is short: it creates objects and sends messages.

Python — Riverbend Library: the complete program
from abc import ABC, abstractmethod


class LibraryItem(ABC):
    """Anything the library lends. Abstract: only real item types can be created."""

    def __init__(self, item_id, title):
        self._item_id = item_id
        self._title = title
        self._on_loan = False

    @property
    def item_id(self):
        return self._item_id

    @property
    def title(self):
        return self._title

    @property
    def on_loan(self):
        return self._on_loan

    @abstractmethod
    def loan_days(self):
        """How many days this kind of item may be kept."""

    def check_out(self):
        if self._on_loan:                  # branching
            return False
        self._on_loan = True
        return True

    def check_in(self):
        self._on_loan = False

    def describe(self):
        status = "on loan" if self._on_loan else "available"
        return f"{self._title} [{status}]"


class Book(LibraryItem):
    def __init__(self, item_id, title, author):
        super().__init__(item_id, title)
        self._author = author

    def loan_days(self):
        return 14

    def describe(self):
        return f"{super().describe()} by {self._author}"


class ReferenceBook(Book):
    def check_out(self):
        return False                       # reference books never leave the library


class DVD(LibraryItem):
    def __init__(self, item_id, title, minutes):
        super().__init__(item_id, title)
        self._minutes = minutes

    def loan_days(self):
        return 7

    def describe(self):
        return f"{super().describe()}, {self._minutes} min"


class Member:
    MAX_LOANS = 3                          # class attribute: the same rule for everyone

    def __init__(self, member_id, name, email):
        self._member_id = member_id
        self._name = name
        self.email = email                 # runs the validating setter
        self.__loans = []                  # private

    @property
    def member_id(self):
        return self._member_id

    @property
    def name(self):
        return self._name

    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        if "@" not in value:
            raise ValueError("email must contain @")
        self._email = value

    @property
    def loan_count(self):
        return len(self.__loans)

    def borrow(self, item):
        if len(self.__loans) >= Member.MAX_LOANS:
            return False
        if not item.check_out():           # message passing: ask the item
            return False
        self.__loans.append(item)
        return True

    def give_back(self, item):
        if item in self.__loans:
            self.__loans.remove(item)
            item.check_in()


class Library:
    def __init__(self):
        self._items = {}                   # data structure: dictionary keyed by id
        self._members = {}

    def add_item(self, item):
        self._items[item.item_id] = item

    def join(self, member):
        self._members[member.member_id] = member

    def checkout(self, member_id, item_id):
        member = self._members.get(member_id)
        item = self._items.get(item_id)
        if member is None or item is None:
            return "Unknown member or item"
        if member.borrow(item):
            return f"{member.name} borrowed {item.title} for {item.loan_days()} days"
        return f"{member.name} could not borrow {item.title}"

    def report(self):
        for item in self._items.values():  # iteration
            print(item.describe())         # polymorphism: each item describes itself


def main():
    library = Library()
    library.add_item(Book("B1", "River Road", "A. Nguyen"))
    library.add_item(DVD("D1", "Star Coast", 95))
    library.add_item(ReferenceBook("R1", "Atlas of Australia", "P. Smith"))
    library.join(Member("M1", "Priya", "priya@example.com"))

    print(library.checkout("M1", "B1"))
    print(library.checkout("M1", "D1"))
    print(library.checkout("M1", "R1"))
    print(library.checkout("M1", "B1"))
    print(library.checkout("M9", "B1"))
    library.report()


main()
Output
Priya borrowed River Road for 14 days
Priya borrowed Star Coast for 7 days
Priya could not borrow Atlas of Australia
Priya could not borrow River Road
Unknown member or item
River Road [on loan] by A. Nguyen
Star Coast [on loan], 95 min
Atlas of Australia [available] by P. Smith

Reading the program against the design

Requirement from the task definitionWhere the code meets it
A member can borrow up to three itemsMember.MAX_LOANS checked in borrow() (branching)
Books 14 days, DVDs 7Each subclass overrides loan_days()
Reference books are never borrowedReferenceBook overrides check_out() to return False. No other class changed.
An item cannot be borrowed twiceThe _on_loan flag, tested in LibraryItem.check_out()
Staff can print what is on the shelvesLibrary.report() loops (iteration) and calls describe() on each item
Bad input is handledAn unknown id returns a message instead of crashing; a bad email raises ValueError

Extending the program

A design is good if new needs can be met without rewriting what works. The library now buys audio books, lent for 21 days. Because Library only sends the messages loan_days() and describe(), the only new code is one subclass. Nothing in Library, Member or LibraryItem is edited.

Python — a new item type: one new class, no edits elsewhere
class AudioBook(Book):
    def __init__(self, item_id, title, author, hours):
        super().__init__(item_id, title, author)
        self._hours = hours

    def loan_days(self):
        return 21

    def describe(self):
        return f"{super().describe()}, {self._hours} h audio"


library = Library()
library.add_item(AudioBook("A1", "Salt Country", "M. Okafor", 9))
library.join(Member("M2", "Tom", "tom@example.com"))
print(library.checkout("M2", "A1"))
library.report()
Output
Tom borrowed Salt Country for 21 days
Salt Country [on loan] by M. Okafor, 9 h audio

OOP Design Checklist — Use This for Your HSC Project

  • Have you identified all objects/entities in your system?
  • Are attributes private with getters/setters where needed?
  • Is there appropriate use of inheritance (IS-A relationship)?
  • Is composition used for HAS-A relationships?
  • Are method names clear and do they follow single responsibility?
  • Have you drawn a UML class diagram in your documentation?

When NOT to Use OOP

  • Simple scripts (e.g., a one-off data conversion) don't need classes — plain functions are simpler and faster to write.
  • OOP shines when you have multiple similar objects that share structure but differ in state or behaviour (e.g., many Book objects in a library).
  • Data-heavy transformations (e.g., mathematical pipelines, batch processing) may suit a functional or procedural style more naturally.
  • For your HSC project: if your system involves 3 or more entity types that each have their own data and behaviour, OOP is the right choice and will earn better marks for design quality.
Exam tip: "Design and implement" Questions that ask you to design and implement a program for a stated need are marked on whether the design fits the need and the code works. Start from the scenario: list the classes, say which inherits from which, then write the code with each requirement traceable to a line (a limit becomes a branch, a list of objects becomes a loop, a repeated task becomes a method). Comment the important lines. Keep the mainline short and let objects do the work. Typical lost marks: data left public, logic in the mainline instead of in a method, an inheritance relationship that fails the is-a test, and code that has never been run. NESA's marker feedback on the 2025 HSC online exam (one exam only) asked for code "able to be executed in the coding environment", correct indentation, and functions called in the main program only after they are defined, so practise typing and running Python, not just reading it. If you are short of time, write the class and method headers with comments first (a skeleton) and fill in what you can.

Implementing and Modifying OOP Code

🎯 (SE-11-06)

📌 Implement and modify OOP programming code

  • clear and uncluttered mainline
  • one logical task per subroutine
  • use of stubs
  • use of control structures and data structures
  • ease of maintenance
  • version control
  • regular backup

Implementing a class and modifying existing code are different skills. Modifying is what most programmers do most of the time, and the practices below are what make it safe. Each one is shown on the Riverbend Library code.

Clear and Uncluttered Mainline

The main program should be clean and easy to read, delegating complex logic to helper methods. Avoid embedding business logic directly in the main method. Instead, create objects and call their methods. This separation of concerns makes the flow of the program obvious and maintainable. Compare the two mainlines below, which produce the same output.

Python — cluttered mainline: the rules are mixed into the main program
loans = {"M1": 2}
limit = 3
items = {"B1": True, "D1": False}          # True = available
member_id, item_id = "M1", "B1"

if member_id in loans:
    if item_id in items:
        if items[item_id]:
            if loans[member_id] < limit:
                items[item_id] = False
                loans[member_id] += 1
                print("Borrowed", item_id)
            else:
                print("Limit reached")
        else:
            print("Already on loan")
    else:
        print("No such item")
else:
    print("No such member")
Output
Borrowed B1
Python — uncluttered mainline: the objects hold the rules
def main():
    library = Library()
    library.add_item(Book("B1", "River Road", "A. Nguyen"))
    library.join(Member("M1", "Priya", "priya@example.com"))
    print(library.checkout("M1", "B1"))


main()
Output
Priya borrowed River Road for 14 days

The second mainline reads like a description of what the program does. The four levels of nested if have not vanished: they live in Library.checkout() and Member.borrow(), where each is small, named and can be tested alone.

One Logical Task Per Subroutine

Each method should do one thing and do it well (the Single Responsibility Principle). A method named process_loan() should handle the loan only, not also print receipts and update statistics. This makes code easier to understand, test and reuse, and it means a bug has fewer places to hide. If you have to use the word "and" to describe what a method does, split it.

Python — refactoring one method that does four jobs into four methods
class LoanDesk:
    def __init__(self):
        self.count = 0

    # BEFORE: validates, records, counts AND formats, all in one method
    def process_loan_v1(self, member, title, days):
        if days <= 0:
            return "Invalid loan period"
        self.count += 1
        return f"Loan {self.count}: {member} has {title} for {days} days"

    # AFTER: each method has one job
    def _is_valid(self, days):
        return days > 0

    def _record(self):
        self.count += 1
        return self.count

    def _receipt(self, number, member, title, days):
        return f"Loan {number}: {member} has {title} for {days} days"

    def process_loan_v2(self, member, title, days):
        if not self._is_valid(days):
            return "Invalid loan period"
        return self._receipt(self._record(), member, title, days)


desk1, desk2 = LoanDesk(), LoanDesk()
print(desk1.process_loan_v1("Priya", "River Road", 14))
print(desk2.process_loan_v2("Priya", "River Road", 14))     # same behaviour
print(desk2.process_loan_v2("Priya", "River Road", 0))
Output
Loan 1: Priya has River Road for 14 days
Loan 1: Priya has River Road for 14 days
Invalid loan period

Changing the structure of code without changing what it does is called refactoring. The identical output above is the proof that the behaviour was kept, which is why refactoring is always done with tests to run before and after.

Use of Stubs

A stub is a minimal implementation or placeholder for a subroutine, method, or class that will be fully developed later. Stubs allow high-level logic to be tested before all detailed methods are complete. This enables parallel development: one student can write Library against a stub while another writes the real Member. A stub should either return a fixed, believable value or say clearly that it is not finished, so that nobody mistakes it for real code.

Python — two kinds of stub
class Database:
    def save(self, record):                 # stub 1: fixed behaviour so callers can run
        print("Saving...", record)
        return True

    def load(self, record_id):              # stub 2: refuses to pretend
        raise NotImplementedError("load() is not written yet")


db = Database()
print(db.save({"item": "B1"}))
try:
    db.load("B1")
except NotImplementedError as err:
    print("Stub reached:", err)
Output
Saving... {'item': 'B1'}
True
Stub reached: load() is not written yet

Control Structures and Data Structures

Implementation must effectively use both control structures (if/else, loops) to direct program flow and data structures (arrays, lists, trees) to organise data. Choosing the right combination improves both efficiency and code clarity.

NeedStructureRiverbend example
An ordered group where the same kind of thing is added and removedListA member's loans (__loans)
Fast lookup by a keyDictionaryItems and members keyed by id
Membership only, no duplicatesSetIds of overdue items
Do something for every objectfor loopLibrary.report()
Repeat until a condition changeswhile loopA menu that runs until the librarian chooses Exit
Choose between actionsif/elif/elseThe checks inside borrow()

Ease of Maintenance

Code should be maintainable through:

  • Clear naming conventions for classes, methods, and variables.
  • Proper encapsulation, hiding implementation details behind clear interfaces.
  • Comprehensive comments explaining non-obvious logic.
  • Avoiding deep nesting and overly complex methods.
  • Following consistent formatting and style.
  • Named constants in place of "magic numbers", so a rule is changed in one place.

A maintenance task shows the payoff. The council extends loans on all books from 14 to 21 days. In the Riverbend code that is a change to one line inside Book.loan_days(), and every part of the program that asks for the loan period now gets the new answer. Had the number 14 been typed into several methods, one of them would almost certainly have been missed.

Python — a one-line modification
class Book(LibraryItem):
    def loan_days(self):
        return 21                          # was 14: the only line that changes

Version Control

Using version control systems (Git, GitLab, Bitbucket) is essential for OOP development. Version control allows multiple developers to work on different classes simultaneously, track changes, revert to previous versions if needed, and maintain an audit trail of who changed what and why. Commits should be frequent and small, each with a message that says what changed and why.

Weak commit messageBetter commit message
fixed stuffStop members borrowing a fourth item
updateAdd ReferenceBook: overrides check_out to refuse loans
final version 2Extend book loans from 14 to 21 days

Regular Backup

Beyond version control, regular backups of the entire codebase ensure protection against data loss from hardware failures. Backups should be stored off-site or in the cloud to protect against local disasters, and automated backups are preferred to ensure consistency and reliability. A common guideline is the 3-2-1 rule: three copies of the data, on two different kinds of storage, with one copy off-site. A backup is only proven when you have tested restoring from it.

Version controlBackup
Main purposeTrack and share changes; return to any earlier versionRecover the whole project after loss or damage
Protects againstA bad change; two people overwriting each otherA failed disk, theft, fire, accidental deletion, ransomware
Works if the computer is lost?Only if the repository is also hosted somewhere elseYes, if a copy is off-site
Exam tip: "Implement and modify" Expect a short piece of OOP code and a request such as "modify the class so that...". Read the whole class first, change only what the requirement needs, and keep the rest of the design intact: add a subclass or override rather than editing every caller. Then say why your change is safe: the interface did not change, so callers are unaffected. When a question asks about practices, link each one to a result (a clear mainline makes the program readable; one task per subroutine makes methods testable; stubs allow early testing; version control gives you history and a way to revert). Do not treat version control and backup as the same thing.

Testing and Evaluating Code

🎯 (SE-11-06, SE-11-07)

📌 Apply methodologies to test and evaluate code

  • unit, subsystem and system testing
  • black, white and grey box testing
  • quality assurance

Testing is how you gather the evidence that code works. It is done at several levels and with several viewpoints, and it is never finished on the first pass: every time code changes, the tests are run again. The examples below test the Riverbend Library program from the design section.

Testing Hierarchy: Unit, Subsystem, and System

Testing occurs at multiple levels, building from smallest to largest components:

Unit Testing

Testing individual classes or methods in isolation to ensure they perform their intended function correctly. For example, testing a Member.borrow() method with various inputs (a free item, an item already on loan, a member already at the limit). Unit tests are the most granular and should be written by the developer who wrote the code.

A unit test can be as simple as a function that sets up an object, sends it a message and uses assert to state what must be true. If the condition is false, Python raises an AssertionError and the test fails. Good unit tests cover normal data, boundary data (at the edge of a rule) and abnormal data (invalid input).

TestKind of dataInputExpected result
A new item is availableNormalNew Bookon_loan is False
Borrow a free bookNormalMember with 0 loansTrue; loan count 1
Third item allowedBoundary (last allowed)Member borrows 3 itemsAll three succeed
Fourth item refusedBoundary (first refused)Member borrows a 4thFalse; count stays 3
Reference bookAbnormal for a loanAny member borrows itFalse
Invalid emailAbnormal"no-at-sign"ValueError raised
Python — unit tests using simple asserts
def test_new_item_is_available():
    assert Book("B1", "River Road", "A. Nguyen").on_loan is False


def test_borrow_free_book():
    member = Member("M1", "Priya", "p@example.com")
    book = Book("B1", "River Road", "A. Nguyen")
    assert member.borrow(book) is True
    assert member.loan_count == 1


def test_limit_boundary():
    member = Member("M1", "Priya", "p@example.com")
    for n in range(3):
        book = Book(f"B{n}", "Title", "Author")
        assert member.borrow(book) is True, f"item {n + 1} should be allowed"
    extra = Book("B9", "Title", "Author")
    assert member.borrow(extra) is False, "fourth item should be refused"
    assert member.loan_count == 3


def test_reference_book_never_lent():
    member = Member("M1", "Priya", "p@example.com")
    atlas = ReferenceBook("R1", "Atlas", "P. Smith")
    assert member.borrow(atlas) is False


def test_bad_email_rejected():
    try:
        Member("M1", "Priya", "no-at-sign")
    except ValueError:
        return                                # the expected outcome
    assert False, "ValueError was not raised"


tests = [test_new_item_is_available, test_borrow_free_book, test_limit_boundary,
         test_reference_book_never_lent, test_bad_email_rejected]
passed = 0
for test in tests:
    try:
        test()
        print("PASS", test.__name__)
        passed += 1
    except AssertionError as err:
        print("FAIL", test.__name__, err)
print(f"{passed} of {len(tests)} tests passed")
Output
PASS test_new_item_is_available
PASS test_borrow_free_book
PASS test_limit_boundary
PASS test_reference_book_never_lent
PASS test_bad_email_rejected
5 of 5 tests passed

The tests exercise the objects only through their public methods and properties (borrow(), loan_count), never by reaching into private data. That keeps them valid when the internals are refactored.

A failing test is useful because it points at the fault. Here a developer wrote the limit check with the wrong comparison, and the boundary tests catch it. Boundary values are where off-by-one errors live, so always test on both sides of the edge.

Python — a boundary test that finds a bug
def can_borrow(current_loans, limit=3):
    return current_loans <= limit          # BUG: should be < (allows a fourth item)


def test_can_borrow():
    assert can_borrow(0) is True, "0 loans: should be allowed"
    assert can_borrow(2) is True, "2 loans: third item should be allowed"
    assert can_borrow(3) is False, "3 loans: fourth item should be refused"


try:
    test_can_borrow()
    print("PASS")
except AssertionError as err:
    print("FAIL:", err)
Output
FAIL: 3 loans: fourth item should be refused

Subsystem Testing

Testing groups of related classes that work together as a functional subsystem. For example, testing the interaction between Library, Member and LibraryItem to check that a checkout changes the state of both the member and the item. This level catches integration issues that unit tests might miss: each class can pass its own tests and still be called with the wrong arguments or in the wrong order by another.

Python — subsystem test: three classes working together
library = Library()
book = Book("B1", "River Road", "A. Nguyen")
priya = Member("M1", "Priya", "priya@example.com")
library.add_item(book)
library.join(priya)

message = library.checkout("M1", "B1")
assert message == "Priya borrowed River Road for 14 days"
assert book.on_loan is True                # the item changed...
assert priya.loan_count == 1               # ...and so did the member

priya.give_back(book)
assert book.on_loan is False and priya.loan_count == 0
print("Subsystem test passed")
Output
Subsystem test passed

System Testing

Testing the entire integrated application end-to-end to ensure all components work together and meet the overall requirements. System testing includes testing the application's interaction with external systems (databases, APIs, etc.) and verifying that the entire workflow functions as expected, using the program as a user would.

Python — system test: run the whole program and check what the user sees
import io
from contextlib import redirect_stdout

buffer = io.StringIO()
with redirect_stdout(buffer):              # capture everything the program prints
    main()
lines = buffer.getvalue().splitlines()

assert lines[0] == "Priya borrowed River Road for 14 days"
assert lines[2] == "Priya could not borrow Atlas of Australia"
assert lines[4] == "Unknown member or item"
assert len(lines) == 8
print("System test passed:", len(lines), "lines of output checked")
Output
System test passed: 8 lines of output checked

Testing Approaches: Black, White, and Grey Box

These describe how much the tester knows about the code, not how big the thing under test is. The same unit, Member.borrow(), can be tested in all three ways.

Black Box Testing

Testing inputs and outputs without access to or knowledge of the internal code structure. The tester treats the software as a "black box" and tests it purely based on specifications. This approach ensures the software meets functional requirements from a user's perspective. For example, using only the statement "a member can borrow up to three items", try borrowing one, three and four items and check the results, without reading borrow(). The unit tests above are black-box tests.

White Box Testing

Testing with complete access to and understanding of the internal code structure. The tester examines the code logic, control flow, and data flow to ensure all paths and conditions are tested. This approach is critical for security, ensuring all potential vulnerabilities and logic errors are identified. For borrow(), reading the code shows three paths, so at least three tests are needed to run every branch (path coverage):

Path through borrow()Test that reaches itResult
1. len(loans) >= MAX_LOANS is trueMember already holds 3 itemsFalse
2. Limit not reached, but check_out() returns FalseItem already on loan, or a ReferenceBookFalse
3. Both checks passFree item, member under the limitTrue, item appended

Grey Box Testing

A hybrid approach where the tester has partial knowledge of the internal structure (like knowing which classes exist and their general purpose) but not the implementation details. This allows more targeted testing than black box while being less resource-intensive than white box. For example, knowing that Member delegates to LibraryItem.check_out() and that the limit is a constant, the tester chooses the "item already on loan" and "third versus fourth item" cases deliberately, but still checks only through the public interface.

NESA glossary wording
  • Black box testing: "A testing technique where there is no knowledge of the internal workings of an application. A user will provide inputs and observe the outputs in order to determine functionality of the program."
  • White box testing: "A testing technique used to examine the internal workings of an application including the design, code and inner workings of software. This technique gives the ability to verify the flow of inputs and outputs through the application."
  • Grey box testing: "A testing technique where there is a partial knowledge of the internal workings of an application. Its purpose is to search for defects related to improper code structure or the improper use of functions. This technique gives the ability to test both the code and the presentation layer of an application."

Source: NESA, Software Engineering 11–12 syllabus glossary.

Testing objects: things to remember

  • Test through the interface. Check state with public methods and properties. A test that reads a private attribute breaks when the class is refactored.
  • Test polymorphism by looping. Run the same test over every subclass (Book, DVD, ReferenceBook), since each may override behaviour. A subclass must still honour what the superclass promises, unless its documentation says otherwise.
  • Isolate with stubs. To unit-test Library without a real Member, pass in a stub that returns fixed answers.
  • Re-run after every change. Regression testing means running the old tests again to prove new code did not break old behaviour. This is what makes refactoring safe.

Try it

Which kind of testing?

Practise naming levels and approaches of testing. Enable JavaScript to use this tool. In short: unit, subsystem and system testing describe what is tested; black, white and grey box describe how much of the code the tester can see.

Quality Assurance Process

Quality Assurance (QA) is the systematic process of ensuring software meets specified engineering standards and quality criteria. It looks at the whole process, not just at running tests. QA includes:

  • Requirements Verification: Ensuring the code implements all specified requirements (each line of the task definition has a passing test).
  • Code Review: Peer examination of code for logic errors, adherence to standards, and maintainability.
  • Automated Testing: Using test suites that run automatically on every code change to catch regressions.
  • Documentation: Ensuring code is properly documented so maintainability is assured.
  • Performance Testing: Verifying the code meets performance requirements under expected and peak loads.
  • Security Testing: Identifying and fixing security vulnerabilities. This is developed further in Year 12 (Secure Software Architecture).
Exam tip: "Apply" methodologies to test and evaluate Be ready to (a) design test data for a described method, labelled as normal, boundary or abnormal, each with an expected result, and (b) state which level or box type a described test is. Quick rules: one method or class alone is a unit test; several classes together is a subsystem test; the whole program from the user's side is a system test. Black box means the tester cannot see the code, white box means they can, grey box means partly. A full-mark test table always gives the expected output before running the code. Common mistakes: confusing the level of testing (unit, subsystem, system) with the box type (black, white, grey), and giving only normal data.