Programming Fundamentals for Engineers

Chapter 10
Object-Oriented and Interface-Based Design

Bachelors Program in Electrical Engineering
Programming Fundamentals for Engineers   •   Bachelor

Object-Oriented and Interface-Based Design

Introduction and Context

Engineering software increasingly represents physical devices, measurements, control algorithms, communication services, and human-machine interfaces. A program that begins as a small calculation can become difficult to maintain when new sensors, operating modes, hardware platforms, or safety requirements are added. Object-oriented and interface-based design provide ways to organize such software around meaningful responsibilities rather than around a long sequence of unrelated procedures.

Object-oriented programming (OOP) models a system as cooperating objects. An object combines state—data describing its current condition—with behavior—operations that use or modify that data. Interface-based design goes further by separating what a component promises from how it fulfills that promise. This allows software to depend on stable abstractions while implementations change.

The goal is not to use as many classes or patterns as possible. Good design uses the simplest structure that clearly expresses responsibilities, protects important state, supports testing, and permits necessary change.

Learning Objectives

By the end of this chapter, you should be able to:

  • explain classes, objects, attributes, methods, and constructors;
  • apply encapsulation to protect invariants and control access to state;
  • distinguish composition, inheritance, and interface implementation;
  • use polymorphism to substitute compatible components;
  • design small interfaces and generic abstractions;
  • evaluate coupling, cohesion, extensibility, and complexity;
  • model an engineering application using maintainable object-oriented software;
  • recognize limitations, safety concerns, and ethical implications of software abstractions.

1. The Object-Oriented Model

A class is a definition or blueprint describing the data and operations associated with a kind of object. An object is a runtime instance of a class. For example, a TemperatureSensor class may define a calibration offset, a communication address, and a method named read_celsius(). Several sensor objects may be created from that class, each with different addresses or calibration values.

Important terms include:

  • Attribute: data associated with an object.
  • Method: an operation defined by a class.
  • Constructor: an operation used to establish a valid initial state.
  • Identity: the fact that two objects are distinct entities, even if their values are equal.
  • State: the values held by an object at a particular time.
  • Behavior: the operations an object can perform.

A useful engineering abstraction is to ask:

  1. What responsibility does this object have?
  2. What information must it own to perform that responsibility?
  3. Which operations should be visible to other objects?
  4. Which details should remain hidden?

For example, a motor controller may expose set_speed() and stop(), while hiding pulse-width modulation calculations and fault-handling details.

A Simple Class Example

class Resistor:
    def __init__(self, resistance_ohms):
        if resistance_ohms <= 0:
            raise ValueError("Resistance must be positive")
        self._resistance_ohms = resistance_ohms

    def resistance(self):
        return self._resistance_ohms

The constructor validates an important condition: resistance must be positive. The leading underscore communicates that the attribute is an implementation detail. Python does not enforce private access in the same way as some languages, but the design intention remains clear.

2. Encapsulation and Invariants

Encapsulation combines data with the operations that govern it and restricts uncontrolled access to internal state. It is not merely the use of private variables. Its deeper purpose is to protect invariants: conditions that must remain true whenever an object is observable by the rest of the program.

For a battery model, possible invariants include:

  • state of charge is between 0% and 100%;
  • rated capacity is positive;
  • terminal voltage is not negative under the model assumptions;
  • a disconnected battery cannot accept a charging command.

If every caller can directly change attributes, each caller must remember these rules. Encapsulation places the rules near the data they protect.

A method should reject invalid input early and leave the object in a valid state. This is especially important in control, power, medical, and transportation software, where an invalid state may produce physical risk.

Encapsulation Is a Design Decision

Not every attribute requires a getter and setter. A setter that blindly assigns a value may provide the appearance of encapsulation without providing its benefits. Prefer operations that express meaningful actions:

