Object-Oriented Programming

Chapter 10
Design Principles, Refactoring, and Code Quality

Bachelors Program in AI & ML
Object-Oriented Programming   •   Bachelor

Design Principles, Refactoring, and Code Quality

Introduction

Object-oriented programming is not only a way to organize data and behavior. It is also a discipline for managing change. A program may work correctly when first released and still become expensive, fragile, and difficult to extend as requirements evolve. Design principles and refactoring techniques help developers preserve the quality of a codebase while adding features, fixing defects, and responding to new technologies.

This chapter examines the principles commonly associated with maintainable object-oriented software: SOLID, DRY, KISS, the Law of Demeter, high cohesion, and low coupling. It also explains code smells, safe refactoring, complexity, technical debt, and practical quality assessment. These principles are guidelines rather than absolute laws. Good design requires judgment because improving one quality attribute can sometimes harm another, such as performance, simplicity, or delivery speed.

The examples use Java-like pseudocode. The ideas apply equally to Python, C++, C#, Kotlin, and other object-oriented languages.

Learning Objectives

After studying this chapter, students should be able to:

1. Explain the purpose and limitations of major object-oriented design principles.

2. Apply SOLID, DRY, KISS, the Law of Demeter, high cohesion, and low coupling.

3. Recognize common code smells and infer the design problems they indicate.

4. Refactor code safely using tests and small, reversible changes.

5. Discuss complexity, maintainability, technical debt, and design trade-offs.

6. Evaluate design alternatives in a realistic software engineering case.

1. Code Quality as a Design Concern

Code quality is multidimensional. Correctness means that software satisfies its specified behavior. Other important qualities include:

  • Maintainability: the effort required to understand, modify, test, and extend software.
  • Reliability: the ability to perform consistently under stated conditions.
  • Readability: the ease with which developers can understand code.
  • Testability: the ease of isolating and verifying behavior.
  • Performance: the responsiveness and resource efficiency of the system.
  • Security: resistance to unauthorized access, misuse, and data loss.
  • Scalability: the ability to handle increasing workload without unacceptable degradation.

No single metric completely measures quality. A short program may be difficult to test; a highly abstract design may be flexible but unnecessarily complicated. Quality is therefore best treated as fitness for purpose, assessed against requirements, risks, and expected change.

A useful distinction is between external quality and internal quality. External quality is visible to users, for example response time or functional correctness. Internal quality concerns structure, dependencies, naming, duplication, and testability. Good internal quality reduces the cost of preserving external quality over time.

2. Foundational Design Principles

2.1 High Cohesion

Cohesion describes how strongly the responsibilities of a module belong together. A highly cohesive class has a focused purpose. For example, a `PayrollCalculator` that calculates salaries, formats payslips, sends email, and writes database records has low cohesion because its responsibilities belong to several different concerns.

High cohesion improves comprehension, testing, and reuse. It does not mean that every class must contain only one method. It means that the methods and data in a class should contribute to a coherent abstraction.

A practical question is: “Can this class be described accurately using one clear responsibility statement?” If the answer requires several unrelated verbs, the class may need decomposition.

2.2 Low Coupling

Coupling is the degree of dependence between modules. Low coupling means that a change in one component is less likely to require changes in many others. Interfaces, dependency injection, encapsulation, and event-based communication can reduce coupling.

Consider:

```java

class OrderService {

private MySqlOrderRepository repository = new MySqlOrderRepository();

}

```

The service is coupled directly to a concrete database implementation. A more flexible design is:

```java

class OrderService {

private OrderRepository repository;

OrderService(OrderRepository repository) {

this.repository = repository;

}

}

```

The service depends on an abstraction and can be tested with an in-memory or mock repository. Low coupling does not mean eliminating all dependencies. Dependencies are necessary; the goal is to make them explicit, stable, and appropriate.

2.3 DRY: Do Not Repeat Yourself

DRY states that each piece of knowledge should have one authoritative representation. It is often misunderstood as “never write similar lines twice.” Two pieces of code may look similar but represent different business rules. Prematurely combining them can create an abstraction that is difficult to understand.

Duplication has several forms:

  • Syntactic duplication: repeated statements or algorithms.
  • Semantic duplication: the same business rule expressed in different places.
  • Process duplication: repeated manual procedures.

