Object-Oriented Programming (Java/Python)

Chapter 9
Object-Oriented Analysis, UML, SOLID, and Refactoring

Bachelors Program in Full Stack Development
Object-Oriented Programming (Java/Python)   •   Bachelor

Object-Oriented Analysis, UML, SOLID, and Refactoring

Introduction

Object-oriented programming is not only the activity of writing classes and methods. Professional software development begins earlier: developers must interpret requirements, identify important domain concepts, assign responsibilities, model collaborations, and make design decisions that remain adaptable as the system changes. Object-oriented analysis and design provide methods for moving from an informal problem description to an implementable software structure.

This chapter connects four activities:

  1. Object-oriented analysis (OOA): discovering concepts, responsibilities, and interactions from requirements.
  2. Unified Modeling Language (UML): communicating structure and behavior through standardized diagrams.
  3. Design quality evaluation: examining coupling, cohesion, maintainability, and technical debt.
  4. Refactoring: improving internal structure without changing observable behavior.

The discussion applies to both Java and Python. The languages differ in syntax, typing discipline, and runtime mechanisms, but the underlying design principles are largely shared.

Learning Objectives

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

  • translate requirements into actors, use cases, domain concepts, and responsibilities;
  • distinguish analysis models from design and implementation models;
  • construct and interpret use-case, class, and sequence diagrams;
  • apply the SOLID principles critically rather than mechanically;
  • explain coupling, cohesion, code smells, and technical debt;
  • make evidence-based refactoring decisions;
  • evaluate design alternatives in a realistic software case.

From Requirements to Responsibilities

A requirement describes a desired capability, constraint, or quality attribute. For example:

A library member can borrow an available book copy. The system records the loan and prevents borrowing when the member has reached the borrowing limit.

This statement contains several elements:

  • Actor: library member;
  • Goal: borrow a book copy;
  • Domain concepts: member, book copy, loan, borrowing limit;
  • Rules: only available copies may be borrowed, and a limit must be enforced;
  • Possible system responsibilities: checking availability, checking eligibility, creating a loan, and changing copy status.

A useful analysis question is not merely “What classes should be created?” but:

Which object has the information needed to make a decision, and which object should perform the operation that follows?

This question is related to the Information Expert principle. A responsibility should normally be assigned to the object that possesses the most appropriate information for carrying it out. For example, a Member may determine whether another loan is permitted because it knows its active loans and borrowing limit. A BookCopy may determine whether it is available because it owns its current status.

Functional and Non-functional Requirements

Functional requirements specify behavior, such as registering a user or calculating a payment. Non-functional requirements describe qualities or constraints, such as performance, security, accessibility, reliability, and legal compliance. A design that satisfies functional requirements but exposes personal information or cannot be tested reliably is not a successful design.

Requirements should be made testable. “The system should be fast” is weak; “95% of search requests should complete within 500 milliseconds under the stated workload” is more useful because it identifies a measurable acceptance condition.

Use-Case Analysis

A use case describes a goal-oriented interaction between an actor and a system. A concise use-case specification normally includes:

  • name and goal;
  • primary actor;
  • stakeholders and interests;
  • preconditions;
  • trigger;
  • main success scenario;
  • alternative and exception flows;
  • postconditions;
  • business rules and non-functional constraints.

#### Example: Borrow Book Copy

Primary actor: Library member

Preconditions: The member is authenticated; the copy exists.

Main flow:

  1. The member requests a copy.
  2. The system verifies that the member may borrow.
  3. The system verifies that the copy is available.
  4. The system creates a loan with a due date.
  5. The system marks the copy as on loan.
  6. The system confirms the transaction.

Alternative flows:

  • If the member has reached the limit, the request is rejected.
  • If the copy is unavailable, the system offers a reservation.
  • If persistence fails, no partial loan must be recorded.

Use cases should describe externally meaningful behavior, not internal method calls. “Call LoanService.createLoan()” is a design detail, whereas “Record the loan” is an appropriate system behavior.

Domain Models and Responsibility Assignment

A domain model is a conceptual representation of important objects, relationships, and vocabulary in a problem domain. It is not necessarily a database schema and does not need to contain implementation details such as getters, framework annotations, or private helper methods.