class Battery:
    def __init__(self, capacity_ah):
        if capacity_ah <= 0:
            raise ValueError("Capacity must be positive")
        self._capacity_ah = capacity_ah
        self._charge_ah = 0.0

    def charge(self, amount_ah):
        if amount_ah < 0:
            raise ValueError("Charge amount cannot be negative")
        self._charge_ah = min(self._capacity_ah,
                              self._charge_ah + amount_ah)

    def state_of_charge(self):
        return self._charge_ah / self._capacity_ah

The class controls the upper bound on stored charge. The method state_of_charge() returns a ratio between 0 and 1 under the stated assumptions. It does not expose the internal stored-charge variable for arbitrary modification.

3. Composition: Building Systems from Parts

Composition means that one object contains or uses other objects. A PowerSystem may contain a battery, inverter, sensor collection, and fault monitor. The larger object delegates specialized work to its parts.

Composition is often the best default because it reflects physical systems and keeps responsibilities localized. It also allows parts to be replaced independently.

class Controller:
    def __init__(self, sensor, actuator):
        self._sensor = sensor
        self._actuator = actuator

    def execute(self):
        measurement = self._sensor.read()
        command = self.calculate_command(measurement)
        self._actuator.apply(command)

The controller does not need to know the electrical details of the sensor or actuator. It requires only that they support the operations it uses. This reduces coupling.

Composition can represent different relationships:

  • Ownership: an object creates and manages a component whose lifetime is closely related to its own.
  • Aggregation: an object uses components that may exist independently.
  • Delegation: an object forwards a responsibility to another object.

The distinction is conceptual rather than purely syntactic. A design should make lifetime, responsibility, and failure behavior clear.

4. Inheritance and Its Appropriate Use

Inheritance defines a relationship in which a subclass specializes a superclass. A subclass receives or reuses behavior from its parent and may add or override behavior.

For example, ThermocoupleSensor and ResistanceSensor might specialize a general Sensor abstraction. Inheritance can be useful when the subtype truly satisfies the behavioral expectations of the base type.

The central test is substitutability: wherever a base-type object is expected, can the subtype be used without surprising the client? If a ReadOnlySensor is expected to provide measurements, a subtype that sometimes returns unrelated diagnostic text violates the abstraction even if the programming language permits the inheritance.

Inheritance is risky when used only to reuse a few lines of code. It creates a strong coupling between parent and child. Changes in the parent can affect many subclasses, and deep hierarchies can be difficult to understand.

Composition Compared with Inheritance

ConcernCompositionInheritance
Main relationship“has a” or “uses a”“is a kind of”
CouplingUsually lowerUsually higher
Runtime replacementUsually easyOften less flexible
Reuse mechanismDelegation and collaborationReuse and specialization
Best useVariable parts and servicesStable, genuine subtype relationships

A practical rule is: favor composition unless inheritance communicates a stable domain relationship and preserves substitutability.

5. Polymorphism

Polymorphism means that one operation can work with objects of different concrete types through a common abstraction. The calling code sends the same message, while the receiving object supplies type-specific behavior.

Consider a data-acquisition system:

class Sensor:
    def read(self):
        raise NotImplementedError

class VoltageSensor(Sensor):
    def read(self):
        return 12.4

class CurrentSensor(Sensor):
    def read(self):
        return 2.1

def record(sensor):
    value = sensor.read()
    print(value)

The record() function does not need a conditional statement for every sensor model. It relies on the common operation read(). This reduces modification when a new sensor type is introduced.

Polymorphism can be:

  • Subtype polymorphism: different subclasses implement a common parent abstraction.
  • Interface polymorphism: unrelated classes implement the same interface.
  • Parametric polymorphism: one algorithm works with many data types through generic parameters.
  • Ad hoc polymorphism: one operation name has different implementations for different argument types, such as function overloading.

Polymorphism is valuable only when the abstraction is meaningful. A common method name does not automatically imply compatible semantics. Units, accuracy, timing, failure behavior, and side effects must also be documented.

6. Interfaces and Contracts

