Programming Fundamentals and Object-Oriented Programming

Chapter 10
Design Patterns, Refactoring and Code Quality

Post Graduate Diploma in Full Stack Development
Programming Fundamentals and Object-Oriented Programming   •   Post Graduate

10. Design Patterns, Refactoring and Code Quality

Introduction and Context

Software systems rarely become difficult because of one complicated function. They become difficult when responsibilities are mixed, dependencies are hidden, changes require editing unrelated modules, and temporary solutions become permanent architecture. In a full-stack application, a payment service may contain database queries, HTTP calls, validation, logging, and business rules in one class. Such code may work initially, but it becomes expensive to test, extend, and operate.

Design patterns and refactoring provide two complementary responses to this problem. A design pattern is a reusable design idea for a recurring problem. Refactoring is the disciplined improvement of internal code structure without changing its externally observable behaviour. Code quality is the broader goal: software should be understandable, testable, maintainable, reliable, secure, and sufficiently efficient for its context.

Patterns are not ready-made pieces of code and should not be applied mechanically. They are design vocabulary and decision-making tools. Refactoring is not cosmetic rewriting; it is controlled change supported by tests, version control, and incremental verification. Together, these practices help developers manage complexity without introducing unnecessary abstraction.

Learning Objectives

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

  • explain the purpose and limitations of design patterns;
  • distinguish creational, structural, and behavioural patterns;
  • apply Factory, Adapter, Strategy, Observer, and Repository patterns appropriately;
  • identify common code smells and select suitable refactorings;
  • use tests and version control to refactor safely;
  • evaluate trade-offs involving coupling, cohesion, complexity, performance, and maintainability;
  • assess code-quality practices in professional and organizational settings.

Design Patterns as Design Knowledge

A pattern describes a recurring context, a problem within that context, and a proven family of solutions. It normally includes the participating objects, their responsibilities, relationships, consequences, and situations in which the solution is inappropriate. The value of a pattern lies less in its name than in the design reasoning it communicates.

Patterns are commonly grouped into three families. Creational patterns address object creation. Structural patterns organize relationships between classes or objects. Behavioural patterns coordinate responsibilities, communication, and algorithms. These categories overlap in real systems, but they offer a useful learning and analysis framework.

A pattern should be selected only after the problem is understood. For example, introducing a Strategy hierarchy when there is only one stable algorithm adds indirection without benefit. Conversely, allowing a controller to construct vendor-specific payment clients directly can make future replacement and testing unnecessarily difficult. Good design balances simplicity with anticipated change.

The Factory Pattern

The Factory pattern centralizes or encapsulates object creation. Instead of allowing client code to know the concrete class to instantiate, a factory selects and returns an appropriate implementation.

```java

interface NotificationSender {

void send(String recipient, String message);

}

class NotificationFactory {

static NotificationSender create(String channel) {

if (channel.equals("email")) return new EmailSender();

if (channel.equals("sms")) return new SmsSender();

throw new IllegalArgumentException("Unsupported channel");

}

}

```

A client depends on `NotificationSender`, not on every concrete sender. This supports substitution and localizes creation rules. Factory Method delegates creation to subclasses, whereas a simple or static factory commonly uses a method containing selection logic. A factory does not automatically eliminate all conditionals; it is useful when construction is complex, variable, repeated, or likely to change.

Its costs include an additional abstraction and the possibility that a large factory becomes a new maintenance problem. Factories should therefore remain cohesive and should not conceal configuration errors or lifecycle responsibilities.

The Adapter Pattern

An Adapter converts one interface into another expected by a client. It is particularly valuable when integrating legacy code, third-party libraries, or external APIs that cannot be modified.

```java

interface PaymentGateway {

PaymentResult charge(Money amount, Customer customer);

}

class LegacyGatewayAdapter implements PaymentGateway {

private final LegacyGateway gateway;

public PaymentResult charge(Money amount, Customer customer) {

LegacyResponse response = gateway.makePayment(

amount.value(), customer.email());

return translate(response);

}

}

```

The application uses the stable `PaymentGateway` interface while the adapter translates calls, data formats, exceptions, and sometimes error semantics. The adapter should not become a place for unrelated business rules. Authentication, retries, and observability may be coordinated nearby, but the adapter's central responsibility is translation.