For the library example, likely concepts include Member, Book, BookCopy, Loan, and Reservation. A Book represents an intellectual work, while a BookCopy represents a physical or individually trackable item. Distinguishing these concepts prevents errors such as treating ten physical copies as one borrowable object.

CRC-Style Analysis

CRC means Class–Responsibility–Collaborator. For each candidate class, identify:

ClassResponsibilitiesCollaborators
Memberdetermine borrowing eligibility; maintain member identityLoan
BookCopyreport and change availability stateLoan
Loanrecord borrower, copy, dates, and statusMember, BookCopy
LoanServicecoordinate the borrowing transactionMember, BookCopy, LoanRepository

CRC analysis is valuable because it emphasizes behavior and collaboration rather than data containers. A class with only fields and accessors may be appropriate in some cases, but widespread “anemic domain models” can place all business logic in procedural services and weaken encapsulation.

A responsibility should be:

  • cohesive with the class’s purpose;
  • supported by the class’s data or legitimate collaborators;
  • testable;
  • expressed at an appropriate abstraction level;
  • consistent with security and transaction boundaries.

UML as a Communication Language

The Unified Modeling Language is a standardized visual language for describing software systems. UML is not a programming language and does not prescribe one architecture. It helps teams communicate assumptions and design decisions.

Use-Case Diagrams

A use-case diagram shows actors, system goals, and relationships among use cases. It is useful during requirements analysis, but it does not describe algorithms or class structure. An include relationship indicates required reused behavior; an extend relationship indicates optional or conditional behavior added under specified circumstances. These relationships should not be used merely to make a diagram look detailed.

Class Diagrams

A class diagram represents structural relationships. Common elements include:

  • class names, attributes, and operations;
  • association relationships;
  • multiplicities such as 1, 0..1, and 0..*;
  • generalization or inheritance;
  • aggregation and composition;
  • dependency relationships;
  • visibility markers such as + public and - private.

For example, one Member may have zero or many Loan objects, while each loan belongs to exactly one member. This can be expressed conceptually as:

Member 1 ─── 0..* Loan

Multiplicity is a constraint, not decoration. If the implementation allows a loan without a member when the domain forbids it, the code and model disagree.

Inheritance should represent a genuine “is-a” relationship with substitutability. If PrintedBook and ElectronicBook share a stable abstraction and can be used wherever a Book is expected, inheritance may be appropriate. If the variation is likely to change independently, composition may be safer.

Sequence Diagrams

A sequence diagram shows objects or components as lifelines and messages in time order. For borrowing, a plausible interaction is:

  1. Member sends borrow(copy) to LibraryService.
  2. LibraryService asks Member whether borrowing is allowed.
  3. It asks BookCopy whether it is available.
  4. It creates a Loan.
  5. It instructs BookCopy to become unavailable.
  6. It saves the loan and returns confirmation.

The sequence diagram exposes ordering and responsibility problems. For example, changing copy status before successfully saving the loan may create inconsistent state unless the operation is transactional or compensating logic is provided.

Analysis, Design, and Implementation Models

An analysis model uses domain language and avoids premature technology decisions. A design model introduces interfaces, services, persistence boundaries, and architectural concerns. An implementation model contains concrete Java or Python classes, framework configuration, and deployment details. Confusing these levels can make an early model unnecessarily complex.

SOLID Principles

SOLID is a set of design principles associated with maintainable object-oriented software. They are heuristics, not laws. Applying them without considering context can produce excessive abstraction.

Single Responsibility Principle

The Single Responsibility Principle (SRP) states that a class should have one reason to change. “Responsibility” is best interpreted as a coherent source of change, often associated with one stakeholder or business concern.

A class that validates loans, formats PDF reports, sends email, and writes SQL has several independent reasons to change. Separating these concerns can improve cohesion and testability. SRP does not mean every class must contain only one method.

Open–Closed Principle

The Open–Closed Principle (OCP) says that software entities should be open for extension but closed for modification. In practice, stable abstractions can allow new behavior to be added with limited changes to tested code.

