Object-Oriented Programming with C++

Chapter 9
Object-Oriented Analysis, UML, SOLID, and Design Patterns

Bachelor Program in Mechatronics Engineering
Object-Oriented Programming with C++   •   Bachelor

Object-Oriented Analysis, UML, SOLID, and Design Patterns

Introduction and Context

Software in mechatronic systems rarely consists of isolated calculations. A mobile robot, automated production cell, or motor-control application combines sensors, actuators, communication protocols, safety rules, user commands, and fault-handling behavior. Object-oriented programming provides mechanisms for organizing this complexity, but syntax alone does not produce a good design. The engineer must first understand the problem, identify responsibilities, model interactions, and select abstractions that can evolve without unnecessary complexity.

This chapter presents a disciplined path from requirements to implementation. It introduces use-case analysis, class and sequence diagrams, architectural boundaries, the SOLID principles, and seven practical design patterns: Factory, Strategy, Observer, State, Command, Adapter, and dependency injection. The emphasis is not on applying patterns mechanically. A pattern is useful only when it solves a recurring design problem at an acceptable cost.

Learning Objectives

After studying this chapter, you should be able to:

  1. Translate stakeholder requirements into actors, use cases, scenarios, and acceptance criteria.
  2. Construct and interpret UML use-case, class, and sequence diagrams.
  3. Assign responsibilities to classes using cohesion and coupling as design criteria.
  4. Explain and apply the SOLID principles in C++.
  5. Select and implement common design patterns appropriately.
  6. Use dependency injection to improve testing and replaceable components.
  7. Evaluate designs for overengineering, safety, maintainability, and technical risk.

1. From Requirements to Objects

1.1 Requirements as the starting point

A requirement states a desired capability, constraint, or quality. Functional requirements describe what a system does; non-functional requirements describe how well or under what constraints it does it.

For example, an automated conveyor controller may have these requirements:

  • An operator can start and stop the conveyor.
  • The controller stops the motor when an emergency input is active.
  • A jam is detected when an item remains at a sensor for more than 2 s.
  • The system shall record every emergency stop event.
  • The control loop shall respond within 50 ms.

The first three are mainly functional. Response time, safety behavior, and logging reliability are constraints that strongly influence architecture. A class diagram created before clarifying these requirements often reflects the programmer’s assumptions rather than the real system.

A useful requirement should be testable. “The system is user-friendly” is vague; “an operator can acknowledge a fault using one control-panel action” is more precise. Acceptance criteria translate requirements into observable results.

1.2 Actors, use cases, and scenarios

An actor is an external role that interacts with the system. It may be a person, another software system, or a physical device. A use case describes a goal achieved through interaction with the system; it is not merely a class or a button.

For the conveyor, actors may include Operator, Safety Relay, and Maintenance Logger. Use cases include Start Conveyor, Stop Conveyor, Handle Emergency Stop, and Record Fault.

A use-case description commonly includes:

ElementPurpose
GoalResult the actor wants
PreconditionsConditions that must already hold
Main success scenarioNormal interaction sequence
AlternativesValid variations
ExceptionsFailure or abnormal behavior
PostconditionsGuaranteed result after completion
Acceptance criteriaObservable tests of success

A scenario is one path through a use case. For Start Conveyor, a main scenario could be: the operator requests start; the controller verifies that the emergency input is inactive; the controller commands the motor; and the system reports running status. An alternative scenario occurs when the safety relay is open: the request is rejected and a safety fault is reported.

Use cases should remain at an appropriate level. “Operate conveyor safely” is too broad for detailed design, whereas “invoke digitalWrite()” is an implementation detail. A useful use case expresses a stakeholder goal while remaining independent of a particular programming language.

2. UML as a Communication Language

The Unified Modeling Language (UML) is a standardized visual language for describing software structure and behavior. UML diagrams are models, not executable specifications. Their value lies in making assumptions visible to engineers, technicians, testers, and customers.

2.1 Use-case diagrams

A use-case diagram shows actors and their relationships to system goals. A system boundary separates the system under design from external participants. The include relationship identifies behavior that is always reused by another use case; extend represents optional or conditional behavior. These relationships should be used sparingly because a written scenario is often clearer than a dense network of ovals.