Adapters reduce the blast radius of vendor change. They also create a clear boundary for testing and protect the domain model from external terminology. Their limitation is that translation can be lossy or ambiguous; a successful technical response may not mean a successful business transaction.

The Strategy Pattern

Strategy encapsulates interchangeable algorithms behind a common interface. It is appropriate when an application must select behaviour at runtime or when alternative algorithms evolve independently.

```java

interface ShippingStrategy {

Money calculate(Order order);

}

class CheckoutService {

private final ShippingStrategy shipping;

CheckoutService(ShippingStrategy shipping) {

this.shipping = shipping;

}

Money total(Order order) {

return order.subtotal().add(shipping.calculate(order));

}

}

```

Examples include tax calculation, pricing rules, authentication mechanisms, compression, and route selection. Strategy replaces extensive conditional logic with polymorphism and makes individual algorithms easier to test. However, it may be excessive when the alternatives are few, stable, and clearer in a small conditional expression. Strategies also require a reliable selection mechanism and consistent contracts regarding null values, errors, performance, and side effects.

The Observer Pattern

Observer establishes a one-to-many dependency: when a subject changes state or emits an event, registered observers are notified. In web systems, observers may support audit logging, cache invalidation, metrics, email notifications, or user-interface updates.

A modern implementation often uses an event publisher and subscribers. The publisher should expose a meaningful event, such as `OrderPaid`, rather than its entire internal state. This reduces coupling between the producer and consumers.

Observer introduces important operational concerns. Synchronous observers increase the time and failure surface of the initiating operation. Asynchronous observers improve isolation but introduce queues, retries, duplicate delivery, ordering issues, and eventual consistency. Subscribers should therefore be idempotent where possible, and events should contain adequate identifiers and version information.

The Repository Pattern

A Repository provides a domain-oriented interface for accessing aggregates or entities while hiding persistence details. For example:

```java

interface OrderRepository {

Optional<Order> findById(OrderId id);

void save(Order order);

}

```

The domain or application layer can work with this interface while an infrastructure implementation uses SQL, a document database, or an external service. Repository abstractions improve testing and protect business code from persistence-specific queries.

A repository is not merely a generic wrapper around every database operation. It should express meaningful domain operations and preserve transaction and consistency expectations. A repository that returns partially populated objects, leaks ORM sessions, or creates hundreds of arbitrary methods may increase rather than reduce complexity. For simple applications, direct data access may be more transparent.

Connecting Patterns to Design Principles

Patterns are more effective when interpreted through design principles. High cohesion means that a module has a focused purpose. Low coupling means that modules have limited knowledge of one another. Encapsulation protects decisions likely to change. The Single Responsibility Principle concerns one reason for change, not necessarily one method or one line of code. The Open/Closed Principle encourages extension through stable abstractions, but it does not justify predicting every future requirement.

Dependency inversion places stable, high-level policy above volatile implementation details. Dependency injection is a practical technique for achieving this: dependencies are supplied from outside rather than constructed inside the class. Interface segregation discourages forcing clients to depend on methods they do not use. These principles are heuristics, not laws. Excessive interfaces, layers, and indirection can make a small system harder to understand.

A useful design review asks: What is likely to change? Which module should own that decision? Can the behaviour be tested in isolation? What failure modes are introduced? Is the abstraction paying for itself?

Refactoring: Improving Structure Without Changing Behaviour

Refactoring is a sequence of small, behaviour-preserving transformations. The distinction between refactoring and rewriting is important. A rewrite often changes structure and behaviour simultaneously, making defects difficult to locate. Refactoring separates these risks.

A safe workflow is:

1. establish the current behaviour through automated tests and, where necessary, characterization tests;

2. identify one design problem and define the intended improvement;

3. make a small transformation;

4. run focused tests, static analysis, and formatting checks;

5. review the diff and commit a coherent change;

6. repeat while monitoring behaviour and performance.

The edit-test cycle is strengthened by continuous integration, code review, feature flags, and observability. Tests should cover business outcomes rather than internal implementation details, otherwise a sound refactoring may require needless test rewriting.