For example, a checkout system might depend on a DiscountPolicy interface. New policies can be introduced as implementations rather than adding a growing conditional chain. OCP is valuable when variation is expected; abstracting every possible variation creates unnecessary complexity.

Liskov Substitution Principle

The Liskov Substitution Principle (LSP) requires that objects of a subtype can replace objects of the supertype without violating client expectations. A subtype must preserve relevant contracts, including valid inputs, guarantees, and behavioral meaning.

A subtype that throws an exception for an operation promised by the parent, or that weakens an important postcondition, may violate LSP. The classic rectangle–square example illustrates that mathematically valid inheritance can still be behaviorally unsuitable in mutable software APIs.

Interface Segregation Principle

The Interface Segregation Principle (ISP) recommends several focused interfaces rather than one large interface that forces clients to depend on unused operations. A reporting client may need ReadableReport, while an administrative client may also need EditableReport.

In Python, protocols and duck typing can support this idea without explicit interface declarations. In Java, interfaces provide compile-time contracts, but they should remain focused and meaningful.

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) states that high-level policy should not depend directly on low-level details; both should depend on abstractions. Details should depend on policies through stable interfaces.

class LoanService {
    private final LoanRepository repository;

    LoanService(LoanRepository repository) {
        this.repository = repository;
    }
}

The service depends on LoanRepository, not on a concrete SQL implementation. Dependency injection makes the dependency explicit and permits a test double. DIP does not require an interface for every class; an abstraction is justified when it provides substitutability or isolates a volatile detail.

Coupling, Cohesion, and Design Metrics

Coupling is the degree to which one module depends on others. High coupling increases the number of places affected by change. Cohesion is the degree to which the responsibilities within a module belong together. High cohesion usually makes a class easier to understand and test.

Common warning signs include:

  • a class importing many unrelated modules;
  • frequent changes to several classes for one feature;
  • methods that manipulate another object’s internal data extensively;
  • a class with unrelated groups of methods;
  • inheritance used only to reuse a small implementation detail.

Metrics can support judgment but cannot replace it. Examples include:

  • Weighted Methods per Class (WMC): often the sum of method complexities in a class. The exact result depends on the chosen complexity measure.
  • Coupling Between Object classes (CBO): the number of other classes to which a class is coupled, according to a defined measurement rule.
  • Response For a Class (RFC): the number of methods potentially executed in response to a message to the class.
  • Lack of Cohesion in Methods (LCOM): a family of metrics measuring the relationship between methods’ use of instance data. Different LCOM definitions produce different values.

Because metric definitions vary, teams must record the tool, version, threshold, and calculation method. A high CBO value is a prompt for investigation, not proof of poor design. A small class can still have severe security defects, and a large class may be justified by a stable domain boundary.

Code Smells and Technical Debt

A code smell is a symptom suggesting a design or maintainability problem. It is not automatically a defect. Common smells include:

  • Long Method: too much logic in one operation;
  • Large Class: many unrelated responsibilities;
  • Duplicate Code: the same knowledge appears in multiple places;
  • Long Parameter List: a method requires too many independent arguments;
  • Feature Envy: a method uses another object’s data more than its own;
  • Primitive Obsession: domain concepts are represented only by strings or numbers;
  • Switch or conditional complexity: behavior selection grows without a stable abstraction;
  • Shotgun Surgery: one change requires small edits across many classes;
  • Speculative Generality: abstractions exist without a demonstrated need;
  • Inappropriate Intimacy: classes depend excessively on each other’s internals.

Technical debt is the future cost created when a shortcut, obsolete design, or deferred improvement makes later change more expensive or risky. Debt may be intentional, such as releasing a validated prototype quickly, or unintentional, such as allowing inconsistent conventions to accumulate.

A practical debt register should record:

ItemEvidenceImpactRemediationPriority
duplicated pricing rulesfour implementations differinconsistent invoicescentralize policyhigh
slow report queryproduction traceuser delayadd index and measuremedium
obsolete interfaceno active clientsconfusion onlyremove after dependency checklow

Prioritization should consider user impact, probability, remediation effort, security, and change frequency. Debt should not be repaid merely because code is aesthetically unsatisfying.

Refactoring as Evidence-Based Change