For example, “Start Conveyor” may include “Check Safety Conditions.” “Display Jam Warning” may extend “Monitor Conveyor” when a jam occurs. The diagram does not specify timing, algorithms, or class responsibilities; those belong in other models.

2.2 Class diagrams

A class diagram describes types, attributes, operations, and relationships. Common relationship meanings are:

  • Association: objects know or use one another.
  • Aggregation: a weak whole–part relationship; parts may exist independently.
  • Composition: a strong whole–part relationship; the whole controls part lifetime.
  • Generalization: inheritance, representing an is-a relationship.
  • Dependency: temporary use, such as a function parameter.
  • Realization: a class implements an interface.

Multiplicity communicates how many objects participate. For example, a ConveyorController may use exactly one Motor, while a Logger may record many FaultEvent objects. Composition should not be used simply because one object contains another; it implies ownership and lifetime responsibility.

A design should prefer a small, meaningful public interface. Data that must remain consistent should be protected by invariants. An invariant is a condition that remains true for every valid object state. For a motor command, a possible invariant is that a commanded speed lies within the permitted range. The class, rather than every caller, should enforce it.

2.3 Sequence diagrams

A sequence diagram emphasizes time-ordered messages between participants. Consider the normal Start Conveyor scenario:

  1. OperatorPanel sends start() to ConveyorController.
  2. The controller asks SafetyInput whether the circuit is safe.
  3. The controller sends enable() to Motor.
  4. The controller publishes a ConveyorStarted event to interested displays and logs.

A sequence diagram is especially useful when behavior is distributed among several objects. It can reveal excessive delegation, circular calls, or a controller that has accumulated too many responsibilities. Conditions and alternatives should be shown explicitly, such as a branch for an unsafe input.

2.4 From diagrams to C++

UML should guide code without dictating every line. A simplified class model might become:

class ISafetyInput {
public:
    virtual ~ISafetyInput() = default;
    virtual bool isSafe() const = 0;
};

class IMotor {
public:
    virtual ~IMotor() = default;
    virtual void enable() = 0;
    virtual void disable() = 0;
};

class ConveyorController {
public:
    ConveyorController(ISafetyInput& safety, IMotor& motor)
        : safety_(safety), motor_(motor) {}

    bool start() {
        if (!safety_.isSafe()) return false;
        motor_.enable();
        return true;
    }

    void stop() { motor_.disable(); }

private:
    ISafetyInput& safety_;
    IMotor& motor_;
};

The interfaces represent abstractions. The controller does not depend on a particular PLC input card or motor driver. References express non-owning dependencies; the caller must ensure that the referenced objects outlive the controller.

3. Architecture and Responsibility Allocation

Architecture describes the major parts of a system and the rules governing their interaction. A practical mechatronic application may use four conceptual layers:

  1. Hardware or infrastructure: GPIO, fieldbus, files, network sockets, and vendor SDKs.
  2. Application services: orchestration of use cases.
  3. Domain model: states, policies, rules, and meaningful domain objects.
  4. Interface: operator panel, command line, dashboard, or test harness.

The names vary across architectural styles, including layered, hexagonal, and ports-and-adapters architectures. The shared idea is dependency direction: volatile technical details should depend on stable domain abstractions, not the reverse.

Two key design properties are cohesion and coupling. Cohesion measures how closely related the responsibilities within a module are. Coupling measures how strongly modules depend on each other. High cohesion and low coupling generally improve changeability, but eliminating all coupling is impossible and undesirable. A system with no dependencies cannot perform useful work.

A responsibility should normally be assigned to the class having the necessary information, unless doing so creates excessive coupling or violates a safety boundary. A class named SystemManager that reads sensors, drives motors, stores files, validates commands, and updates the user interface is often a sign of low cohesion. Splitting it into focused components can help, but creating dozens of one-method classes without a real variation or testing benefit is overengineering.

4. SOLID Principles

SOLID is a set of design heuristics. They are not laws, and applying them mechanically can produce needless indirection.

4.1 Single Responsibility Principle