An interface specifies a contract: the operations a component provides and, ideally, their expected behavior. It describes what clients may rely upon without requiring knowledge of implementation details.

A useful interface contract states:

  • operation names and parameter meanings;
  • return values and units;
  • valid input ranges;
  • error or exception behavior;
  • timing and concurrency expectations;
  • side effects;
  • resource ownership and cleanup responsibilities.

In a statically typed language, an interface may be declared explicitly. In Python, a protocol or abstract base class can communicate a similar design. The following pseudocode illustrates the idea:

from typing import Protocol

class Sensor(Protocol):
    def read(self) -> float:
        """Return a measurement in the documented unit."""
        ...

class CalibratedSensor:
    def __init__(self, raw_sensor, offset):
        self._raw_sensor = raw_sensor
        self._offset = offset

    def read(self):
        return self._raw_sensor.read() + self._offset

Any object with a compatible read() operation may be used by a function that expects Sensor. The interface supports dependency injection: a real hardware sensor, simulator, or test double can be supplied without changing the client.

Interface Segregation

A large interface forces clients to depend on operations they do not use. A device interface containing read(), write(), calibrate(), configure_network(), and update_firmware() may be inappropriate for a simple measurement client. Smaller interfaces are easier to implement and test.

Instead, separate responsibilities such as:

class Readable(Protocol):
    def read(self): ...

class Calibratable(Protocol):
    def calibrate(self, reference): ...

A component may implement one or both. This arrangement avoids unnecessary obligations and makes dependencies explicit.

7. Generic Abstractions

A generic abstraction defines an algorithm or data structure independently of one particular data type. Examples include a queue of measurements, a repository of devices, or a filtering function that accepts numeric values.

Generics improve reuse while retaining clarity about the types of values involved. In a statically typed language, a generic repository might be represented conceptually as Repository<T>, where T is the stored entity type. In Python, type variables provide documentation and support by development tools:

from typing import Generic, TypeVar

T = TypeVar("T")

class Repository(Generic[T]):
    def __init__(self):
        self._items = []

    def add(self, item: T):
        self._items.append(item)

    def all(self) -> list[T]:
        return list(self._items)

Generic design should not be introduced merely to make code appear sophisticated. It is useful when the same behavior applies to multiple types and the abstraction remains understandable. Excessively general designs can hide domain meaning and increase cognitive load.

8. A Design Process for Engineering Software

A disciplined design process can be organized as follows.

Identify Responsibilities

List what the system must do, not the classes you expect to write. For a monitoring system, responsibilities may include acquiring measurements, validating data, detecting alarms, storing records, and displaying status.

Assign One Primary Responsibility

A class may collaborate with many components, but its purpose should be explainable in one sentence. A class that reads sensors, stores data, sends email, and draws a dashboard probably has low cohesion.

Define Boundaries

Determine which information belongs inside each object and which operations form its public contract. Hide volatile implementation details such as a vendor-specific communication protocol behind a stable interface.

Model Collaboration

Describe the sequence of interactions. For example:

  1. A scheduler requests a sample.
  2. A sensor gateway obtains raw data.
  3. A validator checks range and quality flags.
  4. An alarm service evaluates conditions.
  5. A repository stores the accepted record.

Choose Relationships Carefully

Use composition for replaceable parts, inheritance for genuine specialization, interfaces for stable contracts, and generics for repeated type-independent behavior.

Test the Abstraction

Ask whether a simulator can replace the hardware component. Ask whether invalid states are prevented. Ask whether a new implementation can be added without modifying unrelated clients. These questions reveal whether the design is genuinely decoupled.

9. Worked Application: A Modular Temperature-Monitoring System

Background

This illustrative case concerns a small industrial enclosure containing electronics that must remain within a safe temperature range. The initial prototype reads one sensor and prints a warning. The engineering team expects future versions to support several sensor vendors, simulation during development, data logging, and different alarm policies.

Problem

