Home

Design Patterns

Li

Li Wei

November 3, 202411 min read

Design Patterns

Overview

Software Design Patterns (SDP), also known simply as design patterns, are a catalogued collection of repeatedly used, widely known coding design experiences. They describe recurring problems that arise during software design and the solutions to those problems. In other words, they are a set of proven approaches for solving specific issues—summaries of the coding wisdom of earlier developers—that are sufficiently general to be reused many times.

The essence of design patterns is the practical application of object‑oriented design principles—a deep understanding of class encapsulation, inheritance, polymorphism, as well as class relationships and composition.

Correct use of design patterns brings several benefits:

  • Improves programmers’ thinking, coding, and design skills.
  • Makes software design more standardized and engineering‑oriented, greatly increasing development efficiency and shortening project timelines.
  • Produces code that is highly reusable, readable, reliable, flexible, and maintainable.

Design patterns are divided into three major categories:

  • Creational patterns – describe how to create objects; their main feature is “separating object creation from use.” The GoF (Gang of Four) book lists five creational patterns: Singleton, Prototype, Factory Method, Abstract Factory, Builder.
  • Structural patterns – describe how to assemble classes or objects into larger structures. The GoF book lists seven structural patterns: Proxy, Adapter, Bridge, Decorator, Facade, Flyweight, Composite.
  • Behavioral patterns – describe how classes or objects collaborate to accomplish tasks that no single object can handle alone, and how responsibilities are distributed. The GoF book lists eleven behavioral patterns: Template Method, Strategy, Command, Chain of Responsibility, State, Observer, Mediator, Iterator, Visitor, Memento, Interpreter.

UML

Unified Modeling Language (UML) is a visual modeling language for designing software. Its characteristics are simplicity, uniformity, graphical notation, and the ability to express both dynamic and static aspects of a design.

UML defines nine kinds of diagrams from different viewpoints of the target system: use‑case diagram, class diagram, object diagram, state diagram, activity diagram, sequence diagram, collaboration diagram, component diagram, deployment diagram.

Class Diagram Overview

A class diagram shows the static structure of a model—its classes, the internal structure of each class, and the relationships among classes. It does not display transient information. Class diagrams are a core component of object‑oriented modeling.

Purpose

  • In software engineering, a class diagram is a static structure diagram that describes the set of classes in a system, their attributes, and the relationships among them, simplifying understanding of the system.
  • Class diagrams are key artifacts produced during analysis and design, and they serve as important models for coding and testing.

Class Diagram Notation

Representing a Class

In a UML class diagram, a class is shown as a rectangle divided into compartments for the class name, attributes (fields), and methods. For example, the diagram below depicts an Employee class with three attributes—name, age, address—and a work() method.

Visibility symbols precede attribute or method names:

  • + : public
  • - : private
  • # : protected

Full attribute notation: visibility name : type [ = defaultValue ]

Full method notation: visibility name(parameterList) [ : returnType ]

Notes

  • Content inside brackets is optional.
  • Some notations place the type before the variable name and the return type before the method name.

Example

The Demo class defines three methods:

  • method() – public, no parameters, no return value.
  • method1() – private, no parameters, returns String.
  • method2() – protected, takes two parameters (int and String), returns int.
Relationships Between Classes

Association – a reference relationship between objects (e.g., teacher‑student, master‑apprentice, husband‑wife). It is the most common class‑to‑class relationship and comes in three forms: general association, aggregation, and composition.

Three types of association

  • Unidirectional association – shown as a solid line with an arrow. In the diagram, each Customer has an Address, represented by a member variable of type Address in the Customer class.

  • Bidirectional association – both sides hold a reference to the other’s type, shown as a solid line without arrows. The diagram shows Customer maintaining a List<Product> (a customer can buy many products) and Product holding a Customer reference (the product knows which customer bought it).

  • Self‑association – a line with an arrow that points back to the same class. The diagram indicates that Node contains a member variable of type Node (i.e., “contains itself”).

Aggregation – a strong form of association representing a whole‑part relationship where the part can exist independently of the whole. In UML it is shown as a solid line with a hollow diamond pointing to the whole. Example: a school aggregates teachers; if the school closes, teachers still exist. The diagram illustrates a university‑teacher relationship.

Composition – an even stronger whole‑part relationship. The whole controls the lifecycle of the part; when the whole is destroyed, the part disappears, and the part cannot exist independently. UML shows this with a solid line and a filled diamond pointing to the whole. Example: a head composes a mouth; without a head, the mouth cannot exist. The diagram depicts this relationship.

Dependency – a weak, temporary “uses” relationship, the least coupled form of association. A class depends on another when it accesses the other’s methods via local variables, parameters, or static calls. UML represents this with a dashed arrow from the client (using class) to the supplier (used class). The diagram shows a driver depending on a car.

Generalization (Inheritance) – the strongest coupling, representing an “is‑a” relationship between a superclass and its subclasses. UML uses a solid line with a hollow triangle pointing to the superclass. In code, this is implemented with object‑oriented inheritance. Example: Student and Teacher both inherit from Person; the diagram reflects this hierarchy.

Realization (Implementation) – the relationship between an interface and a class that implements it. UML shows this with a dashed line with a hollow triangle pointing from the implementing class to the interface. Example: Car and Boat both implement a Transport interface; the diagram illustrates this.

Software Design Principles

Design‑pattern principles are the rules programmers should follow when coding; they form the foundation of all patterns (i.e., why patterns are designed the way they are).

Open/Closed Principle (OCP)