Refactoring is a disciplined change to the internal structure of software that preserves externally observable behavior. It differs from rewriting, which may replace large portions without preserving behavior incrementally.

A safe refactoring cycle is:

  1. establish a clean baseline;
  2. identify a specific smell or risk;
  3. characterize current behavior with tests, logs, or acceptance criteria;
  4. make one small structural change;
  5. run automated tests and static checks;
  6. review the resulting design and measurement evidence;
  7. commit the change separately;
  8. repeat only when the benefit remains justified.

Typical refactorings include:

  • Extract Method;
  • Extract Class;
  • Rename Method or Variable;
  • Move Method;
  • Replace Conditional with Polymorphism;
  • Introduce Parameter Object;
  • Encapsulate Collection;
  • Replace Inheritance with Composition;
  • Remove Duplicate Code.

Refactoring is not automatically safe. Tests may omit important behavior, reflection may depend on names, serialization may depend on fields, and concurrency may expose timing changes. Public APIs require compatibility analysis. Database migrations and distributed-system changes often need staged rollout rather than a purely local refactoring.

Before-and-After Example

A fragile Python pricing function might contain unrelated discount rules:

def price(order, customer_type):
    total = sum(item.price * item.quantity for item in order.items)
    if customer_type == "student":
        total *= 0.90
    elif customer_type == "premium":
        total *= 0.80
    return total

A policy abstraction can isolate variation:

class DiscountPolicy:
    def discount(self, total):
        return total

class StudentDiscount(DiscountPolicy):
    def discount(self, total):
        return total * 0.90

class PremiumDiscount(DiscountPolicy):
    def discount(self, total):
        return total * 0.80

def price(order, policy):
    subtotal = sum(i.price * i.quantity for i in order.items)
    return policy.discount(subtotal)

This change is beneficial if discount policies are growing, independently tested, or changed by different stakeholders. If there are only two stable rules, the abstraction may be unnecessary. Evidence, not the appearance of SOLID compliance, should determine the decision.

Illustrative Case: Campus Equipment Rental

Background

A university rents cameras, microphones, and laptops to students. The first implementation contains a RentalManager class that authenticates users, checks eligibility, calculates fees, sends email, writes database records, and formats receipts.

Problem

A new requirement introduces staff discounts and late-return penalties. Developers must modify several conditional branches. Automated tests are slow because every test constructs a database connection and sends notification messages. Production incidents show that equipment is sometimes marked available before a rental record is committed.

Analysis

The class has low cohesion and high coupling. It violates SRP because rental policy, persistence, notification, and presentation are mixed. It also has a transaction-ordering defect. The domain concepts are Customer, Equipment, Rental, FeePolicy, and NotificationGateway.

A sequence analysis identifies a required invariant:

Equipment availability and the corresponding rental record must agree after a successful transaction.

The team measures baseline behavior: rental creation success rate, test execution time, number of changed files per pricing change, and frequency of inconsistent availability records.

Approach

The team proceeds incrementally:

  1. writes characterization tests for existing fee and eligibility behavior;
  2. extracts FeePolicy from RentalManager;
  3. injects RentalRepository and NotificationGateway abstractions;
  4. moves equipment state transition into a domain method such as equipment.reserve();
  5. places rental creation and state update in one transactional application operation;
  6. separates receipt formatting from rental processing;
  7. introduces a staff discount policy and tests it independently;
  8. uses logs and database constraints to verify consistency.

A simplified Java-style design is:

interface FeePolicy {
    Money calculate(Rental rental);
}

final class RentalService {
    private final RentalRepository rentals;
    private final EquipmentRepository equipment;
    private final FeePolicy fees;

    RentalService(RentalRepository rentals,
                  EquipmentRepository equipment,
                  FeePolicy fees) {
        this.rentals = rentals;
        this.equipment = equipment;
        this.fees = fees;
    }

    RentalReceipt rent(Customer customer, Equipment item) {
        customer.checkEligibility();
        item.reserve();
        Rental rental = Rental.start(customer, item, fees);
        rentals.save(rental);
        equipment.save(item);
        return RentalReceipt.from(rental);
    }
}