A procedural prototype contains sensor-driver calls, unit conversion, threshold comparisons, logging, and user-interface output in one function. Replacing the sensor requires editing the monitoring logic. Testing the alarm behavior requires physical hardware.

Analysis

The stable responsibility is not the sensor’s communication protocol. It is the provision of a temperature measurement. Therefore, the application should depend on a temperature-reading interface. Sensor drivers, simulators, and calibration wrappers can implement that interface.

The alarm policy is another responsibility. It should not be embedded in a hardware driver because the same sensor may be used with different operating limits.

Approach

class TemperatureSource:
    def temperature_celsius(self):
        raise NotImplementedError

class SimulatedTemperature:
    def __init__(self, values):
        self._values = iter(values)

    def temperature_celsius(self):
        return next(self._values)

class AlarmPolicy:
    def __init__(self, maximum_celsius):
        self._maximum = maximum_celsius

    def is_alarm(self, temperature_celsius):
        return temperature_celsius > self._maximum

class Monitor:
    def __init__(self, source, policy, logger):
        self._source = source
        self._policy = policy
        self._logger = logger

    def sample(self):
        temperature = self._source.temperature_celsius()
        alarm = self._policy.is_alarm(temperature)
        self._logger.record(temperature, alarm)
        return alarm

The Monitor uses composition. It does not inherit from a sensor, and it does not know how measurements are produced or logged. A simulated source can be used in automated tests:

source = SimulatedTemperature([22.0, 25.5, 31.0])
policy = AlarmPolicy(maximum_celsius=30.0)
monitor = Monitor(source, policy, test_logger)

The example assumes that the source returns values in degrees Celsius and that a strict alarm is required only when the value is greater than the maximum. A real system would also specify sensor failure, stale data, sampling period, hysteresis, unit conversion, and safe behavior when communication is lost.

Outcome

Adding a new sensor requires an adapter that implements temperature_celsius(), rather than changes to Monitor. Alarm behavior can be tested without hardware. The design also makes it possible to substitute a different policy, such as separate warning and shutdown thresholds.

Learning Points

  • Stable domain responsibilities should define interfaces.
  • Composition allows implementations to be replaced.
  • Tests become simpler when hardware is separated from decision logic.
  • Interface contracts must specify units and failure behavior.
  • A small design is preferable to a framework-heavy design that provides no practical benefit.

10. Quality Attributes and Trade-offs

Object-oriented design affects several software quality attributes.

Cohesion measures how closely the responsibilities of a module belong together. High cohesion generally improves comprehension and testing.

Coupling describes dependence between modules. Lower coupling generally makes change safer, although completely independent modules are unrealistic in useful systems.

Maintainability is improved when responsibilities are clear, contracts are stable, and dependencies are replaceable.

Testability improves when external resources are represented by interfaces and injected into the component under test.

Extensibility improves when new behavior can be added through new implementations rather than repeated edits to central conditional logic.

These benefits have costs. More classes can make navigation difficult. Indirection can obscure control flow. Interfaces may become outdated. Dynamic dispatch can complicate debugging. Generic abstractions can be harder for beginners to understand. The appropriate design balances anticipated change against present complexity.

A useful engineering principle is to design for known and credible variation, not for every imaginable future requirement.

11. Common Design Errors

Anemic Domain Objects

An object that contains only public data while all behavior is placed in unrelated functions may fail to protect invariants. This pattern is not always wrong—for example, simple data-transfer records may be appropriate—but it should be deliberate.

Deep Inheritance Trees

Many inheritance levels make behavior difficult to trace. Prefer shallow hierarchies and composition when variation concerns implementation rather than genuine classification.

God Classes

A “god class” controls too many unrelated activities. Split it according to responsibilities and collaborations.

Leaky Abstractions

An abstraction leaks when clients must understand implementation details. For example, a supposedly general sensor interface that exposes a particular serial-port error code is tied to one implementation.

Overuse of Getters and Setters