The Single Responsibility Principle states that a class should have one reason to change. “Responsibility” means a coherent axis of change, not necessarily one method. A Motor class may manage motor commands and enforce speed limits; it should not also format audit reports and manage a graphical display.

A practical test is to identify stakeholders or policies that could request different changes. If a safety engineer and a user-interface designer would modify the same class for unrelated reasons, responsibilities may be mixed.

4.2 Open–Closed Principle

The Open–Closed Principle says that software entities should be open for extension but closed for modification. A strategy interface can allow new control algorithms without changing the code that invokes them. This does not mean that every possible future variation requires an interface. Extension points should be introduced where variation is credible and valuable.

4.3 Liskov Substitution Principle

The Liskov Substitution Principle requires that objects of a subtype can be used wherever the base abstraction is expected without invalidating client assumptions. A derived class must preserve applicable contracts, including preconditions, postconditions, and invariants.

If Motor promises stop() immediately disables output, a subclass that merely requests a delayed stop may violate the abstraction. Public inheritance should represent a true behavioral subtype, not just shared implementation.

4.4 Interface Segregation Principle

The Interface Segregation Principle advises that clients should not depend on methods they do not use. Instead of one large IIndustrialDevice interface containing reading, writing, calibration, firmware update, and diagnostics, define smaller role-based interfaces when clients require different capabilities.

4.5 Dependency Inversion Principle

The Dependency Inversion Principle has two related ideas: high-level policy should not depend directly on low-level details, and both should depend on abstractions. Abstractions should not be designed merely as mirrors of a vendor library.

Dependency inversion is implemented through polymorphism, templates, callbacks, or other mechanisms. In C++, runtime interfaces with virtual functions are useful where implementations must be selected dynamically; templates may be preferable where the implementation is fixed at compile time and zero-overhead abstraction is important.

5. Design Patterns

A design pattern is a named, reusable design idea for a recurring problem. It is not a code fragment to insert automatically. Each pattern introduces structure and therefore a cost in concepts, classes, and maintenance.

5.1 Factory

A Factory centralizes object creation when the concrete type depends on configuration, hardware, or a mode selected at runtime. The client receives an abstraction rather than constructing a concrete class.

std::unique_ptr<IMotor> makeMotor(const Config& c) {
    if (c.driver == "simulated")
        return std::make_unique<SimulatedMotor>();
    if (c.driver == "can")
        return std::make_unique<CanMotor>(c.canId);
    throw std::invalid_argument("Unknown motor driver");
}

The factory is valuable when construction involves validation, ownership, or several related objects. It is unnecessary when a direct constructor is clear and no variation exists. std::unique_ptr communicates exclusive ownership and ensures destruction through RAII.

5.2 Strategy

Strategy encapsulates interchangeable algorithms behind a common interface. A robot may use different path-planning or speed-control policies.

class ISpeedPolicy {
public:
    virtual ~ISpeedPolicy() = default;
    virtual double targetSpeed(double error) const = 0;
};

class ProportionalPolicy : public ISpeedPolicy {
public:
    explicit ProportionalPolicy(double gain) : gain_(gain) {}
    double targetSpeed(double error) const override {
        return gain_ * error;
    }
private:
    double gain_;
};

The controller can receive an ISpeedPolicy& and remain independent of the chosen algorithm. The policy must still respect physical limits; a raw output should normally be clamped by a clearly defined safety or actuator boundary. Algorithms involving feedback also require attention to units, sampling, saturation, and stability; Strategy does not solve control-engineering problems by itself.

5.3 Observer

Observer establishes a one-to-many notification relationship. A subject publishes an event, and observers react without the subject knowing their concrete types. It is useful for status displays, logging, and telemetry.

Potential hazards include notification order, re-entrant callbacks, dangling observers, and excessive event coupling. In modern C++, observer lifetime should be explicit. A subscription token or managed connection is safer than storing raw pointers indefinitely. For safety-critical shutdown, direct and deterministic control is usually preferable to an informal event chain.

5.4 State

State represents behavior that changes according to the object’s current mode. A conveyor may be Stopped, Running, Paused, or Faulted. Without State, conditional logic can spread through every operation:

if (mode == Mode::Running) { ... }
else if (mode == Mode::Faulted) { ... }