Semantic duplication is usually the most dangerous. If tax calculation appears in three services, a policy change may update only two of them. However, abstraction should be introduced when the shared concept is understood. A useful rule is to tolerate small duplication temporarily rather than create a misleading generalization.

2.4 KISS: Keep It Simple

KISS encourages the simplest design that adequately satisfies current requirements. Simplicity reduces cognitive load, defect opportunities, and maintenance cost. It does not mean ignoring future needs; it means avoiding speculative complexity unsupported by evidence.

Examples of unnecessary complexity include an elaborate inheritance hierarchy for two stable variations, a configuration framework for a single constant, or several design patterns applied without a problem to solve. Simple code should still be robust, secure, and well-tested.

2.5 The Law of Demeter

The Law of Demeter, or principle of least knowledge, advises that an object should communicate primarily with close collaborators rather than navigating through long chains of objects. Code such as:

```java

customer.getAccount().getAddress().getCountry().getTaxRate()

```

exposes the internal structure of several objects. A change in any link can break the caller. A better design may place the operation where the relevant knowledge belongs:

```java

customer.taxRate()

```

The Law of Demeter is not a ban on every chained call. Fluent interfaces and immutable value objects may use chains safely. The concern is inappropriate knowledge of object structure, especially when a caller retrieves data merely to perform another object’s responsibility.

3. SOLID Principles

SOLID is a group of five design principles. They are most useful when applied as questions about change and responsibility rather than as rigid rules.

3.1 Single Responsibility Principle

The Single Responsibility Principle (SRP) states that a class should have one reason to change. A “responsibility” is a reason for change arising from a coherent stakeholder or concern, not necessarily one small task.

For example:

```java

class Invoice {

void calculateTotal() { }

void saveToDatabase() { }

void print() { }

}

```

Calculation, persistence, and presentation may change for unrelated reasons. A design can separate these concerns into `Invoice`, `InvoiceRepository`, and `InvoicePrinter`.

SRP does not require creating a class for every method. Excessive fragmentation can increase navigation and coupling. The appropriate boundary depends on domain concepts and likely changes.

3.2 Open–Closed Principle

The Open–Closed Principle (OCP) says that software entities should be open for extension but closed for modification. Existing, tested behavior should not require repeated alteration whenever a new variation is introduced.

A conditional design might be:

```java

if (paymentType.equals("CARD")) { ... }

else if (paymentType.equals("CASH")) { ... }

```

If payment types are expected to grow, polymorphism can isolate variation:

```java

interface PaymentMethod {

Receipt pay(Money amount);

}

class CardPayment implements PaymentMethod { ... }

class CashPayment implements PaymentMethod { ... }

```

OCP is achieved by identifying a stable abstraction and placing likely variation behind it. It is not a justification for interfaces everywhere. Abstraction has a cost and should be based on evidence of variation.

3.3 Liskov Substitution Principle

The Liskov Substitution Principle (LSP) states that objects of a subtype should be usable wherever objects of the supertype are expected without violating the client’s expectations.

Suppose `Bird` has a `fly()` method and `Penguin` inherits from `Bird` but cannot fly. The hierarchy expresses an incorrect abstraction. A better model may separate `Bird` from a `FlyingBird` capability.

LSP involves more than matching method signatures. A subtype should respect behavioral contracts, including valid inputs, promised outputs, side effects, and error behavior. A subtype that strengthens preconditions or weakens guarantees can violate substitutability.

3.4 Interface Segregation Principle

The Interface Segregation Principle (ISP) recommends that clients should not be forced to depend on methods they do not use. A large interface such as:

```java

interface Worker {

void work();

void eat();

void submitReport();

}

```

may be inappropriate for an automated process that can work but cannot eat. Smaller role-specific interfaces reduce unnecessary coupling:

```java

interface Workable { void work(); }

interface Reportable { void submitReport(); }

```

The principle is about client needs, not about making every interface tiny. Over-segmentation can make the design harder to discover.

3.5 Dependency Inversion Principle

The Dependency Inversion Principle (DIP) has two related ideas:

1. High-level policy should not depend directly on low-level details; both should depend on abstractions.

2. Abstractions should not depend on details; details should depend on abstractions.