Unrestricted accessors can allow invalid combinations of state. Prefer operations that express domain actions and enforce rules.

Premature Generalization

A framework designed for hypothetical future devices may be more complicated than the current application. Begin with a clear concrete case, then generalize when repeated variation is demonstrated.

12. Safety, Ethics, and Governance

Engineering software may influence physical processes, public infrastructure, health, privacy, or environmental outcomes. Object-oriented structure does not by itself make a system safe.

Safety-critical components should define failure modes, safe defaults, bounds checking, diagnostic behavior, and clear responsibility for decisions. A method named stop() must specify whether it commands an immediate stop, a controlled stop, or merely requests one. Ambiguous interfaces are a safety risk.

Logging and monitoring systems may collect personal or operationally sensitive data. Designers should minimize unnecessary collection, control access, protect records, and define retention policies. Interfaces should not hide security-critical behavior behind vague names.

AI-generated code can assist with scaffolding, test generation, and documentation, but engineers remain responsible for correctness, licensing review, security, and validation against hardware requirements. Generated code should be inspected rather than trusted because it compiles.

13. Contemporary Practice and Best Practices

Modern engineering teams commonly combine object-oriented design with modular packages, automated tests, continuous integration, static analysis, code review, and documentation generated from interfaces. Dependency injection, adapters, repositories, and simulation doubles are especially useful when software interacts with hardware or external services.

Recommended practices include:

  1. Give each class a clear, limited purpose.
  2. Keep public interfaces small and behaviorally precise.
  3. Validate inputs and preserve invariants.
  4. Prefer composition for replaceable components.
  5. Use inheritance only for genuine substitutable subtypes.
  6. Document units, timing, errors, side effects, and resource ownership.
  7. Inject external dependencies to support testing.
  8. Use generics when they remove real duplication without hiding meaning.
  9. Measure complexity through review and testing rather than relying on patterns by name.
  10. Refactor when the design reveals repeated conditionals, duplicated rules, or unstable dependencies.

Chapter Summary

Object-oriented design organizes software around objects that combine state and behavior. Classes define object structure, while constructors establish valid initial conditions. Encapsulation protects state and maintains invariants. Composition builds larger systems from collaborating parts and is often the safest default for engineering applications. Inheritance expresses specialization but should be used only when subtypes remain substitutable for their base abstractions. Polymorphism allows common client code to work with multiple implementations. Interfaces define contracts and enable dependency injection, simulation, and independent replacement of components. Generic abstractions support reusable algorithms and data structures when type-independent behavior is genuinely present.

Maintainable design depends on cohesion, controlled coupling, explicit contracts, testability, and proportionate complexity. In engineering systems, interfaces must document units, timing, errors, side effects, and safety behavior. The strongest design is not the one with the most classes or patterns; it is the simplest design that protects important rules and accommodates credible change.

Key Terms

Class: A definition describing the structure and behavior of objects.

Object: A runtime entity with identity, state, and behavior.

Attribute: Data associated with an object.

Method: An operation defined by a class.

Constructor: An operation that establishes an object’s initial state.

Encapsulation: Combining data and behavior while controlling access to internal state.

Invariant: A condition that must remain true for a valid object.

Composition: Building an object from other objects or delegating work to them.

Aggregation: A relationship in which an object uses components that may exist independently.

Inheritance: A relationship in which a subtype specializes a parent type.

Polymorphism: The ability to use different implementations through a common operation or abstraction.

Interface: A contract specifying operations and expected behavior.

Substitutability: The ability to replace one implementation with another without violating client expectations.

Coupling: The degree of dependence between software components.

Cohesion: The degree to which a component’s responsibilities belong together.

Dependency injection: Supplying dependencies from outside a component rather than constructing them internally.

Generic abstraction: A reusable abstraction parameterized over one or more types.

Adapter: A component that translates one interface into another expected interface.

Invariant: A rule that must hold whenever an object is in an observable state.

Review Questions