A small enum and a transition table may be clearer than polymorphic state objects. The State pattern becomes attractive when each state has substantial, distinct behavior. A transition model should define permitted events, resulting states, actions, and forbidden transitions. Safety transitions, such as emergency stop, should have explicit priority and should not depend on uncertain observer timing.

5.5 Command

Command encapsulates a request as an object. It supports queues, logging, replay, authorization, and undo where undo is meaningful.

class ICommand {
public:
    virtual ~ICommand() = default;
    virtual void execute() = 0;
};

class StopConveyor : public ICommand {
public:
    explicit StopConveyor(IMotor& motor) : motor_(motor) {}
    void execute() override { motor_.disable(); }
private:
    IMotor& motor_;
};

A command queue can decouple an operator interface from execution. However, queued commands create timing and authorization questions. A stop command should not wait behind a long-running non-safety command unless the architecture guarantees the required response.

5.6 Adapter

Adapter converts one interface into another. It is particularly valuable at a hardware boundary, where a vendor API should not spread through the domain model.

class VendorDrive {
public:
    void setEnable(bool value);
};

class VendorMotorAdapter : public IMotor {
public:
    explicit VendorMotorAdapter(VendorDrive& drive) : drive_(drive) {}
    void enable() override { drive_.setEnable(true); }
    void disable() override { drive_.setEnable(false); }
private:
    VendorDrive& drive_;
};

The adapter is also an appropriate location for unit conversion, error translation, and vendor-specific sequencing. Such transformations must be documented and tested; silently converting millimeters to meters or active-low to active-high signals is a serious defect risk.

5.7 Dependency injection

Dependency injection means that an object receives its collaborators rather than constructing them internally. Constructor injection is usually the clearest form:

class FaultService {
public:
    FaultService(ILogger& log, IClock& clock)
        : log_(log), clock_(clock) {}
private:
    ILogger& log_;
    IClock& clock_;
};

Injection supports testing with fakes or mocks, configuration of hardware implementations, and explicit dependency visibility. It does not require a service locator or a large dependency-injection framework. In small embedded programs, passing one or two interfaces directly is often better than introducing a container.

6. Worked Design Example: A Robotic Conveyor Cell

The following is an illustrative case, not a report of a particular commercial installation.

Background and problem

A robotic conveyor cell transfers parts to a robot pickup position. A photoelectric sensor detects parts, a motor drive controls motion, and an operator panel provides start, stop, and reset commands. The initial prototype used one large class containing GPIO reads, vendor-drive calls, state flags, and display updates. Adding simulation for testing became difficult because the class directly accessed hardware.

The required behavior is:

  • The conveyor starts only when the safety circuit is healthy and the system is reset.
  • An emergency input disables motion immediately.
  • A detected jam places the system in a fault state.
  • Operators can reset a fault only after the cause is cleared.
  • Logs contain timestamped transitions.

Analysis

Actors are Operator, Safety Circuit, and Maintenance Logger. Principal use cases are Start, Stop, Reset Fault, Monitor Part Flow, and Handle Emergency Stop. The domain states are Stopped, Running, and Faulted; an emergency input is a high-priority condition that forces a safe stopped output.

The architecture separates hardware adapters from application policy. ISafetyInput, IMotor, IPartSensor, ILogger, and IClock are ports. Concrete GPIO, CAN, and file implementations are adapters. ConveyorController owns the transition policy but does not know vendor APIs.

A simplified sequence for Start is:

  1. The panel sends a start command.
  2. The controller checks that the state is Stopped and the safety input is healthy.
  3. The controller commands the motor adapter to enable.
  4. The controller changes state to Running.
  5. The logger records the transition.

If the safety check fails, no motor command is issued and the controller remains stopped. If a jam timer expires, the controller disables the motor, changes to Faulted, and logs the fault. The timer policy can be injected or represented by a clock abstraction so tests do not depend on real time.

Design evaluation

The design applies Adapter at the hardware boundary, dependency injection for ports, State for nontrivial operating modes, Command for panel requests, and Observer for non-safety status notifications. Factory can select real or simulated adapters from configuration. Strategy would be justified only if multiple credible jam-detection or motion policies must be exchanged.