Dependency injection is a common implementation technique. A recommendation engine may depend on a `ProductRepository` interface rather than constructing a particular database adapter. This supports testing and replacement of infrastructure.

DIP does not mean every class must depend on an interface. An abstraction is valuable when it represents a meaningful boundary, such as a payment gateway, clock, file system, or external service.

4. Code Smells

A code smell is a surface indication of a possible design problem. It is not necessarily a defect. Developers must investigate the context before changing code.

Common smells include:

  • Long method: a method performs too many steps or mixes levels of abstraction.
  • Large class or God object: one class controls many unrelated responsibilities.
  • Feature envy: a method uses another class’s data more than its own.
  • Shotgun surgery: one conceptual change requires edits across many files.
  • Divergent change: one class changes for many unrelated reasons.
  • Duplicate code: repeated logic risks inconsistent updates.
  • Long parameter list: a method requires too many inputs, often indicating a missing object.
  • Primitive obsession: domain concepts such as money, email, or coordinates are represented only by primitive values.
  • Switch or conditional complexity: repeated branching on type suggests missing polymorphism.
  • Temporary field: a field is meaningful only in certain execution paths.
  • Speculative generality: abstractions exist for hypothetical future needs.
  • Comments explaining confusing code: comments may be useful, but persistent confusion often indicates poor naming or structure.

Smells should be prioritized by risk. A duplicated line in stable code may be harmless, whereas duplicated authorization logic is dangerous. Static analysis tools can identify patterns, but human review is required to understand intent.

5. Refactoring Safely

Refactoring is the disciplined restructuring of existing code without changing its externally observable behavior. It differs from rewriting because behavior is preserved through incremental transformations.

5.1 Preconditions for Refactoring

Before refactoring important behavior, establish a safety net:

1. Identify the behavior and affected clients.

2. Run existing automated tests.

3. Add characterization tests when behavior is poorly documented.

4. Ensure version control provides small, reversible commits.

5. Define a narrow goal, such as “extract invoice persistence.”

A characterization test records what the system currently does. It is not necessarily a test of ideal behavior, but it helps prevent accidental changes during restructuring.

5.2 A Safe Refactoring Cycle

A practical cycle is:

1. Inspect: locate duplication, coupling, unclear responsibilities, or excessive complexity.

2. Test: confirm a passing baseline.

3. Change one structural aspect: for example, rename a method or extract a class.

4. Run tests and static checks.

5. Review the resulting design.

6. Commit the change.

7. Repeat only if the design remains understandable.

Typical refactorings include Extract Method, Extract Class, Rename, Move Method, Introduce Parameter Object, Replace Conditional with Polymorphism, Encapsulate Field, and Replace Magic Number with Named Constant.

Refactoring should be separated from unrelated feature changes where possible. Mixing a large redesign with new behavior makes review and defect diagnosis difficult.

5.3 Example: Extracting a Responsibility

Before:

```java

class OrderService {

void place(Order order) {

double total = 0;

for (Line line : order.lines()) {

total += line.price() * line.quantity();

}

database.save(order, total);

email.send(order.customerEmail(), "Order confirmed");

}

}

```

The method calculates, persists, and communicates. A first step is to extract calculation into the domain object:

```java

class Order {

Money total() {

Money result = Money.zero();

for (Line line : lines) {

result = result.add(line.subtotal());

}

return result;

}

}

class OrderService {

void place(Order order) {

database.save(order, order.total());

email.send(order.customerEmail(), "Order confirmed");

}

}

```

The refactoring improves cohesion and expresses a domain operation. Further separation of persistence and notification may be appropriate, but it should be performed incrementally.

6. Complexity, Maintainability, and Technical Debt

6.1 Complexity

Complexity describes the difficulty of understanding or executing software. Cyclomatic complexity approximates the number of linearly independent paths through a method. For a control-flow graph, a common formula is:

\[

M = E - N + 2P

\]

where \(M\) is cyclomatic complexity, \(E\) is the number of control-flow edges, \(N\) is the number of nodes, and \(P\) is the number of connected components. For one connected method, it is often simplified to the number of decision points plus one. This is a structural indicator, not a direct measure of defects or maintainability.