Common Code Smells and Responses

A code smell is a symptom suggesting a possible design problem, not proof of one. A long method may indicate missing abstractions, but it may also be a clear procedure. A large class may have several responsibilities and benefit from Extract Class. Duplicated code may be removed through Extract Method or a shared component, provided the duplicated sections represent the same concept. Accidental similarity should not be forced into a common abstraction.

Long parameter lists often indicate a missing value object or a cohesive parameter object. Primitive obsession occurs when strings or numbers represent concepts such as email addresses, money, or identifiers without validation or behaviour. Replacing primitives with domain types can improve correctness, but excessive wrapping can reduce readability.

Feature envy occurs when a method uses another object's data more than its own; Move Method may restore responsibility. Switch statements repeated across the system may suggest polymorphism or Strategy. Shotgun surgery occurs when one conceptual change requires edits in many files; centralizing a volatile decision can help. Divergent change occurs when one class is changed for unrelated reasons; Extract Class can improve cohesion.

Dead code, speculative generality, comments that explain confusing code rather than clarify intent, and deeply nested conditionals are other common warning signs. Refactoring should address the underlying design issue, not merely rename variables or reorganize files.

A Refactoring Example

Suppose a checkout method validates an order, calculates tax, selects shipping, charges a gateway, saves an order, and sends email. Its length is not the only problem: it combines policy, integration, persistence, and notification. A safe sequence might extract validation, introduce a `ShippingStrategy`, inject a `PaymentGateway`, move persistence behind an `OrderRepository`, and publish an `OrderPaid` event.

Each step should preserve the externally visible checkout contract. The final design is not automatically superior merely because it contains more classes. It is superior if responsibilities are clearer, volatile dependencies are isolated, and the system can be tested and changed with less risk.

Measuring and Governing Code Quality

Quality is multidimensional. Readability concerns whether a competent developer can understand intent. Maintainability concerns the cost and risk of change. Testability concerns the ability to verify behaviour. Reliability concerns consistent operation under expected and unexpected conditions. Security includes protection against misuse and unauthorized access. Performance concerns response time, throughput, and resource consumption.

Useful indicators include cyclomatic complexity, duplication, dependency direction, defect trends, test coverage, build duration, and change-failure rate. Metrics are signals rather than targets. High coverage can coexist with weak assertions, and low complexity does not guarantee sound domain modelling. Teams should combine automated measures with code review, production telemetry, and user impact.

Quality governance includes coding standards, architectural decision records, dependency review, secure review practices, and a consciously managed technical-debt backlog. Technical debt is not always bad; it is a trade-off that should be visible, owned, and repaid when its interest becomes material.

Applied Case: Modernising an Order Platform

Background

This illustrative case concerns a growing online retailer whose monolithic order application originally served a small product range. Over time, it added multiple payment providers, regional tax rules, warehouse integrations, email notifications, and promotional pricing.

Situation and Problem

The `OrderService` had become a 1,200-line class. It constructed SDK clients directly, contained nested conditionals for tax and shipping, wrote SQL queries, and sent email before the transaction completed. A payment-provider change required edits in six methods. Tests were slow because they used real databases and external services.

Analysis

The principal problems were high coupling, low cohesion, hidden dependencies, and unclear transaction boundaries. The team also found duplicated tax logic and notification failures that caused checkout requests to fail after payment had succeeded. A complete rewrite was considered too risky because the existing service processed valuable transactions.

Approach and Solution

The team first added characterization tests around successful checkout, payment rejection, tax calculation, and persistence failures. It introduced dependency injection for the payment client and placed a `PaymentGateway` interface around the vendor SDK using an Adapter. Vendor selection moved to a Factory configured by region.

Tax and shipping calculations were extracted as Strategies. SQL access was moved behind an `OrderRepository`, while transaction management remained explicit in the application service. After payment and persistence succeeded, the service published an `OrderPaid` event. Email became an idempotent subscriber with retry handling. Each change was delivered in small pull requests with automated tests and production monitoring.

Outcome

The application retained its external behaviour while provider-specific code became localized. Unit tests could exercise pricing and checkout policy without network access. Notification failure no longer falsely reported checkout failure, although the business accepted that email delivery was eventually consistent. The team did not create a general-purpose framework; it introduced only the abstractions required by observed change.