Testing can use a fake motor that records calls, a fake safety input, and a controllable clock. Tests should verify both positive and negative behavior: safe start enables the motor; unsafe start does not; emergency input disables output; reset is rejected while the fault cause remains; and a jam produces exactly one fault transition. These tests express requirements rather than implementation details.

Outcome and learning points

The resulting design isolates vendor changes, permits hardware-free unit tests, and makes safety transitions visible. It does not create an abstraction for every sensor property or introduce a framework merely because patterns exist. The central lesson is that patterns follow observed design pressure: variation, difficult testing, unstable dependencies, or complex state behavior.

7. Challenges, Limitations, and Professional Practice

Avoiding overengineering

Abstraction has a cost. An interface with one implementation may be justified by hardware replacement or testing, but not automatically. Before introducing a pattern, identify the concrete problem, the expected change, and the simpler alternative. Prefer a direct function when a strategy object would add no meaningful variation. Prefer an enum and a small transition function when polymorphic state classes would obscure behavior.

Ownership and lifetime

C++ design must make ownership explicit. Use automatic objects where possible, std::unique_ptr for exclusive dynamic ownership, and std::shared_ptr only when shared lifetime is genuinely required. Non-owning references and pointers require documented lifetime guarantees. Observer lists and asynchronous callbacks deserve particular scrutiny because they can invoke destroyed objects.

Concurrency and real-time behavior

Patterns do not guarantee thread safety or real-time performance. Mutexes can block; dynamic allocation can introduce nondeterministic timing; event queues can overflow; and callbacks can execute in unexpected contexts. A control loop with a strict deadline should use a design appropriate to its scheduling and memory constraints. Safety functions should be simple, deterministic, independently testable, and subject to the relevant engineering standards.

Ethical and governance considerations

Software controlling machinery can affect worker safety and property. Engineers should trace safety requirements to tests, record assumptions, control configuration changes, and distinguish diagnostic convenience from safety authority. Logging must protect integrity and privacy where operators can be identified. AI-generated code may assist with drafts, but generated abstractions, memory ownership, error handling, and hardware assumptions require human review, compilation, testing, and—where applicable—formal or regulated verification.

Contemporary development practices

Modern C++ encourages RAII, strong types, enum class, override, standard containers, smart pointers, static analysis, sanitizers, automated tests, and continuous integration. C++20 concepts and modules can improve compile-time interfaces and build organization, although adoption depends on toolchains. Architecture decision records help teams document why a pattern was introduced and what alternatives were rejected.

Chapter Summary

Object-oriented design begins with stakeholder goals and testable requirements, not with classes. Use cases describe externally visible goals; class diagrams describe structure and responsibility; sequence diagrams reveal time-ordered collaboration. Good designs seek high cohesion, controlled coupling, explicit ownership, and clear dependency direction.

SOLID principles provide useful heuristics: isolate reasons to change, support credible extension, preserve subtype contracts, keep interfaces focused, and depend on abstractions rather than volatile details. Factory, Strategy, Observer, State, Command, Adapter, and dependency injection address different problems. Their use is justified by actual variation, testing needs, integration boundaries, or behavioral complexity—not by fashion.

In mechatronic systems, architecture must also account for timing, safety, hardware failure, lifetime, and traceability. Patterns can improve maintainability, but they cannot replace requirements analysis, control engineering, verification, or professional judgment.

Key Terms

Use case: A description of a stakeholder goal achieved through system interaction.

Actor: An external role interacting with a system.

Scenario: One particular path through a use case.

Acceptance criterion: An observable condition used to determine whether a requirement is satisfied.

UML: A standardized language for modeling software structure and behavior.

Association: A relationship in which objects are connected or communicate.

Composition: A strong whole–part relationship involving ownership and lifetime.

Cohesion: The degree to which responsibilities in a module belong together.

Coupling: The degree of dependency between modules.

Invariant: A condition that remains true for every valid object state.

SOLID: A group of object-oriented design principles.

Abstraction: A stable representation that hides unnecessary implementation detail.

Factory: A mechanism that centralizes and controls object creation.

Strategy: An interchangeable encapsulated algorithm.

