5. Object Modelling and Software DesignIntroduction and ContextObject-oriented programming is most effective when code is preceded by a disciplined understanding of the problem domain. A software system is not merely a collection of classes and methods; it is a model of entities, responsibilities, collaborations, constraints, and changes that matter to stakeholders. Object modelling provides the bridge between requirements and implementation. It helps designers decide what objects exist, what each object knows, what each object does, and how objects cooperate to achieve system behaviour. In C++, these decisions are eventually represented through classes, objects, member functions, access control, inheritance, composition, templates, exceptions, and interfaces. However, C++ syntax cannot compensate for a poor model. If responsibilities are assigned arbitrarily, relationships are misunderstood, or changing requirements are ignored, the resulting program may compile while remaining difficult to test, extend, and maintain. This chapter develops a systematic approach to object modelling and software design. It focuses on domain concepts, association, aggregation, composition, dependency, responsibility assignment, and UML class, sequence, and state diagrams. The emphasis is not on drawing diagrams for their own sake, but on using models as reasoning tools before and during implementation. Learning ObjectivesAfter studying this chapter, the learner should be able to: 1. Translate textual requirements into candidate objects, classes, attributes, operations, and relationships. 2. Distinguish association, aggregation, composition, generalization, and dependency. 3. Apply responsibility-driven design and suitable responsibility-assignment principles. 4. Construct and interpret UML class, sequence, and state diagrams. 5. Relate UML models to C++ design decisions such as ownership, lifetime, interfaces, and coupling. 6. Analyse design alternatives using cohesion, coupling, substitutability, and change impact. 7. Identify common modelling errors and improve an object-oriented design through iterative refinement. From Requirements to a Domain ModelRequirements are normally expressed in the language of users and business stakeholders. A requirement such as “A customer places an order, pays for it, and receives a notification when shipment is dispatched” contains several kinds of information. Nouns suggest domain concepts; verbs suggest behaviour or relationships; conditions suggest constraints and states. A useful first step is to identify candidate concepts. In the example, Customer, Order, Payment, Shipment, and Notification are possible concepts. These candidates are not automatically classes. Some may become attributes, some may be operations, and some may disappear after clarification. For example, “date” may be an attribute of Order, whereas “payment” may deserve a separate class if it has identity, status, or independent rules. A domain model should represent concepts meaningful to the problem, not implementation details. A database table, user-interface screen, or utility function may eventually be important in the architecture, but it should not be introduced as a domain object unless the domain itself treats it as significant. Modelling begins with the problem space and later considers the solution space. A practical modelling cycle is iterative: - Extract candidate concepts and relationships from requirements.
- Clarify the meaning, identity, and lifecycle of each candidate.
- Assign responsibilities to objects.
- Represent collaborations in interaction diagrams.
- Validate the model against use cases and constraints.
- Refine the model in response to new understanding.
This cycle prevents a common mistake: trying to design a complete class hierarchy before understanding the system's behaviour. Concepts, Attributes, and OperationsA class represents a set of objects with common structure and behaviour. An object has identity, state, and behaviour. In C++, a class may define data members and member functions, but good modelling does not begin by listing every possible field. It begins by asking what the object means and what responsibility it should own. For an Order, possible attributes include orderNumber, orderDate, and status. Possible operations include addItem(), calculateTotal(), cancel(), and markAsShipped(). Whether these operations belong to Order depends on the rules. If order total depends on line-item prices, Order or OrderLine should own that calculation rather than an unrelated controller. Attributes should be selected carefully. A derived value such as totalAmount may be calculated rather than stored, unless persistence or performance requirements justify storing it. Redundant state increases the possibility of inconsistency. Similarly, an object should protect invariants—conditions that must remain true—through controlled operations rather than exposing all data publicly. Relationships Among ObjectsRelationships express how objects are connected or collaborate. Their semantics matter because they influence ownership, lifetime, coupling, navigation, and changeability. AssociationAn association is a structural relationship in which objects know about, communicate with, or are connected to one another. A Customer places Orders; a Librarian issues Books; a Sensor reports to a MonitoringService. An association does not, by itself, imply ownership. Associations may have multiplicity. A customer may place zero or many orders, while each order may belong to exactly one customer. In UML, this can be represented as Customer 1 to Order 0..*. Names and role labels clarify direction and purpose. An association may be one-way or navigable in both directions, but bidirectional navigation should not be added automatically because it increases coupling and consistency obligations. AggregationAggregation is a whole-part relationship in which the part can exist independently of the whole. A Department aggregates Professors, or a Playlist aggregates Songs. If the Department is removed, a Professor may still exist; if a Playlist is deleted, the Song objects remain available elsewhere. UML represents aggregation with a hollow diamond at the whole. In practice, aggregation is often less precise than ordinary association. Designers should use it only when the whole-part meaning contributes to understanding. Merely storing a reference or pointer does not establish aggregation. CompositionComposition is a strong whole-part relationship. The part's lifecycle is controlled by the whole, and the part normally should not be shared independently. An Order is composed of OrderLines; a Document may be composed of Sections. When the owning object is destroyed, its composed parts are destroyed or become invalid as domain entities. In C++, composition is commonly implemented using value members, containers of values, or smart pointers with exclusive ownership such as std::unique_ptr. For example: ```cpp class Order { std::vector<OrderLine> lines; public: void addLine(const Product& product, int quantity); }; ``` This implementation communicates that OrderLine belongs to Order and has no independent lifetime. Nevertheless, implementation alone does not prove domain composition; the lifecycle rule must come from the domain. DependencyA dependency indicates that one element temporarily uses another. A method parameter, local variable, return type, or call to a service can create a dependency. For example, InvoicePrinter may depend on Invoice, although it does not own the invoice. Dependencies are generally weaker and more temporary than associations. Excessive dependencies create ripple effects: changing one class requires changes in many clients. Dependency inversion, interfaces, and dependency injection can reduce this coupling. In C++, a class may depend on an abstract interface rather than a concrete implementation, allowing testing and substitution. Generalization and RealizationGeneralization expresses an “is-a” relationship between a more general type and a specialized type. SavingsAccount is a kind of Account, and Circle is a kind of Shape. In C++, public inheritance can represent substitutable specialization, but inheritance should not be used merely to reuse implementation. The Liskov Substitution Principle requires that objects of a subtype can be used wherever objects of the base type are expected without violating essential correctness assumptions. If a subtype must reject operations that the base type promises, the hierarchy may be conceptually wrong. Composition and delegation are often safer alternatives to inheritance. Realization describes a class implementing an interface or contract. In C++, this may be represented by an abstract base class with pure virtual functions. A PaymentGateway interface, for instance, can be implemented by CardGateway, BankGateway, or TestGateway. Responsibility-Driven DesignA responsibility is an obligation of an object. It may involve knowing information, maintaining an invariant, performing an operation, or coordinating with another object. Responsibility-driven design asks: which object is the most suitable owner of this responsibility? A useful distinction is between doing responsibilities and knowing responsibilities. An Order may know its line items and calculate its total; a CheckoutService may coordinate payment and shipment; a PaymentGateway may perform authorization. The aim is not to eliminate coordination but to place it at an appropriate level. The GRASP principles provide a practical vocabulary for assignment decisions: - Information Expert: assign a responsibility to the class possessing the information needed to fulfil it.
- Creator: assign creation to a class that aggregates, contains, records, or closely uses the created object.
- Controller: assign system-event handling to a suitable facade or use-case controller rather than a user-interface class.
- Low Coupling: avoid unnecessary knowledge and dependencies between classes.
- High Cohesion: keep responsibilities focused and related.
- Polymorphism: use polymorphic variation when behaviour differs according to type.
- Pure Fabrication: introduce a non-domain service when doing so improves cohesion or reduces coupling.
- Indirection: use an intermediate object to avoid direct coupling.
- Protected Variations: isolate points likely to change behind stable interfaces.
These principles are heuristics, not mechanical rules. For example, assigning every operation to the information expert may produce an object with too many responsibilities. A domain service can be justified when an operation involves several independent entities and does not naturally belong to one of them. Cohesion and CouplingCohesion measures how strongly the responsibilities of a class belong together. A class that validates orders, sends emails, formats reports, and manages database connections has low cohesion. A class focused on order pricing has higher cohesion. Coupling measures the degree of dependency among classes. High coupling makes changes expensive and testing difficult. Low coupling is desirable, but eliminating all coupling is impossible and undesirable: objects must collaborate to provide system behaviour. The design objective is purposeful, stable collaboration rather than isolation. A class should expose a small, meaningful public interface. Encapsulation protects internal representation and permits implementation changes. For example, an Account should provide deposit() and withdraw() while preventing arbitrary external modification of its balance. UML as a Design LanguageThe Unified Modeling Language provides standardized visual notation for communicating structure and behaviour. UML diagrams are models, not executable truth. Their value lies in making assumptions visible and enabling discussion among analysts, developers, testers, and stakeholders. Class DiagramsA class diagram describes static structure. A class is commonly shown with compartments for name, attributes, and operations. Visibility symbols include + for public, - for private, and # for protected. Associations show links, multiplicities, role names, and sometimes navigability. Generalization is shown with a hollow triangular arrow, while dependency is commonly shown with a dashed arrow. For an order-processing system, a simplified model might contain: - Customer, associated with zero or more Orders.
- Order, composed of one or more OrderLines.
- OrderLine, associated with one Product and containing a quantity.
- PaymentGateway, realized by concrete gateway implementations.
- CheckoutService, depending on Order and PaymentGateway.
Multiplicity is important. “1” means exactly one; “0..1” means optional one; “0..” means zero or more; “1..” means one or more. Multiplicity should reflect business rules, not arbitrary programming convenience. A class diagram should also record constraints when they matter. For example, an order may be cancellable only before shipment, and an OrderLine quantity may be greater than zero. Constraints may be written in plain language or using a formal notation such as OCL where appropriate. Sequence DiagramsA sequence diagram shows how objects collaborate over time for one scenario. Participants are represented by lifelines, and messages are arranged from top to bottom. Activation bars show periods during which an object is executing an operation. Combined fragments such as alt, opt, and loop represent alternatives, optional behaviour, and repetition. For the scenario “customer checks out an order,” a sequence may be: 1. CheckoutUI sends checkout(orderId) to CheckoutController. 2. CheckoutController retrieves the Order. 3. CheckoutController asks Order to calculateTotal(). 4. CheckoutController sends authorize(amount) to PaymentGateway. 5. On success, CheckoutController asks Order to markPaid(). 6. CheckoutController requests ShipmentService to arrangeShipment(). 7. The system returns confirmation. An alternative fragment can show payment failure. The sequence diagram helps reveal misplaced responsibilities. If the interface directly calculates totals, updates payment state, and creates shipments, the design likely lacks a suitable controller and domain responsibilities. Sequence diagrams should describe a meaningful use-case scenario rather than every internal method call. Excessive detail makes them fragile and obscures the design question. State DiagramsA state diagram models the lifecycle of an object whose behaviour depends on its current state. An Order might have states New, Paid, Packed, Shipped, Delivered, and Cancelled. Transitions are triggered by events and may have guards and actions. For example: - New --pay()--> Paid
- Paid --pack()--> Packed
- Packed --ship()--> Shipped
- Shipped --deliver()--> Delivered
- New or Paid --cancel()--> Cancelled, subject to business rules
A state model prevents invalid transitions, such as delivering an order that has never been shipped. In C++, state may be represented by an enum and guarded methods, by a State design pattern, or by a richer domain model when each state has substantially different behaviour. The appropriate technique depends on complexity; a design pattern should not be introduced solely because a diagram contains states. From UML to C++ DesignUML does not dictate a single C++ implementation. A composition relationship may become a value member, a container, or an exclusively owned smart pointer. An association may become a reference, pointer, identifier, or observer relationship. The choice depends on lifetime, optionality, ownership, persistence, and performance. Raw owning pointers should generally be avoided in modern C++ because ownership and destruction are unclear. std::unique_ptr expresses exclusive ownership; std::shared_ptr expresses shared ownership but should not be used merely as a convenient default. Shared ownership can create cycles and obscure lifecycle responsibility. References and observer pointers are suitable when an object is used without being owned, provided lifetime is guaranteed by the design. Interfaces reduce concrete dependencies. A CheckoutService depending on PaymentGateway rather than CardGateway can be tested with a fake gateway and can support multiple payment providers. Dependency injection may be performed through constructors: ```cpp class CheckoutService { PaymentGateway& gateway; public: explicit CheckoutService(PaymentGateway& g) : gateway(g) {} }; ``` The code is useful because it reflects a modelling decision: the service requires a payment capability but does not own a particular gateway implementation. An Illustrative Case: Modelling a University Laboratory Booking SystemBackgroundConsider a university laboratory booking system. Students and faculty members request laboratory sessions. Laboratories have capacities and equipment. A booking is approved or rejected according to time conflicts, user eligibility, and laboratory availability. Approved bookings may later be cancelled. This is an illustrative case rather than a description of a particular institution. Situation and ProblemAn initial implementation places all operations in BookingManager. The manager stores users, laboratories, bookings, eligibility rules, conflict checks, notification logic, and database operations. It has become difficult to test and every change affects the same large class. AnalysisCandidate domain concepts are User, Laboratory, Equipment, Booking, TimeSlot, EligibilityPolicy, and NotificationService. A Laboratory contains Equipment items, but equipment may be transferred between laboratories; therefore ordinary association or aggregation may be more appropriate than composition. A Booking has a TimeSlot as a value-like component, and its lifecycle is controlled by the booking system. User and Laboratory exist independently of a Booking. The important invariants include the following: a booking refers to one user and one laboratory; its time slot must be valid; overlapping approved bookings for the same laboratory are forbidden; only eligible users may book particular laboratories; and a cancelled booking cannot be approved again without a new booking request. Approach and DesignThe proposed model assigns responsibilities as follows: - Booking owns its status and validates legal state transitions.
- Laboratory knows its capacity, equipment requirements, and whether a requested slot is suitable.
- BookingRepository retrieves and stores bookings.
- ConflictPolicy determines whether a proposed booking overlaps an existing approved booking.
- EligibilityPolicy determines whether a user may use a laboratory.
- BookingService coordinates the booking use case.
- NotificationService sends confirmation or rejection messages.
A sequence for requesting a booking is: the user interface sends requestBooking() to BookingService; the service retrieves the user and laboratory; it asks EligibilityPolicy to evaluate eligibility; it asks BookingRepository for conflicting bookings; if no conflict exists, it creates a Booking and stores it; finally, NotificationService sends the result. The service is a controller and coordinator, not the owner of every rule. Eligibility and conflict policies are separated because they may change independently. A class diagram would show User and Laboratory associated with Booking, Booking composed with TimeSlot, BookingService dependent on policy interfaces, and concrete policies realizing those interfaces. A state diagram for Booking could include Requested, Approved, Rejected, and Cancelled. Requested may transition to Approved or Rejected. Approved may transition to Cancelled. Rejected and Cancelled are terminal states for that request. OutcomeThe redesigned model improves cohesion and reduces coupling. Policy changes can be tested independently. The user interface does not need to know conflict algorithms, and Booking protects its own state. Different storage technologies can be introduced behind BookingRepository. The model also makes an important business decision explicit: equipment is not necessarily owned for the lifetime of a laboratory. Learning PointsThe case demonstrates that modelling is a reasoning process. Candidate classes must be validated through lifecycle and responsibility questions. Composition should not be inferred merely from containment in a screen or database record. Sequence and state diagrams reveal different aspects of the same design: collaboration over time and legal object lifecycles. Design Risks and Common MistakesOne frequent mistake is converting every noun into a class. Natural-language nouns may refer to attributes, events, roles, or implementation details. Another is creating an anaemic domain model in which classes contain only getters and setters while a service layer performs all meaningful decisions. This may be appropriate in some architectural contexts, but it often weakens encapsulation when domain invariants are displaced from the objects that own them. Overuse of inheritance is another risk. A hierarchy created for code reuse can become rigid when variations do not share a genuine substitutable contract. Prefer composition when behaviour can be assembled or delegated. Similarly, excessive use of shared ownership in C++ often indicates that lifecycle responsibility has not been decided. Large controller classes, bidirectional associations, global objects, primitive obsession, and excessive public data all increase maintenance cost. Diagram-driven development can also fail when diagrams are produced once and never reconciled with code. Models should be lightweight enough to maintain and detailed enough to answer current design questions. A technical design must also consider concurrency, persistence, transactions, error handling, and security. For example, a booking conflict check followed by insertion may require transactional protection if multiple users can book concurrently. UML alone cannot guarantee this property; it must be supported by the architecture and implementation. Professional and Ethical ConsiderationsObject models encode assumptions about people, roles, access, and authority. A User class that combines student, faculty, and administrator privileges without explicit policies may create security defects. Authorization should be enforced at appropriate service boundaries and not rely solely on user-interface controls. Models should avoid unnecessary personal data. Attributes should have a clear purpose, access should be limited, and sensitive information should not be exposed through diagnostic output or diagrams intended for broad circulation. Audit requirements may require recording who performed a state transition and when, but audit data should be protected against unauthorized alteration. Professional design also requires traceability. Important requirements should be linked to classes, operations, tests, and constraints. Reviews should include domain experts, because technically elegant models can still misrepresent organizational rules. Contemporary Practice and Emerging DirectionsModern development commonly combines UML with user stories, domain-driven design, automated tests, architectural decision records, and continuous integration. Rather than attempting to model an entire system exhaustively, teams often maintain a bounded context: a coherent area in which terms and rules have a consistent meaning. Model-driven tools can generate code skeletons or documentation, while static analysis and integrated development environments help detect dependency problems. Automated tests are particularly valuable for state transitions and invariants. Property-based testing can explore many combinations of inputs, while mutation testing can reveal weak tests. AI-assisted tools can suggest classes, sequence diagrams, or refactorings from requirements and code. Such output must be reviewed by developers and domain experts. Generated models may reproduce ambiguity, invent relationships, or overlook security and lifecycle constraints. The responsibility for correctness remains with the engineering team. Best Practices for Object ModellingBegin with use cases and domain language rather than framework classes. Name classes and operations consistently with stakeholder terminology. Record multiplicities and lifecycle assumptions explicitly. Assign responsibilities to preserve high cohesion and low coupling, but accept necessary collaboration. Use interfaces at genuine variation points, not everywhere. Validate class diagrams with sequence diagrams, and validate both with state transitions and business rules. Make ownership explicit in C++ through values, references, and smart pointers whose semantics match the model. Protect invariants inside the objects responsible for them. Keep diagrams purposeful, versioned, and close enough to the implementation to remain useful. Finally, test the model against exceptional cases: cancellation, failure, duplication, concurrent requests, missing data, and unauthorized actions. Chapter SummaryObject modelling translates requirements into a structured understanding of domain concepts, responsibilities, relationships, and behaviour. Candidate nouns and verbs provide starting points, but classes must be validated through identity, lifecycle, invariants, and use cases. Association expresses connection, aggregation expresses independent whole-part structure, composition expresses strong lifecycle ownership, and dependency expresses temporary use. Generalization should represent genuine substitutability rather than simple code reuse. Responsibility-driven design uses principles such as Information Expert, Creator, Controller, Low Coupling, High Cohesion, Polymorphism, and Protected Variations to guide design decisions. UML class diagrams describe structure, sequence diagrams describe collaboration over time, and state diagrams describe legal lifecycle transitions. In C++, these models influence ownership, smart-pointer selection, interfaces, dependency injection, and encapsulation. Effective modelling is iterative, domain-aware, security-conscious, and validated through scenarios, tests, and stakeholder review. Key TermsObject model: A representation of domain entities, relationships, responsibilities, and behaviour. Class: A definition describing common structure and behaviour for a set of objects. Object: An entity with identity, state, and behaviour. Association: A structural connection or collaboration between objects. Aggregation: A weak whole-part relationship in which parts can exist independently. Composition: A strong whole-part relationship in which the whole controls part lifetime. Dependency: A relationship in which one element temporarily uses another. Generalization: An inheritance relationship between a general type and a specialized type. Realization: Implementation of an interface or contractual abstraction. Multiplicity: The permitted number of instances participating in a relationship. Responsibility: An obligation of an object to know information or perform behaviour. Cohesion: The degree to which a class's responsibilities belong together. Coupling: The degree of dependency between software elements. Encapsulation: Protection of internal representation behind a controlled interface. Invariant: A condition that must remain true for an object or system. GRASP: A set of principles for assigning responsibilities in object-oriented design. Lifeline: A participant's presence over time in a UML sequence diagram. State transition: A change from one object state to another caused by an event. Dependency injection: Supplying a required dependency from outside an object. Bounded context: A domain boundary within which terms and rules have consistent meaning. Review QuestionsShort Answer Questions1. Why should every noun extracted from a requirement not automatically become a class? 2. Distinguish aggregation from composition. 3. What information does multiplicity convey in a UML class diagram? 4. State the Information Expert and Controller principles. 5. How does a state diagram complement a class diagram? Long Answer Questions1. Explain the process of translating a use case into candidate classes, responsibilities, and relationships. 2. Discuss association, aggregation, composition, dependency, and generalization with suitable C++ design implications. 3. Explain how cohesion, coupling, encapsulation, and dependency inversion influence maintainable C++ software. 4. Describe the purposes and notations of UML class, sequence, and state diagrams. 5. Analyse the university laboratory booking case and justify its responsibility assignments. Analytical / Application Questions1. A class named OrderManager calculates totals, validates payments, sends emails, and formats invoices. Identify design problems and propose a responsibility-based redesign. 2. Model a hospital appointment system in which patients, doctors, appointments, prescriptions, and notifications interact. Specify at least three relationships, one state model, and one sequence scenario. 3. A C++ system uses std::shared_ptr for every relationship between objects. Analyse the likely lifecycle and coupling risks and recommend alternatives for ownership and non-owning access. 22. Multiple Choice Questions with AnswersQuestion 1Which relationship best represents an Order owning OrderLine objects whose lifetimes are controlled by the Order? A. Association B. Aggregation C. Composition D. Dependency Correct Answer: C. Composition Question 2A class temporarily receives a PaymentGateway object as a method parameter and does not own it. Which relationship is most appropriate? A. Dependency B. Composition C. Generalization D. Aggregation Correct Answer: A. Dependency Question 3Which GRASP principle assigns a responsibility to the class that has the information needed to fulfil it? A. Controller B. Information Expert C. Protected Variations D. Pure Fabrication Correct Answer: B. Information Expert Question 4What is the primary purpose of a UML sequence diagram? A. To show database tables and keys B. To show object collaboration over time C. To show inheritance only D. To specify compiler memory layout Correct Answer: B. To show object collaboration over time Question 5A Booking object must prevent a transition from Cancelled back to Approved. Which UML model most directly represents this rule? A. Package diagram B. Deployment diagram C. State diagram D. Component diagram Correct Answer: C. State diagram Question 6Which design choice most clearly expresses exclusive ownership in modern C++? A. A raw pointer with no documented owner B. std::unique_ptr C. A global variable D. A public integer identifier Correct Answer: B. std::unique_ptr Question 7A Customer may place many Orders, while each Order belongs to exactly one Customer. Which multiplicity is appropriate at the Customer-to-Order relationship? A. 0..* B. 1..1 only C. 0..1 D. 1..* on every Customer necessarily Correct Answer: A. 0..* Question 8Why might a CheckoutService depend on a PaymentGateway interface rather than CardGateway directly? A. To increase concrete coupling B. To eliminate all collaboration C. To support substitution and testing D. To make payment state public Correct Answer: C. To support substitution and testing Question 9Which situation is the strongest indication that inheritance may be inappropriate? A. The subtype satisfies the base type's contract B. The subtype is genuinely substitutable for the base type C. The hierarchy is used only to share unrelated implementation code D. The subtype specializes valid base behaviour Correct Answer: C. The hierarchy is used only to share unrelated implementation code Question 10In the laboratory booking case, why is EligibilityPolicy separated from BookingService? A. To place all behaviour in the user interface B. Because eligibility rules may change independently and deserve focused responsibility C. To ensure every class owns a database connection D. Because policies cannot be tested Correct Answer: B. Because eligibility rules may change independently and deserve focused responsibility
|