Learning Points

The case demonstrates that patterns should follow identified seams of change. It also shows why asynchronous Observer designs require explicit reliability decisions. Most importantly, safe refactoring depends on tests, incremental commits, clear boundaries, and communication among developers, operations staff, and business stakeholders.

Risks, Misuse, and Professional Considerations

Pattern overuse is a major risk. A project can become an architecture of factories, interfaces, managers, and wrappers that obscures simple behaviour. Another risk is abstraction leakage: an ostensibly clean repository may expose database pagination, ORM entities, or vendor exceptions throughout the domain. Refactoring can also introduce semantic changes, especially around time zones, rounding, concurrency, transactions, and error handling.

Developers must preserve security properties while changing code. Logging refactoring should not expose credentials or personal data. Observers and event buses must enforce authorization boundaries and avoid publishing sensitive information unnecessarily. Repository changes should preserve access controls, audit requirements, retention rules, and transaction guarantees. Third-party adapters require attention to licensing, supply-chain security, data residency, and contractual obligations.

AI-assisted coding and automated refactoring tools can accelerate mechanical transformations, generate tests, and identify duplication. They cannot reliably determine business invariants, acceptable architectural complexity, or whether an event should be public. Generated changes require human review, test execution, dependency inspection, and protection of confidential source code and data.

Current Developments and Best Practices

Contemporary systems increasingly combine modular monoliths, microservices, event-driven integration, and managed cloud services. Patterns such as ports and adapters, dependency injection, and repository boundaries remain useful across these forms, but distributed systems add latency, retries, partial failure, and consistency concerns. Domain events and asynchronous processing should be introduced for a real integration or scalability need, not simply because they are fashionable.

Effective practice includes:

  • prefer the simplest design that supports known requirements;
  • name abstractions after domain concepts and stable responsibilities;
  • keep patterns local rather than imposing them across every module;
  • automate formatting, testing, static analysis, dependency scanning, and continuous integration;
  • refactor in small, reversible steps;
  • test contracts at integration boundaries;
  • make failure, retry, idempotency, and transaction behaviour explicit;
  • review changes for security, accessibility, privacy, and operational impact;
  • record significant architectural decisions and revisit them when assumptions change.

Chapter Summary

Design patterns capture reusable solutions to recurring design problems, but they are not templates to apply automatically. Factory encapsulates variable creation, Adapter translates incompatible interfaces, Strategy represents interchangeable algorithms, Observer coordinates one-to-many notification, and Repository separates domain-oriented persistence from infrastructure. Their value depends on cohesion, coupling, dependency direction, and the likelihood of change.

Refactoring improves internal structure while preserving observable behaviour. A disciplined process uses tests, small transformations, code review, version control, and continuous integration. Code smells such as long methods, duplication, feature envy, primitive obsession, and shotgun surgery guide investigation but do not dictate a solution. Code quality includes maintainability, reliability, security, testability, and operational fitness. Professional practice balances these goals against simplicity, cost, risk, and actual requirements.

Key Terms

Pattern: A documented, reusable solution structure for a recurring design problem.

Creational pattern: A pattern concerned with creating and configuring objects.

Structural pattern: A pattern concerned with composing classes or objects.

Behavioural pattern: A pattern concerned with communication and responsibility allocation.

Factory: An abstraction that centralizes or encapsulates object creation.

Adapter: An object that translates one interface into another expected interface.

Strategy: An encapsulated, interchangeable algorithm or policy.

Observer: A one-to-many notification relationship between a subject and subscribers.

Repository: A domain-oriented abstraction over persistence or data access.

Refactoring: Behaviour-preserving improvement of internal code structure.

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

Coupling: The degree of dependency between software components.

Cohesion: The degree to which a component's responsibilities belong together.

Dependency injection: Supplying a component's dependencies from outside the component.

Technical debt: The future cost created by expedient design or implementation choices.

Characterization test: A test that records existing behaviour before change.

Idempotency: The property that repeating an operation has the same intended effect as performing it once.

Eventual consistency: A model in which replicas or consumers may temporarily differ but converge later.