In production, the operation must be integrated with the application’s transaction mechanism. The example illustrates responsibility separation, not a complete persistence implementation.

Outcome

The design now permits fee policies to change independently. Unit tests can use in-memory repositories and fake gateways. The transaction boundary is explicit, and monitoring verifies that successful rentals do not leave equipment and rental records inconsistent. The team does not create abstractions for every entity; it abstracts the parts that vary or are difficult to test.

Learning Points

  • Requirements reveal both domain concepts and invariants.
  • Sequence analysis can expose ordering defects that class diagrams hide.
  • SOLID principles are most useful when connected to observed change and testing problems.
  • Refactoring should be incremental, measurable, and protected by tests.

Limitations, Governance, and Contemporary Practice

UML diagrams become harmful when they are maintained as ceremonial documents that no longer reflect code. Teams should keep diagrams focused, version them with the project, and mark whether they describe current or proposed design. Lightweight diagrams created during design reviews are often more useful than exhaustive diagrams that nobody updates.

AI-assisted programming tools can suggest classes, tests, or refactorings, but generated changes require human review. Developers remain responsible for licensing, security, privacy, correctness, accessibility, and maintainability. AI may reproduce insecure patterns, invent APIs, or conceal assumptions. Sensitive source code and personal data should not be submitted to tools without organizational authorization.

Design decisions should also consider regulatory and ethical requirements. A domain model for health, finance, education, or identity systems must represent authorization, auditability, retention, consent, and data minimization where applicable. Encapsulation is not merely a style preference when it protects sensitive state and enforces policy consistently.

Best Practices

  • Begin with stakeholder goals and domain vocabulary.
  • Model responsibilities before choosing detailed class hierarchies.
  • Use multiplicities and invariants explicitly.
  • Prefer composition when behavior varies independently.
  • Depend on abstractions at volatile boundaries, not everywhere.
  • Keep classes cohesive and interfaces focused.
  • Treat metrics as evidence for investigation rather than automatic verdicts.
  • Refactor in small, tested steps.
  • Measure technical debt through change cost, defects, risk, and operational evidence.
  • Keep diagrams and code aligned.
  • Review security, privacy, accessibility, and legal constraints as design concerns.

Chapter Summary

Object-oriented analysis translates requirements into actors, use cases, domain concepts, invariants, and responsibilities. CRC analysis and the Information Expert principle help assign behavior to appropriate objects. UML use-case, class, and sequence diagrams communicate goals, structure, relationships, and message ordering, while distinguishing analysis models from implementation models.

SOLID principles support maintainability when applied as context-sensitive heuristics. Coupling should generally be controlled and cohesion strengthened, but metrics such as CBO, WMC, RFC, and LCOM require defined measurement rules and careful interpretation. Code smells indicate possible design problems, whereas technical debt describes future cost and risk created by shortcuts or obsolete structures.

Refactoring improves internal structure while preserving observable behavior. Safe refactoring depends on tests, small changes, version control, review, and evidence. In professional development, design quality is not measured by the number of patterns or diagrams but by the system’s ability to remain understandable, testable, secure, and adaptable as requirements evolve.

Key Terms

Object-oriented analysis: Discovering domain concepts, behavior, and responsibilities from requirements.

Use case: A goal-oriented interaction between an actor and a system.

Actor: A person, organization, device, or external system interacting with a system.

Domain model: A conceptual model of important domain concepts and relationships.

Responsibility: A coherent obligation assigned to a class or component.

Information Expert: A responsibility-assignment principle favoring the object with the relevant information.

CRC: Class–Responsibility–Collaborator analysis technique.

UML: A standardized visual language for modeling software systems.

Multiplicity: A constraint describing how many instances may participate in a relationship.

Sequence diagram: A diagram showing interactions in temporal order.

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

Coupling: The degree of dependency between modules.

SOLID: A group of object-oriented design principles.

Code smell: A symptom suggesting a possible design or maintainability problem.

Technical debt: Future cost or risk created by shortcuts or deferred improvement.

Refactoring: Behavior-preserving improvement of internal software structure.

Dependency injection: Supplying an object’s dependencies from outside the object.

Invariant: A condition that must remain true within a valid system state.

Characterization test: A test that records existing behavior before structural change.