Observer: A notification relationship between a subject and multiple subscribers.

State: A representation of behavior dependent on an object’s current mode.

Command: An object representing a request or operation.

Adapter: An object that converts one interface into another.

Dependency injection: Supplying an object’s collaborators from outside rather than constructing them internally.

Overengineering: Adding complexity beyond the problem’s justified requirements.

Review Questions

Short Answer Questions

  1. What is the difference between a functional requirement and a non-functional requirement?
  2. Explain the purpose of a sequence diagram.
  3. What does the Single Responsibility Principle mean by a reason to change?
  4. When is an Adapter useful in a mechatronic system?
  5. Why should safety behavior not depend solely on an informal Observer notification chain?

Long Answer Questions

  1. Explain how requirements can be transformed into use cases and then into classes and interactions. Use a robotic cell as an example.
  2. Discuss all five SOLID principles and illustrate each with a C++ design decision.
  3. Compare Factory, Strategy, and dependency injection. Explain how they may be used together without duplicating responsibilities.
  4. Describe the benefits and risks of applying the State and Command patterns in an industrial controller.
  5. Propose a layered or ports-and-adapters architecture for a sensor–motor system and justify its dependency direction.

Analytical / Application Questions

  1. A controller directly constructs a vendor motor, writes log files, updates a display, and contains six operating-mode conditionals. Identify at least four design problems and propose a staged refactoring plan.
  2. A team proposes an interface and design pattern for every class, although the system has one fixed hardware configuration and no automated tests. Evaluate this proposal using cohesion, coupling, and overengineering criteria.
  3. Design the main use cases, states, interfaces, and sequence of events for a conveyor that must stop on emergency input and report jams after a specified observation interval. Explain which behavior should be deterministic and which may use Observer notifications.

Multiple Choice Questions with Answers

Question 1

Which artifact primarily describes a stakeholder goal and its interaction with a system?

A. Use case

B. Class diagram

C. Header file

D. Sequence buffer

Correct Answer: A. Use case

Question 2

Which SOLID principle states that clients should not depend on methods they do not use?

A. Single Responsibility Principle

B. Interface Segregation Principle

C. Liskov Substitution Principle

D. Open–Closed Principle

Correct Answer: B. Interface Segregation Principle

Question 3

Which pattern encapsulates interchangeable algorithms?

A. Adapter

B. Observer

C. Strategy

D. Factory

Correct Answer: C. Strategy

Question 4

What is the principal purpose of an Adapter?

A. To represent an object’s operating mode

B. To convert one interface into another

C. To notify multiple subscribers

D. To create a command queue

Correct Answer: B. To convert one interface into another

Question 5

Which C++ type normally expresses exclusive dynamic ownership?

A. std::unique_ptr

B. std::weak_ptr

C. std::reference_wrapper

D. std::span

Correct Answer: A. std::unique_ptr

Question 6

A class receives an ILogger reference through its constructor. Which technique is illustrated?

A. Inheritance hiding

B. Dependency injection

C. Object slicing

D. Operator overloading

Correct Answer: B. Dependency injection

Question 7

Which UML relationship most strongly implies that the whole controls the lifetime of its parts?

A. Dependency

B. Generalization

C. Composition

D. Realization

Correct Answer: C. Composition

Question 8

Why is a Factory especially useful when selecting a simulated or real driver from configuration?

A. It eliminates all runtime errors

B. It centralizes variable object creation

C. It guarantees thread safety

D. It replaces requirements analysis

Correct Answer: B. It centralizes variable object creation

Question 9

Which concern is a particular risk in Observer implementations?

A. Dangling subscribers and unclear notification lifetime

B. The inability to define any interface

C. Guaranteed excessive CPU utilization

D. Automatic violation of inheritance rules

Correct Answer: A. Dangling subscribers and unclear notification lifetime

Question 10

What is the best general reason to avoid applying a design pattern automatically?

A. Patterns cannot be implemented in C++

B. Patterns are limited to graphical software

C. They introduce complexity and should address a real design pressure

D. Patterns prevent unit testing

Correct Answer: C. They introduce complexity and should address a real design pressure

Object-Oriented Programming with C++
Chapter 9 of 14