Cyclomatic complexity: A structural measure related to the number of independent control-flow paths.

Review Questions

Short Answer Questions

1. What problem does the Adapter pattern solve?

2. Distinguish a design pattern from a code library.

3. Why are characterization tests useful before refactoring legacy code?

4. Define cohesion and explain why it matters.

5. What is meant by idempotent event handling?

Long Answer Questions

1. Explain the Factory, Strategy, and Repository patterns, including suitable applications and limitations.

2. Describe a disciplined refactoring workflow and explain the role of automated tests and version control.

3. Discuss how Observer can create reliability and consistency challenges in an application.

4. Explain several code smells and match each with a possible refactoring response.

5. Evaluate the claim that more abstraction always produces better software design.

Analytical / Application Questions

1. A payment service directly constructs three vendor SDK clients and contains vendor-specific error handling in its controller. Analyse the design and propose a pattern-based improvement.

2. An order application sends confirmation email inside the database transaction. The email provider is slow and occasionally unavailable. Design a safer solution and identify its consistency and retry implications.

3. A team proposes introducing factories, repositories, event buses, and six interfaces into a small stable CRUD application. Assess the proposal using coupling, cohesion, simplicity, and anticipated change as criteria.

22. Multiple Choice Questions with Answers

Question 1

A checkout application must support multiple shipping algorithms selected according to customer region. Which design most directly represents this variability?

A. Strategy

B. Adapter

C. Repository

D. Observer

Correct Answer: A. Strategy

Question 2

What is the primary responsibility of an Adapter?

A. Persist domain aggregates

B. Translate one interface into another expected by a client

C. Notify all subscribers of a state change

D. Select a database transaction boundary

Correct Answer: B. Translate one interface into another expected by a client

Question 3

Which situation best justifies introducing a Factory?

A. A single immutable value has one obvious constructor

B. Object creation varies by configuration and includes vendor-specific setup

C. A method contains no conditional logic

D. A class requires stronger database indexing

Correct Answer: B. Object creation varies by configuration and includes vendor-specific setup

Question 4

Why should refactoring normally be performed in small steps?

A. Small steps eliminate the need for tests

B. They make behavioural changes easier to isolate and reverse

C. They guarantee better runtime performance

D. They prevent all design disagreements

Correct Answer: B. They make behavioural changes easier to isolate and reverse

Question 5

A method reads more data from another object than from the object that owns the method. Which smell is most closely indicated?

A. Feature envy

B. Dead code

C. Primitive obsession

D. Speculative generality

Correct Answer: A. Feature envy

Question 6

In an event-driven order system, why should an email subscriber often be idempotent?

A. Events are always delivered exactly once

B. Retries or duplicate delivery should not send unintended repeated effects

C. Idempotency removes the need for authentication

D. It guarantees immediate consistency

Correct Answer: B. Retries or duplicate delivery should not send unintended repeated effects

Question 7

Which statement best describes a Repository?

A. It must expose every SQL statement to the domain layer

B. It provides a domain-oriented interface while hiding persistence details

C. It is always required for every small application

D. It converts incompatible third-party interfaces

Correct Answer: B. It provides a domain-oriented interface while hiding persistence details

Question 8

Which is the most appropriate first step before a risky refactoring of poorly documented legacy behaviour?

A. Delete the existing tests

B. Add characterization tests for important existing behaviour

C. Replace the entire system immediately

D. Introduce as many abstractions as possible

Correct Answer: B. Add characterization tests for important existing behaviour

Question 9

Which observation is a limitation of code-coverage metrics?

A. Coverage can never be automated

B. High coverage does not necessarily mean assertions test meaningful behaviour

C. Coverage measures database throughput directly

D. Coverage prevents all security defects

Correct Answer: B. High coverage does not necessarily mean assertions test meaningful behaviour

Question 10

A team adds a complex pattern hierarchy to a small application even though its requirements and algorithms are stable. What is the main design risk?

A. Excessive simplicity

B. Unnecessary abstraction and indirection

C. Automatic data loss

D. Guaranteed lower cohesion in every module

Correct Answer: B. Unnecessary abstraction and indirection

Programming Fundamentals and Object-Oriented Programming
Chapter 10 of 15