Composition: Building behavior from collaborating objects rather than relying on inheritance.

Review Questions

Short Answer Questions

  1. What is the difference between a functional requirement and a non-functional requirement?
  2. Why should a use case describe an actor’s goal rather than internal method calls?
  3. Define coupling and cohesion.
  4. What does the Dependency Inversion Principle recommend?
  5. Why are code metrics not sufficient by themselves to prove design quality?

Long Answer Questions

  1. Explain how a use-case specification can be transformed into domain concepts and responsibilities. Use an example.
  2. Compare class diagrams and sequence diagrams, including the kinds of design problems each can reveal.
  3. Explain all five SOLID principles and discuss one limitation of applying SOLID mechanically.
  4. Describe an evidence-based refactoring process and explain how tests contribute to safety.
  5. Discuss technical debt, including its causes, measurement evidence, and prioritization factors.

Analytical / Application Questions

  1. A PaymentController validates orders, calculates tax, contacts a payment gateway, writes audit records, and generates HTML receipts. Identify likely smells and propose a responsibility-based redesign.
  2. A subclass of Document inherits print() but throws an exception because digital documents cannot be printed. Analyze whether this may violate LSP and propose alternatives.
  3. A team wants to introduce an interface for every class before any variation has been observed. Evaluate this proposal using SOLID, coupling, cohesion, testing, and technical-debt reasoning.

Multiple Choice Questions with Answers

Question 1

Which artifact primarily describes a user goal and the interaction required to achieve it?

A. Use-case specification

B. Database index

C. Deployment descriptor

D. Package manager lockfile

Correct Answer: A. Use-case specification

Question 2

According to the Information Expert principle, a responsibility should generally be assigned to the object that:

A. Has the most methods

B. Owns the user interface

C. Has the information needed to perform it

D. Is highest in the inheritance hierarchy

Correct Answer: C. Has the information needed to perform it

Question 3

What does a sequence diagram emphasize?

A. Source-code licensing

B. The temporal order of messages between participants

C. The physical layout of a database

D. The number of lines in each class

Correct Answer: B. The temporal order of messages between participants

Question 4

Which SOLID principle concerns substituting subtype objects without violating client expectations?

A. Single Responsibility Principle

B. Open–Closed Principle

C. Interface Segregation Principle

D. Liskov Substitution Principle

Correct Answer: D. Liskov Substitution Principle

Question 5

Which situation is the clearest example of low cohesion?

A. A class groups operations for one domain concept

B. A class contains unrelated payment, email, and report-formatting responsibilities

C. A class uses private fields

D. A class has a focused interface

Correct Answer: B. A class contains unrelated payment, email, and report-formatting responsibilities

Question 6

What is the main purpose of dependency injection in the examples in this chapter?

A. To eliminate all concrete classes

B. To make dependencies explicit and replaceable

C. To increase inheritance depth

D. To prevent unit testing

Correct Answer: B. To make dependencies explicit and replaceable

Question 7

Which statement about technical debt is most accurate?

A. It always refers to defective code

B. It can be prioritized using evidence about cost, risk, and impact

C. It is measured only by lines of code

D. It should always be removed immediately

Correct Answer: B. It can be prioritized using evidence about cost, risk, and impact

Question 8

What distinguishes refactoring from a behavior-changing rewrite?

A. Refactoring preserves externally observable behavior

B. Refactoring never changes class names

C. Refactoring requires inheritance

D. Refactoring cannot use automated tests

Correct Answer: A. Refactoring preserves externally observable behavior

Question 9

A team extracts a discount policy because pricing rules change frequently and independently. Which principle is most directly supported?

A. Open–Closed Principle

B. Law of Demeter only

C. Principle of least privilege only

D. Relational normalization

Correct Answer: A. Open–Closed Principle

Question 10

Why should UML metrics and diagrams be interpreted carefully?

A. They are unrelated to software design

B. Metric definitions vary and diagrams can become obsolete

C. They can only be used with Python

D. They automatically guarantee secure software

Correct Answer: B. Metric definitions vary and diagrams can become obsolete

Object-Oriented Programming (Java/Python)
Chapter 9 of 15