Short Answer Questions

  1. Distinguish a class from an object.
  2. Why are invariants important in engineering software?
  3. State one reason composition is often preferred to inheritance.
  4. What information should a reliable interface contract document?
  5. Define polymorphism and give one engineering example.

Long Answer Questions

  1. Explain encapsulation using a battery or sensor example. Identify the state, public operations, and protected invariants.
  2. Compare composition and inheritance, including their relationships, benefits, and risks.
  3. Explain how interfaces and dependency injection improve the testability of hardware-related software.
  4. Discuss how cohesion, coupling, and complexity influence maintainability.
  5. Describe a design process for converting a procedural engineering prototype into a modular object-oriented system.

Analytical / Application Questions

  1. Design interfaces and collaborating classes for a data-acquisition system that must support real sensors, simulated sensors, calibration, logging, and alarm handling. Justify each relationship.
  2. A proposed class controls sensor communication, unit conversion, database storage, email alerts, and dashboard display. Analyze its design weaknesses and propose a refactoring.
  3. An inherited sensor subtype sometimes returns a measurement in Fahrenheit when clients expect Celsius. Analyze the contract violation and propose at least two corrective designs.

Multiple Choice Questions with Answers

Question 1

Which statement best describes encapsulation?

A. Combining data with operations while controlling access to internal state

B. Creating as many subclasses as possible

C. Storing all program data in global variables

D. Replacing every loop with a method

Correct Answer: A. Combining data with operations while controlling access to internal state

Question 2

Which relationship is usually represented by composition?

A. A controller has a sensor

B. A thermocouple is a sensor

C. A square is a rectangle

D. A class is a method

Correct Answer: A. A controller has a sensor

Question 3

What is the main purpose of an interface?

A. To specify a contract independent of implementation details

B. To prevent all objects from having state

C. To guarantee that no runtime errors can occur

D. To replace testing

Correct Answer: A. To specify a contract independent of implementation details

Question 4

Which design most directly supports substituting a simulator for hardware during testing?

A. Depend on a measurement interface and inject the simulator

B. Call hardware driver functions from every test

C. Make the simulator inherit from the test runner

D. Store hardware addresses as global constants

Correct Answer: A. Depend on a measurement interface and inject the simulator

Question 5

Inheritance is most appropriate when:

A. A subtype genuinely satisfies the behavioral contract of its parent type

B. Only a few lines of implementation need copying

C. A class must access every private variable of another class

D. A program has no reusable behavior

Correct Answer: A. A subtype genuinely satisfies the behavioral contract of its parent type

Question 6

What does high cohesion generally indicate?

A. The responsibilities of a component are closely related

B. A component depends on every other component

C. A class has a very deep inheritance tree

D. All attributes are publicly writable

Correct Answer: A. The responsibilities of a component are closely related

Question 7

Which is an example of a generic abstraction?

A. A repository that can store values of a parameterized type

B. A class hard-coded for one particular motor

C. A method that reads one fixed hardware address

D. A constant representing a voltage limit

Correct Answer: A. A repository that can store values of a parameterized type

Question 8

Why can a large interface be problematic?

A. Clients may be forced to depend on operations they do not use

B. It always prevents polymorphism

C. It cannot contain method names

D. It automatically makes code procedural

Correct Answer: A. Clients may be forced to depend on operations they do not use

Question 9

In the illustrative temperature-monitoring system, which component evaluates whether a reading exceeds a limit?

A. AlarmPolicy

B. SimulatedTemperature

C. Repository

D. Constructor

Correct Answer: A. AlarmPolicy

Question 10

Which practice is most appropriate for safety-relevant software interfaces?

A. Document units, timing, failure behavior, side effects, and safe responses

B. Hide all errors so clients never see them

C. Use vague method names to preserve flexibility

D. Allow unrestricted modification of internal state

Correct Answer: A. Document units, timing, failure behavior, side effects, and safe responses

Programming Fundamentals for Engineers
Chapter 10 of 15