For example, a method with three independent `if` decisions has an approximate cyclomatic complexity of 4. This suggests at least four independent paths for basis-path testing, although practical test design may use more or fewer cases depending on conditions and domain constraints.

Other useful measures include lines of code, nesting depth, coupling between modules, duplication percentage, code churn, defect density, and test coverage. Each has limitations. High test coverage does not guarantee meaningful assertions, and low lines of code does not guarantee clarity.

6.2 Maintainability

Maintainability depends on design, documentation, tests, developer familiarity, and system volatility. A maintainability assessment should ask:

  • How many modules must change for a typical feature?
  • Can a developer locate the relevant behavior quickly?
  • Can components be tested in isolation?
  • Are failures localized or widespread?
  • Are domain rules expressed once and clearly?
  • Does the architecture support expected deployment and security needs?

A useful engineering practice is to combine automated metrics with code review and maintenance observations rather than using a single threshold as a quality verdict.

6.3 Technical Debt

Technical debt is the future cost created when a design or implementation shortcut makes later change more expensive. Some debt is deliberate, such as releasing a limited version to test a market assumption. Some is accidental, resulting from inadequate understanding, rushed work, or neglected maintenance.

Debt is not simply “bad code.” A short-term shortcut can be rational if its cost is known, contained, and repaid. Debt becomes dangerous when it is hidden, grows through dependencies, or affects safety, security, and regulatory compliance.

Debt management includes recording known compromises, estimating their impact, prioritizing high-risk items, and allocating capacity for repayment. Refactoring should target debt that obstructs valuable change or creates significant operational risk.

7. A Detailed Applied Case: Refactoring an Online Learning Platform

The following is an illustrative case.

Background

A university’s online learning platform provides course enrollment, payment, assessment, notifications, and reporting. Its original `CourseManager` class contains approximately 2,000 lines. It directly accesses a relational database, sends email, calculates discounts, validates enrollment, and formats reports.

Problem

A new requirement introduces corporate-sponsored students and a mobile application. Developers report that apparently minor changes cause regressions. Unit tests are slow because most tests require a database and email server. A discount change must be copied into enrollment, invoices, and reports.

Analysis

Several smells are present:

  • God object and low cohesion in `CourseManager`.
  • High coupling to database and email implementations.
  • Duplicate discount rules, violating DRY.
  • Long conditional chains for student type, suggesting a variation point.
  • Feature envy in reporting methods that navigate deeply through enrollment objects.
  • High cyclomatic complexity in enrollment validation.

The team maps responsibilities and identifies likely change axes: enrollment policy, pricing, persistence, notification, and presentation. It also measures change frequency and defect history rather than refactoring solely because the class is large.

Approach and Solution

The team first adds characterization tests around enrollment and pricing. It then performs small refactorings:

1. Extracts `PricingPolicy` and moves discount rules into one location.

2. Introduces `EnrollmentRepository` and injects it into an `EnrollmentService`.

3. Replaces student-type conditionals with a `StudentCategory` policy abstraction where behavior genuinely varies.

4. Extracts notification into a `NotificationService`.

5. Replaces deep object navigation with domain operations such as `enrollment.paymentStatus()`.

6. Separates report data preparation from report formatting.

7. Adds focused unit tests using fake repositories and notification gateways.

The team deliberately avoids creating abstractions for stable, trivial code. It also retains a modular monolith rather than moving immediately to microservices. This applies KISS: the architectural boundary is improved without adding distributed-system complexity.

Outcome

New pricing rules can be implemented in one policy module. Enrollment tests run without infrastructure and provide faster feedback. Changes to email delivery do not affect enrollment logic. The codebase still contains technical debt, but it is documented and concentrated at explicit boundaries.

The outcome is not measured only by fewer lines of code. More important improvements include reduced change dispersion, clearer responsibilities, faster tests, and more localized failures. Some additional interfaces and objects increase the number of files, demonstrating that maintainability is not equivalent to minimal file count.

Learning Points

The case illustrates that principles work together. SRP helps separate responsibilities; DIP makes infrastructure replaceable; DRY centralizes business knowledge; KISS prevents overengineering; the Law of Demeter reduces structural knowledge; and high cohesion with low coupling improves testability. The sequence of small, tested changes reduces refactoring risk.

8. Trade-offs and Limitations