Open for extension, closed for modification. When a program needs new functionality, you should add new code rather than change existing code, achieving a plug‑in effect. This improves extensibility, maintainability, and upgradeability.

Achieving OCP typically involves using interfaces and abstract classes. Good abstraction is flexible and widely applicable; as long as the abstraction is sound, the overall architecture remains stable. Variable details can be handled by concrete subclasses derived from the abstraction, allowing changes by adding new subclasses rather than altering existing code.

Single Responsibility Principle (SRP)

A class should have only one reason to change, meaning it should perform one responsibility only.

Example: A UserManager class that handles account creation, validation, login, and logout violates SRP because it has multiple responsibilities. Refactoring into separate classes—each handling a single concern—restores compliance.

Liskov Substitution Principle (LSP)

Any instance of a base class should be replaceable with an instance of a subclass without affecting correctness.

In plain terms, subclasses may extend functionality but must not alter the behavior expected from the base class. Overriding base methods to achieve new behavior can break substitutability, especially when polymorphism is heavily used.

Example: “A square is not a rectangle.” Mathematically a square is a rectangle, but in a geometry‑related software system treating Square as a subclass of Rectangle can cause problems (see the code example in the original text). The conclusion is that Square should not inherit from Rectangle; instead both should implement a common Quadrilateral interface.

Dependency Inversion Principle (DIP)

High‑level modules should not depend on low‑level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

In practice, program to interfaces rather than implementations, reducing coupling between client code and concrete classes.

Example: When assembling a computer, you need a CPU, hard drive, and memory. If the Computer class directly creates concrete IntelCPU, SeagateHDD, and KingstonRAM objects, the design is inflexible. By depending on abstract CPU, HardDrive, and Memory interfaces instead, the Computer can work with any vendor’s components.

Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use. In other words, a class should not be required to implement methods it doesn’t need.

Example: A “secure door” may need fire‑proof, water‑proof, and theft‑proof features. If you later need a door that only requires fire‑proof and water‑proof capabilities, forcing it to implement the full SafetyDoor interface violates ISP. Splitting the responsibilities into separate interfaces resolves the issue.

Law of Demeter (LoD) – “Least Knowledge”

“Talk only to your immediate friends and not to strangers.” If two software entities do not need to communicate directly, they should not call each other; a third party can mediate. This reduces coupling and improves module independence.

In the Demeter context, “friends” include the current object, its member objects, objects it creates, and objects passed as method parameters—any entity that has an association, aggregation, or composition relationship with the current object.

Example: A celebrity’s manager handles fan meetings and media negotiations. The manager is a “friend” of the celebrity, while fans and media companies are “strangers,” illustrating proper application of LoD.

Composite Reuse Principle (CARP)

Prefer composition or aggregation over inheritance when reusing functionality.

Reuse can be inheritance‑based or composition‑based. Inheritance reuse has drawbacks:

  • Breaks encapsulation (the subclass sees the parent’s implementation details – “white‑box” reuse).
  • Creates tight coupling; changes in the superclass ripple to subclasses, hindering extensibility and maintenance.
  • Limits flexibility because inherited behavior is static at compile time.

Composition/aggregation reuse (often called “black‑box” reuse) involves embedding existing objects within new ones, offering several advantages:

  • Preserves encapsulation; the inner object’s details are hidden from the outer object.
  • Reduces coupling; abstract types can be declared as member fields.
  • Increases flexibility; composition can be changed at runtime.

Example: A vehicle management system categorizes cars by power source (gasoline, electric) and by color (white, black, red). Using inheritance leads to many subclasses (e.g., ElectricRedCar). Switching to composition—having separate PowerSource and Color components—dramatically reduces the number of classes needed.

Creational Patterns

Singleton Pattern

Introduction

The Singleton Pattern ensures that only one instance of a class exists throughout the entire software system, and provides a single global access point (typically a static method) to obtain that instance.

Two main variants:

  • Eager initialization – the instance is created when the class is loaded.
  • Lazy initialization – the instance is created only when it is first needed.
Structure

Key participants:

  • Singleton class – the class that can have only one instance.
  • Client – the code that uses the singleton.
Implementations

Eager initialization (three forms)

  • Static variable – declare a static variable of type Singleton and instantiate it at the point of declaration. The instance is created when the class loads, which can waste memory if the object is large and never used.

  • Static initialization block – declare a static variable and instantiate it inside a static block. Functionally similar to the first approach, with the same potential for memory waste.

  • Enum – using an enum to implement a singleton is highly recommended. Enum singletons are thread‑safe and instantiated only once by the JVM. This approach is the only one that cannot be broken by reflection or serialization (it essentially compiles to static final).

Lazy initialization (three forms)

  • Non‑thread‑safe – declare a static variable without initializing it. The instance is created inside getInstance() the first time it is called. In multithreaded environments this can lead to multiple instances being created.

  • Thread‑safe (synchronized method) – add the synchronized keyword to getInstance(). This guarantees safety but incurs a performance penalty because every call acquires the lock, even after the instance is initialized.

  • Double‑checked locking – most calls to getInstance() are reads, which are already thread‑safe. By checking whether the instance is null before entering a synchronized block, and checking again inside the block, you minimize locking overhead while still ensuring safe lazy initialization.

(Content truncated)


Originally written by Li Wei (李唯_) and published in Chinese on 后端技术栈全书 (Full-Stack Backend Engineering). Translated and adapted for DriftSeas with permission.

Keep reading

More related articles from DriftSeas.