9. Object-Oriented Analysis, Design and ModellingIntroduction and ContextObject-oriented programming is not only a way of writing classes and methods. It is a way of understanding a problem domain, allocating responsibilities, and designing a software structure that can evolve without excessive cost. In professional development, many failures attributed to programming errors actually originate earlier: requirements were interpreted poorly, responsibilities were assigned arbitrarily, dependencies were allowed to spread, or the design did not represent the business domain accurately. Object-oriented analysis and design provide a disciplined bridge between requirements and implementation. Analysis asks what the system must represent and do. Design determines how objects, classes, collaborations, interfaces, and subsystems will provide that behaviour. Modelling makes these decisions visible through diagrams and written descriptions, especially using the Unified Modeling Language (UML). For a full-stack developer, these activities are relevant in both small and large systems. A web application may contain controllers, services, repositories, domain entities, value objects, gateways, and user-interface models. Without a coherent design, such systems commonly develop duplicated business rules, tightly coupled modules, difficult-to-test code, and unclear ownership of data and behaviour. This chapter develops a practical method for deriving responsibilities and collaborations from requirements, modelling them with UML, and evaluating designs using SOLID principles, dependency injection, and domain modelling. Learning ObjectivesAfter studying this chapter, the learner should be able to: - distinguish object-oriented analysis from object-oriented design;
- identify domain concepts, responsibilities, collaborations, and constraints from requirements;
- construct and interpret UML class, object, and sequence diagrams;
- apply cohesion, coupling, encapsulation, and polymorphism in design decisions;
- use SOLID principles and dependency injection to improve maintainability;
- distinguish entities, value objects, services, repositories, and application components;
- evaluate alternative designs and recognise common modelling failures;
- produce a maintainable object-oriented design for a realistic application scenario.
From Requirements to ResponsibilitiesA requirement describes a desired capability, rule, or quality of a system. It may be functional, such as “a customer can place an order,” or non-functional, such as “payment information must not be stored in plain text.” Requirements usually describe outcomes rather than implementation structures. The designer must therefore translate them into objects and collaborations without prematurely choosing classes merely because nouns appear in the requirements. A useful analysis begins by identifying four elements: 1. Relevant concepts: people, things, events, records, places, and policies in the domain. 2. Responsibilities: knowledge an object must maintain or actions it must perform. 3. Collaborations: requests exchanged between objects to complete a use case. 4. Constraints: business rules, security requirements, performance expectations, and technical boundaries. Consider the statement: “A customer places an order containing one or more products. The system calculates the total, reserves stock, and requests payment.” Candidate concepts include Customer, Order, Product, Inventory, and Payment. However, the wording does not mean that every noun should become a class. “Product” may be a persistent entity, whereas “total” may be a calculated value. “Payment” may represent a transaction or may be an external service boundary. Responsibilities are often classified as either knowing responsibilities or doing responsibilities. An Order may know its lines, status, and total. It may also add a line, calculate a total, or confirm payment. A repository, by contrast, may know how to retrieve and save orders but should not decide whether an order is commercially valid. Assigning behaviour to the object that possesses the relevant information generally improves cohesion and reduces unnecessary coupling. Analysis versus DesignObject-oriented analysis focuses on the problem domain and user goals. It asks: What does the system mean? Who uses it? What concepts and rules exist? Object-oriented design focuses on the solution structure. It asks: Which object performs each operation? What interfaces are required? How are dependencies supplied? Where should persistence, security, and external integration occur? The two activities overlap and are iterative. A design decision may reveal that a requirement is ambiguous; a refined requirement may require a different class boundary. Analysis should not be treated as a single phase completed before design begins. In agile and iterative development, models are refined as understanding improves. Domain Modelling: Finding Meaningful ObjectsA domain model is a conceptual representation of important domain types, their attributes, relationships, and rules. It is not necessarily a database schema and not necessarily a set of programming classes. Its value lies in creating a shared language among developers, analysts, and domain experts. Entities and Value ObjectsAn entity has a stable identity that remains meaningful even when its attributes change. A Customer with identifier C104 remains the same customer after changing an address. An Order is usually an entity because its identity is important for tracking, payment, and fulfilment. A value object is defined by its attributes rather than by an independent identity. Money, EmailAddress, DateRange, and ShippingAddress are common examples. Two Money objects representing 50 Indian rupees may be considered equal if their currency and amount match. Value objects should normally be immutable, which makes them safer to share and easier to test. Services, Repositories, and FactoriesA domain service represents meaningful domain behaviour that does not naturally belong to one entity or value object. For example, a CurrencyConversionService may calculate an exchange conversion using several currencies and an external rate source. Services should not become general-purpose containers for every operation that a developer finds inconvenient to place elsewhere. A repository provides collection-like access to entities while hiding persistence details. OrderRepository might define findById and save operations, while a database-specific implementation handles SQL or an object-relational mapper. The repository abstraction belongs close to the domain or application boundary; the database implementation belongs to infrastructure. A factory encapsulates complex or conditional creation. It is useful when constructing an object requires validation, selecting a subtype, or assembling several dependencies. A factory is unnecessary when a constructor is simple and clear. Invariants and ConstraintsAn invariant is a condition that must remain true for a valid object or aggregate. An Order may not be confirmed if it contains no lines. A bank account may not have a negative balance unless overdrafts are explicitly allowed. Such rules should be protected by the objects responsible for maintaining them, rather than scattered across controllers and user-interface code. An aggregate is a cluster of related objects treated as a consistency boundary. The aggregate root is the entry point through which external code modifies the cluster. If Order is the aggregate root, callers should not directly modify OrderLine objects in ways that bypass order rules. Aggregate boundaries should be chosen carefully: making every object part of one large aggregate can create locking, performance, and coupling problems. UML as a Design LanguageUML is a standard visual language for describing software structures and interactions. UML diagrams are communication and reasoning tools, not decorative documentation. A model is useful when it helps stakeholders identify missing requirements, questionable dependencies, or incorrect responsibilities. Class DiagramsA class diagram shows the static structure of a system. A class is commonly represented by a rectangle divided into the class name, attributes, and operations. Visibility symbols include plus for public, minus for private, and hash for protected. In modern design, diagrams often omit low-level getters and setters to concentrate on meaningful behaviour. Important relationships include: - Association: a structural relationship, such as Customer places Orders.
- Multiplicity: the number of related instances, such as 1, 0..1, or 1..*.
- Generalisation: an inheritance relationship, such as CardPayment and CashPayment deriving from Payment.
- Dependency: one element temporarily uses another.
- Aggregation: a weak whole-part relationship.
- Composition: a strong ownership relationship in which the part’s lifecycle is controlled by the whole.
- Realisation: a class implements an interface.
Composition should not be used merely because one object appears inside another in code. It expresses lifecycle and ownership. An Order may compose OrderLines if lines have no independent meaning outside that order. A Product is more likely associated with OrderLine because the same product may appear in many orders and has an independent lifecycle. A class diagram should show the level of abstraction appropriate to its purpose. An analysis model may show Customer, Order, and Product. A design model may additionally show OrderRepository, PaymentGateway, and notification interfaces. Object DiagramsAn object diagram is a snapshot of particular instances at a particular time. It is useful for explaining a class diagram and testing whether multiplicities make sense. For example, an object diagram might show order O500 containing two lines, one referring to product P10 and another to P24. It can reveal questions that a general class diagram hides: Can one line refer to two products? Can a pending order have a payment? Is a customer allowed to have no orders? Object diagrams are especially valuable in teaching, domain workshops, debugging, and explaining complex aggregate states. Sequence DiagramsA sequence diagram shows interactions over time. Participants appear across the top, and messages proceed from top to bottom. A typical checkout interaction might be represented as follows: ```text Customer -> CheckoutController: submit(orderData) CheckoutController -> CheckoutService: checkout(command) CheckoutService -> OrderRepository: find(orderId) CheckoutService -> InventoryService: reserve(items) CheckoutService -> PaymentGateway: authorize(amount) CheckoutService -> Order: confirm() CheckoutService -> OrderRepository: save(order) CheckoutService -> NotificationService: sendConfirmation(order) ``` The diagram distinguishes a request from the object that fulfils it. It also makes dependency direction visible. A controller should coordinate the application use case, while the Order entity should enforce its own state rules. An external PaymentGateway should be represented by an abstraction so that the core application does not depend directly on a specific vendor. Sequence diagrams may use conditions, loops, alternative fragments, and return messages. They should not attempt to document every line of implementation. Their purpose is to clarify a significant scenario, especially normal flows and important failure paths. Responsibility-Driven DesignResponsibility-driven design treats objects as collaborators that fulfil responsibilities. A common technique is to write a use-case scenario and identify the objects that participate in each step. The designer then asks which object has the information needed to perform each action and which object should initiate the collaboration. For a library borrowing use case, a possible collaboration is: 1. Member requests a loan. 2. LibraryService obtains the book copy. 3. BookCopy checks availability. 4. Loan is created with a due date. 5. Member records the loan. 6. LoanRepository saves the result. The model should not assign all operations to LibraryService simply because it is convenient. If Loan determines whether it is overdue, that rule belongs to Loan. If BookCopy changes its availability, BookCopy should protect that state. The application service coordinates the transaction but should avoid becoming a “god class.” Two classic responsibility heuristics are useful. Information Expert suggests assigning an operation to the class with the information needed to perform it. Creator suggests that a class should create another when it closely aggregates, contains, or records the created object. These heuristics are not absolute rules; security, transaction boundaries, and architectural concerns may justify another choice. SOLID Principles and Design QualitySOLID principles are guidelines for managing change and dependencies. They should be applied thoughtfully rather than mechanically. Single Responsibility Principle (SRP) states that a class should have one coherent reason to change. It does not mean that every class must contain only one method. An Invoice class may calculate totals and enforce invoice rules because these behaviours concern one responsibility. Combining invoice calculation, PDF rendering, database access, and email delivery would create several unrelated reasons to change. Open–Closed Principle (OCP) recommends that software entities be open for extension but closed for modification in areas likely to vary. A payment processor can depend on a PaymentMethod interface, allowing new payment methods to be added without changing checkout coordination. OCP does not require predicting every future variation; abstraction should be introduced where a genuine variation point exists. Liskov Substitution Principle (LSP) requires that subtypes honour the expectations established by their abstractions. If a method promises to accept any PaymentMethod and process successfully or return a defined failure, a subtype should not unexpectedly reject valid inputs or violate state assumptions. Inheritance is inappropriate when a subtype cannot preserve the behavioural contract. Interface Segregation Principle (ISP) advises creating focused interfaces rather than forcing clients to depend on operations they do not use. A ReadableStore and WritableStore may be better than one large DataStore interface when different clients need different capabilities. Dependency Inversion Principle (DIP) states that high-level policy should not depend directly on low-level implementation details. Both should depend on abstractions. CheckoutService may depend on PaymentGateway rather than StripePaymentGateway. The concrete adapter is configured at the application boundary. Design quality also depends on cohesion and coupling. Cohesion measures how closely related the responsibilities of a module are. Coupling concerns how strongly modules depend on each other. High cohesion and low, purposeful coupling generally improve changeability, testing, and comprehension. A design with many interfaces is not automatically loosely coupled; meaningless abstractions can increase complexity without reducing dependency. Dependency Injection and Architectural BoundariesDependency injection supplies an object’s dependencies from outside rather than making the object construct them internally. Constructor injection is normally preferred because it makes required dependencies explicit and permits an object to be valid immediately after construction. ```text class CheckoutService: constructor(orderRepository, inventoryService, paymentGateway) checkout(command): order = orderRepository.find(command.orderId) inventoryService.reserve(order.items) paymentGateway.authorize(order.total()) order.confirm() orderRepository.save(order) ``` The service does not create a database repository or select a payment vendor. A composition root—often the application startup configuration—connects interfaces to concrete implementations. This improves testing because a fake repository or test payment gateway can be supplied. Dependency injection is not a substitute for good design. Injecting ten dependencies into one class may merely expose that the class has too many responsibilities. Nor should every simple object be managed by a dependency-injection container. Containers are useful for lifecycle management and configuration, but excessive magic can obscure object creation and make debugging more difficult. An Illustrative Case: Designing an Online Examination SystemBackgroundThe following fictional case concerns “AssessPro,” a web-based examination platform used by universities. It supports question banks, timed examinations, candidate submissions, automatic marking for objective questions, and publication of results. Situation and ProblemAn initial prototype places all behaviour in an ExaminationController. The controller retrieves questions, starts attempts, checks answers, calculates scores, writes database records, and sends result emails. A change to scoring rules requires changes in several controller methods. Testing is difficult because every test requires a database and an email service. AnalysisThe principal domain concepts are Examination, Question, Attempt, Answer, Candidate, Score, and Result. An Attempt has identity and lifecycle: it may be started, submitted, or expired. A Score is a value object containing marks and perhaps a grading policy. The examination owns its configured questions, while a question can exist in a question bank independently. Important invariants include the following: an attempt cannot be submitted twice; answers must belong to questions in the examination; submission after the deadline is rejected or handled according to policy; an objective answer may be marked automatically; and results must not be published before submission is accepted. The system also has technical collaborators: AttemptRepository, QuestionRepository, ScoringPolicy, Clock, and ResultPublisher. Clock is an abstraction because time-dependent behaviour should be testable without waiting for real time. ResultPublisher may have an email implementation and a portal-notification implementation. Design ApproachAn Examination entity can provide access to its questions and rules for starting an attempt. Attempt can record answers and enforce its lifecycle. A ScoringPolicy interface can represent alternative rules, such as negative marking or partial credit. A SubmitAttemptService coordinates the use case: 1. retrieve the attempt; 2. obtain the current time from Clock; 3. ask the attempt to validate and submit; 4. ask ScoringPolicy to calculate the score; 5. save the attempt and result; 6. publish a notification through ResultPublisher. The service coordinates, but it does not directly manipulate internal collections or duplicate lifecycle rules. The concrete database repositories and notification adapters are supplied through dependency injection. A simplified sequence is: ```text Candidate -> AttemptController: submit(attemptId, answers) AttemptController -> SubmitAttemptService: execute(command) SubmitAttemptService -> AttemptRepository: find(attemptId) SubmitAttemptService -> Clock: now() SubmitAttemptService -> Attempt: submit(answers, time) SubmitAttemptService -> ScoringPolicy: calculate(attempt) SubmitAttemptService -> AttemptRepository: save(attempt) SubmitAttemptService -> ResultPublisher: publish(result) ``` OutcomeThe design isolates examination rules from HTTP, database, and email concerns. A new scoring policy can be introduced by implementing ScoringPolicy. Tests can use an in-memory repository, fixed clock, and fake publisher. The controller remains thin, and the domain objects protect their own invariants. Learning PointsThe case demonstrates that maintainability comes from meaningful boundaries rather than from a large number of classes. Domain entities should protect important state, application services should coordinate use cases, and infrastructure should implement replaceable technical details. The model must also represent time, policies, and lifecycle states explicitly when they affect correctness. Common Design Failures and Trade-offsOne frequent mistake is noun-driven modelling, in which every noun becomes a class and every verb becomes a method. This produces classes with no meaningful behaviour and may confuse data structures with domain concepts. Another is the anemic domain model, where entities contain only fields while all rules are placed in services. An anemic model can be reasonable for simple data-centric applications, but it is risky when entities have rich invariants and lifecycle behaviour. The opposite error is excessive complexity: speculative abstractions, deep inheritance hierarchies, and interfaces created for classes that have no realistic alternative implementation. Design should respond to actual volatility and business rules. A god object accumulates unrelated responsibilities. A feature envy smell occurs when one class repeatedly accesses another class’s data to perform that class’s work. Shotgun surgery occurs when one change requires edits across many unrelated modules. These smells are signals for review, not automatic proof that refactoring is required. Inheritance can communicate a genuine “is-a” relationship, but composition often offers greater flexibility. A payment processor can compose a fraud-checking policy rather than inherit from many specialized processor classes. The appropriate decision depends on substitutability, variation, and the stability of the abstraction. Models can also become obsolete. Documentation should be updated when a model supports communication or architectural decisions, while trivial implementation detail need not be diagrammed. Lightweight, executable tests and code reviews should complement UML rather than be replaced by it. Professional, Ethical, and Contemporary ConsiderationsObject-oriented design affects more than maintainability. Poor boundaries can expose sensitive data, permit unauthorized state changes, or make auditability difficult. Security responsibilities should be explicit: authorization must not be inferred from object ownership, and sensitive credentials should not be placed in domain objects unnecessarily. Payment and identity integrations should use approved interfaces, secure storage, and appropriate logging practices. In regulated or high-impact systems, design decisions should support traceability. Important state transitions, decision rules, and overrides may require audit records. Privacy principles favour data minimisation and clear separation between operational data and analytics data. Contemporary systems increasingly combine object-oriented cores with REST APIs, event-driven messaging, serverless functions, and microservices. Domain modelling remains useful, but boundaries must account for network failure, eventual consistency, idempotency, and versioned contracts. An object interaction that is safe in one process may be expensive or unreliable across a network. A domain event such as AttemptSubmitted can decouple result publication, but asynchronous processing introduces delivery and ordering concerns. AI-assisted coding can generate classes, tests, and diagrams, but generated designs still require human evaluation. Developers must verify domain rules, security assumptions, error handling, copyright or licensing constraints, and the suitability of abstractions. Tools can accelerate modelling; they cannot determine the organisation’s true business meaning without stakeholder validation. Best Practices for Object-Oriented ModellingBegin with a use case and a concrete scenario rather than an abstract list of classes. Use domain language consistently and validate it with stakeholders. Assign responsibilities to the object that owns the relevant knowledge, while keeping application services responsible for coordination. Protect invariants at the boundary of the object or aggregate that owns them. Prefer small, cohesive interfaces and constructor injection for required dependencies. Depend on abstractions at genuine variation or infrastructure boundaries. Use UML selectively: class diagrams for structure, object diagrams for concrete snapshots, and sequence diagrams for important collaborations. Model failure paths as well as successful paths. Review designs for coupling, testability, security, and likely sources of change. Refactor when responsibilities become scattered or concentrated. Finally, keep models and code aligned enough that both remain useful forms of communication. Chapter SummaryObject-oriented analysis and design transform requirements into meaningful domain concepts, responsibilities, collaborations, and technical boundaries. Analysis explores the problem domain; design allocates behaviour and dependencies to a solution structure. Entities represent identity, value objects represent descriptive values, services coordinate or express behaviour that does not belong to one object, and repositories isolate persistence. UML class diagrams describe static structure, object diagrams show instance snapshots, and sequence diagrams explain time-ordered collaborations. Responsibility-driven design, information expertise, cohesion, and controlled coupling help produce understandable systems. SOLID principles provide guidance for managing change, while dependency inversion and dependency injection separate high-level policy from infrastructure. The AssessPro case illustrates how domain entities can protect invariants while application services coordinate repositories, clocks, scoring policies, and notifications. Effective designs avoid both god objects and needless abstraction. They also account for security, privacy, auditability, distributed execution, and the responsible use of contemporary development tools. Key TermsEntity: A domain object identified by a stable identity over time. Value object: An immutable or conceptually descriptive object defined by its attributes rather than identity. Responsibility: Knowledge or behaviour assigned to an object. Collaboration: A purposeful interaction in which objects exchange requests or information. Domain model: A conceptual representation of important domain concepts, relationships, and rules. Invariant: A condition that must remain true for an object or aggregate to be valid. Aggregate: A consistency boundary containing related domain objects. Aggregate root: The controlled entry point for modifying an aggregate. Domain service: A stateless operation representing domain behaviour not naturally owned by one entity. Repository: An abstraction that provides access to persisted domain objects. Factory: An object or operation that encapsulates complex creation logic. UML: A standard visual language for modelling software structure and behaviour. Multiplicity: The allowed number of instances participating in a relationship. Cohesion: The degree to which the responsibilities of a module are closely related. Coupling: The degree of dependency between modules or classes. Polymorphism: The ability to use different implementations through a common abstraction. Dependency injection: Supplying an object’s dependencies from outside the object. SOLID: A group of principles for designing maintainable object-oriented software. Composition root: The application boundary where concrete dependencies are assembled. Domain event: A record that an important occurrence in the domain has taken place. Review QuestionsShort Answer Questions1. How does object-oriented analysis differ from object-oriented design? 2. What distinguishes an entity from a value object? 3. What information is communicated by a UML sequence diagram? 4. Define an invariant and give one example. 5. Why is constructor injection commonly preferred? Long Answer Questions1. Explain how requirements can be transformed into responsibilities and collaborations. Illustrate your answer with an order-processing example. 2. Describe UML class, object, and sequence diagrams and explain when each is most useful. 3. Explain the SOLID principles and relate each one to maintainability. 4. Discuss the roles of entities, value objects, domain services, repositories, and factories in a domain model. 5. Explain how dependency inversion and dependency injection separate application policy from infrastructure. Analytical / Application Questions1. A CustomerService class validates customers, sends emails, writes database records, and calculates loyalty discounts. Analyse the design using SRP and responsibility-driven design, then propose a revised collaboration structure. 2. Design a sequence of collaborating objects for cancelling an online hotel reservation. Identify at least two invariants, one external dependency, and one suitable abstraction. 3. A proposed subclass of ReadOnlyAccount introduces a withdraw method that sometimes changes the balance and sometimes throws an exception for valid account operations. Evaluate whether the design satisfies LSP and recommend an alternative. 22. Multiple Choice Questions with AnswersQuestion 1Which responsibility is most appropriately assigned to an Order entity? A. Opening a database connection B. Calculating its own total from its order lines C. Rendering an HTML page D. Selecting a cloud hosting provider Correct Answer: B. Calculating its own total from its order lines Question 2Which statement best describes a value object? A. It is always stored in a separate database table B. It is defined primarily by a stable identity C. It is defined by its attributes and is commonly immutable D. It must contain application-controller logic Correct Answer: C. It is defined by its attributes and is commonly immutable Question 3What does a UML sequence diagram primarily communicate? A. The physical layout of database tables B. The time-ordered interactions among participants C. The inheritance hierarchy of a system D. The deployment locations of servers Correct Answer: B. The time-ordered interactions among participants Question 4A CheckoutService depends directly on StripePaymentGateway. Which design principle is most directly violated? A. Dependency Inversion Principle B. Single Responsibility Principle C. Liskov Substitution Principle D. Interface Segregation Principle Correct Answer: A. Dependency Inversion Principle Question 5Which example best illustrates composition? A. A Product is referenced by many OrderLines B. An Order owns OrderLines whose lifecycle ends with the Order C. A Customer uses a PaymentGateway D. A Controller calls a Service Correct Answer: B. An Order owns OrderLines whose lifecycle ends with the Order Question 6Why is a Clock abstraction useful in a timed examination system? A. It automatically improves database indexing B. It allows time-dependent behaviour to be tested deterministically C. It removes the need for examination rules D. It guarantees that all users have the same time zone Correct Answer: B. It allows time-dependent behaviour to be tested deterministically Question 7Which design most clearly violates the Single Responsibility Principle? A. An Order validates that it has at least one line before confirmation B. A Money value object compares amount and currency C. An Invoice class calculates totals, writes SQL, renders PDFs, and sends emails D. A Repository saves and retrieves orders Correct Answer: C. An Invoice class calculates totals, writes SQL, renders PDFs, and sends emails Question 8What is the main purpose of an aggregate root? A. To provide controlled access to changes within an aggregate B. To replace all domain services C. To represent every database table in one object D. To ensure that all objects inherit from one superclass Correct Answer: A. To provide controlled access to changes within an aggregate Question 9A client needs only read operations, but a DataStore interface also requires delete and update methods. Which principle suggests splitting the interface? A. Open–Closed Principle B. Interface Segregation Principle C. Liskov Substitution Principle D. Single Responsibility Principle Correct Answer: B. Interface Segregation Principle Question 10Which approach best reduces the risk of a god object? A. Move every operation into the controller B. Give one service responsibility for all use cases C. Allocate cohesive responsibilities to domain objects and collaborating services D. Replace all classes with global functions Correct Answer: C. Allocate cohesive responsibilities to domain objects and collaborating services
|