Design principles can conflict. Applying OCP too aggressively may produce abstractions for variations that never occur. Strict DRY may merge concepts that later evolve differently. Highly decoupled designs may require more interfaces and indirection. Encapsulation can make a simple value harder to access. Additional validation and defensive checks may reduce performance, although they may be necessary for safety and security.

Inheritance is not inherently bad, but composition is often safer when behavior varies independently. Design patterns are not goals; they are named solutions to recurring problems. A pattern should be justified by a real force such as replaceable algorithms, event decoupling, or object construction complexity.

There are also organizational constraints. Teams may lack time for broad refactoring, legacy systems may have weak tests, and external APIs may impose awkward designs. In safety-critical, medical, financial, or educational systems, maintainability decisions must also consider auditability, privacy, accessibility, security, and legal obligations. Refactoring must preserve authorization, personal-data handling, logging requirements, and evidence needed for compliance.

9. Contemporary Practice and AI-Assisted Development

Modern integrated development environments provide automated renaming, extraction, dependency analysis, and static checking. Continuous integration can run unit tests, integration tests, linters, security scans, and quality gates on every change. These tools reduce mechanical errors but do not understand all business contracts.

AI coding assistants can suggest refactorings, tests, and explanations. Their output must be reviewed for semantic correctness, security vulnerabilities, licensing concerns, privacy leakage, and inappropriate abstractions. Generated code may reproduce duplication or confidently alter behavior. A responsible workflow treats AI output as a draft: developers define the design intent, inspect the diff, run tests, and validate domain and regulatory requirements.

A contemporary quality process therefore combines human judgment, automated feedback, version control, code review, observability, and explicit technical-debt management.

Best-Practice Checklist

  • Give classes and methods clear, domain-oriented names.
  • Keep responsibilities coherent and dependencies explicit.
  • Depend on abstractions at genuine architectural boundaries.
  • Prefer composition when behavior varies independently.
  • Remove harmful duplication, but do not abstract accidental similarity prematurely.
  • Keep methods small enough to understand and test.
  • Avoid deep knowledge of object internals.
  • Use tests as a safety net before structural change.
  • Refactor in small, reviewable, reversible steps.
  • Combine metrics with human review and maintenance evidence.
  • Record technical debt and prioritize it by risk and business impact.
  • Preserve security, privacy, accessibility, and compliance properties during refactoring.

Chapter Summary

Maintainable object-oriented software is designed for change as well as present correctness. High cohesion keeps related responsibilities together, while low coupling limits the impact of change. DRY reduces duplicated knowledge, KISS controls unnecessary complexity, and the Law of Demeter limits inappropriate structural knowledge. SOLID principles provide a vocabulary for responsibility, extension, substitutability, interfaces, and dependency management.

Code smells are warning signs rather than automatic defects. Refactoring safely requires tests, small transformations, continuous verification, and disciplined version control. Complexity measures such as cyclomatic complexity can guide investigation but cannot replace judgment. Technical debt represents future change cost and should be made visible and managed according to risk. In professional development, code quality is a balance among maintainability, performance, security, delivery constraints, and domain requirements. Tools, including AI assistants, are valuable only when their suggestions are reviewed and validated.

Key Terms

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

Coupling: The degree of dependency between modules.

DRY: The principle that each piece of knowledge should have one authoritative representation.

KISS: The principle of preferring an adequately simple solution over unnecessary complexity.

Law of Demeter: A guideline encouraging objects to communicate with close collaborators rather than navigate object structures.

SOLID: Five object-oriented design principles concerning responsibility, extension, substitutability, interfaces, and dependencies.

Single Responsibility Principle: A class should have one coherent reason to change.

Open–Closed Principle: Entities should support extension without repeated modification of stable behavior.

Liskov Substitution Principle: Subtypes should preserve the behavioral expectations of their supertypes.

Interface Segregation Principle: Clients should not depend on methods they do not use.

Dependency Inversion Principle: High-level policy and low-level details should depend on suitable abstractions.

Code smell: An indication of a possible design or implementation problem.

Refactoring: Restructuring code without changing its externally observable behavior.

Cyclomatic complexity: A control-flow measure approximating independent execution paths.

Maintainability: The ease and cost of understanding, modifying, testing, and extending software.

Technical debt: Future cost created by shortcuts or compromises in software design and implementation.

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

Characterization test: A test that records existing behavior to support safe change.

Review Questions

Short Answer Questions

1. What is the difference between cohesion and coupling?

2. Why is duplication of business knowledge more dangerous than simple textual similarity?

3. State the purpose of the Liskov Substitution Principle.

4. What is a code smell, and why is it not automatically a defect?

5. Why should refactoring normally be performed in small, tested steps?

Long Answer Questions

1. Explain all five SOLID principles and illustrate how they can improve the design of an order-processing system.

2. Discuss how DRY, KISS, and the Law of Demeter influence maintainability. Include situations in which each principle might be misapplied.

3. Explain a complete safe-refactoring workflow, from identifying a problem to verifying and reviewing the result.

4. Compare high cohesion and low coupling with respect to testing, change impact, and team development.

5. Discuss technical debt, including deliberate and accidental debt, and propose a method for managing it in a software project.

Analytical / Application Questions

1. A class validates users, stores them in a database, sends welcome messages, and generates monthly reports. Identify at least three design problems and propose a sequence of safe refactorings.

2. A payment module uses a large conditional statement for card, cash, bank transfer, and cryptocurrency payments. Analyze whether replacing it with polymorphism is justified. Discuss relevant SOLID principles and possible overengineering.

3. A team reports that a small feature requires edits in eight modules and that tests require a live database. Diagnose likely causes using cohesion, coupling, code smells, and technical debt. Recommend measurable improvements.

Multiple Choice Questions with Answers

Question 1

Which property means that the responsibilities of a class strongly belong together?

A. High cohesion

B. Low cohesion

C. High duplication

D. Deep inheritance

Correct Answer: A. High cohesion

Question 2

A service receives a repository through its constructor instead of creating a concrete database repository. Which principle is most directly supported?

A. Liskov Substitution Principle

B. Dependency Inversion Principle

C. Interface Segregation Principle

D. Single Responsibility Principle

Correct Answer: B. Dependency Inversion Principle

Question 3

What does the Open–Closed Principle primarily encourage?

A. Replacing all classes with functions

B. Extending behavior without repeatedly modifying stable code

C. Using inheritance for every variation

D. Allowing clients to access internal fields

Correct Answer: B. Extending behavior without repeatedly modifying stable code

Question 4

Which situation is the clearest example of feature envy?

A. A method uses another class's data and operations more than its own

B. A class has one focused responsibility

C. A method has a descriptive name

D. A repository is injected through an interface

Correct Answer: A. A method uses another class's data and operations more than its own

Question 5

For one connected method with three independent decision points, the approximate cyclomatic complexity is:

A. 1

B. 2

C. 3

D. 4

Correct Answer: D. 4

Question 6

What is the main purpose of a characterization test during legacy-code refactoring?

A. To redesign the user interface

B. To record existing behavior so structural changes can be checked

C. To measure processor speed

D. To replace all integration tests

Correct Answer: B. To record existing behavior so structural changes can be checked

Question 7

Which principle is most directly concerned with preventing clients from depending on unused interface methods?

A. Interface Segregation Principle

B. Open–Closed Principle

C. Single Responsibility Principle

D. Law of Demeter

Correct Answer: A. Interface Segregation Principle

Question 8

Which statement about technical debt is most accurate?

A. All technical debt is accidental and must be removed immediately

B. Technical debt describes future cost created by design or implementation compromises

C. Technical debt is identical to lines of code

D. Technical debt can be measured perfectly by test coverage

Correct Answer: B. Technical debt describes future cost created by design or implementation compromises

Question 9

A chain such as customer.getAccount().getAddress().getCountry() most directly raises concern about which guideline?

A. DRY

B. KISS

C. Law of Demeter

D. Liskov Substitution Principle

Correct Answer: C. Law of Demeter

Question 10

Why should a developer avoid creating an abstraction solely because two code fragments currently look similar?

A. Abstraction always decreases testability

B. Similar code may represent different concepts that will evolve independently

C. Interfaces cannot contain methods

D. Duplicated code is never a maintenance concern

Correct Answer: B. Similar code may represent different concepts that will evolve independently

Object-Oriented Programming
Chapter 10 of 15