Design Patterns

Betül Necanlı
16 min readNov 11, 2023

--

⭐️ This article includes my notes on design patterns using Kotlin. As you delve into this article, please keep in mind that these notes are a product of my understanding and interpretation.Feel free to drop me a message with any corrections, suggestions, or questions you might have.

Happy coding! 🚀

Abstract Factory Pattern

The Abstract Factory pattern is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes. It’s useful when you need to ensure that the created objects are compatible and follow a specific theme.

✍️ Suppose we are building a UI library that supports creating different types of buttons and checkboxes for different operating systems: Windows and MacOS.

  • GUIFactory is the abstract factory interface that declares the creation methods for buttons and checkboxes.
  • WindowsFactory and MacOSFactory are concrete implementations of the abstract factory, providing instances of WindowsButton, WindowsCheckbox, MacOSButton, and MacOSCheckbox.
  • Application is the client code that uses the abstract factory to create a family of related UI components.

You would use the Abstract Factory pattern when:

  1. You need to ensure that the created objects are compatible. In this example, you want to make sure that a Windows button is used with a Windows checkbox and a MacOS button is used with a MacOS checkbox.
  2. You want to isolate the system-specific classes. Abstract Factory allows you to encapsulate the creation of product objects, making it easier to change the product families or add new families without modifying the client code.

📝 In real-world scenarios, Abstract Factory is often used in GUI libraries, where different operating systems or look-and-feel themes require different sets of UI components.

Adapter Pattern

The Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces by converting the interface of a class into another interface that a client expects.

✍️Let’s consider an example where you have a legacy class with a different interface, and you want to use it in a system that expects a different interface. We’ll create an adapter to make these two interfaces compatible.

In this example:

  • Target is the interface expected by the client.
  • Adaptee is a class with a different interface that we want to use.
  • Adapter is a class that implements the Target interface and delegates the actual work to the Adaptee.

You would use the Adapter Pattern when:

  1. You have existing classes with incompatible interfaces, and you cannot change their code.
  2. You want to use a class in a system that expects a different interface. Instead of modifying the existing code, you create an adapter to make the two interfaces compatible.

📝 The Adapter Pattern is commonly used when integrating new code with existing code or when working with third-party libraries that have different interfaces. It allows for smoother integration without modifying the existing codebase.

Builder Pattern

The Builder Pattern is a creational design pattern that allows the construction of a complex object step by step. It separates the construction of a complex object from its representation, so that the same construction process can create different representations.

✍️Let’s say we are building a Person class with various optional attributes like name, age, address, and phone number. The Builder Pattern can help create Person objects with different combinations of these optional attributes.

  • Person is the class we want to construct.
  • PersonBuilder is the builder interface that declares methods for setting each optional attribute and a method for building the Person object.
  • ConcretePersonBuilder is the concrete builder that implements the PersonBuilder interface. It maintains the state of the object being constructed and provides methods for setting each attribute.
  • The client code uses the builder to construct Person objects with different combinations of attributes.

You would use the Builder Pattern when:

  1. The construction of an object involves a large number of optional parameters. Instead of having multiple constructors with different parameter combinations, the Builder Pattern provides a clean way to construct objects step by step.
  2. You want to create immutable objects. The Person class in this example is immutable because its attributes are set only once during construction.
  3. You want to improve the readability of the client code. With the Builder Pattern, the client code can be more expressive, and it’s clear which attributes are being set.

📝 The Builder Pattern is commonly used in scenarios where you need to create complex objects with many optional parameters, and it provides a flexible and readable way to do so.

Command Pattern

The Command Pattern is a behavioral design pattern that turns a request into a stand-alone object, containing all information about the request. This transformation allows for parameterization of clients with different requests, queuing of requests, and logging of the requests. It also provides support for undoable operations.

✍️We’ll create a simple remote control that can be programmed with different commands to control various devices.

  • Command is the interface that declares the execute method.
  • LightOnCommand and LightOffCommand are concrete command classes that encapsulate the actions of turning on and off a light.
  • Light is the receiver class, representing the device being controlled.
  • RemoteControl is the invoker class, which is programmed with a command and can press a button to execute that command.

You would use the Command Pattern when:

  1. You want to parameterize objects with operations. In this example, the RemoteControl is parameterized with different commands (LightOnCommand and LightOffCommand).
  2. You want to queue requests, log requests, or undo operations. By encapsulating a request in a command object, you can store, queue, and manipulate requests as needed.
  3. You want to decouple senders and receivers. The sender (RemoteControl) knows nothing about the receiver (Light), and commands provide a way to connect the two without directly coupling them.

📝 The Command Pattern is particularly useful in scenarios where you need to support a variety of operations on objects, especially when you want to make the system extensible by adding new commands without modifying existing code.

Decorator Pattern

The Decorator Pattern is a structural design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class. It’s often used to extend the functionalities of classes in a flexible and reusable way.

✍️We’ll use a simple coffee ordering system where we want to add condiments (decorators) to a base coffee.

  • Coffee is the component interface representing the base coffee.
  • SimpleCoffee is a concrete component representing the base coffee.
  • CoffeeDecorator is an abstract class implementing the Coffee interface. It acts as a base class for concrete decorators.
  • MilkDecorator and SugarDecorator are concrete decorators that add functionality (cost and description) to the base coffee.

You would use the Decorator Pattern when:

  1. You want to add responsibilities to objects dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
  2. You want to avoid a high level of coupling between classes. Decorators allow you to stack new functionalities on top of existing classes without tightly coupling them.
  3. You need to extend a class’s behavior in a reusable way. Decorators can be easily combined and reused to create different combinations of functionalities.

📝 The Decorator Pattern is commonly used in scenarios where you want to extend the behavior of classes in a flexible and dynamic way, especially when you have a set of base functionalities and want to add optional or additional functionalities to objects at runtime.

Dependency Injection (DI)

Dependency Injection (DI) is a design pattern that deals with how components get hold of their dependencies. The basic idea is to provide the dependencies from the outside rather than creating them within the class.

  • MessageService is an interface defining a contract for sending messages.
  • EmailService is an implementation of the MessageService interface.
  • NotificationService is a client class that depends on MessageService. Instead of creating an instance of EmailService within NotificationService, we inject it through the constructor.

You would use Dependency Injection when:

  1. You want to achieve a higher degree of decoupling between components. By injecting dependencies, a class doesn’t need to instantiate or manage its dependencies directly, making it more flexible and easier to test.
  2. You want to make your code more maintainable and scalable. Dependency Injection makes it easier to modify and extend your codebase by allowing you to change implementations or add new features without modifying existing classes.
  3. You want to facilitate unit testing. Injecting dependencies makes it easier to substitute real implementations with mock objects during testing, allowing you to isolate and test components independently.

✍️There are several ways to perform Dependency Injection:

Constructor Injection: Inject dependencies through the constructor.

Setter Injection: Inject dependencies through setter methods.

Method Injection: Inject dependencies through methods.

The choice between these methods depends on the specific requirements of your application. Constructor injection is often preferred for mandatory dependencies, while setter or method injection can be used for optional dependencies.

📝 Dependency Injection frameworks, such as Dagger, Koin, or Spring, automate the process of injecting dependencies in more complex applications. They provide mechanisms for managing the lifecycle of objects and wiring dependencies together.

Facade Pattern

The Facade Pattern is a structural design pattern that provides a simplified interface to a set of interfaces in a subsystem. It defines a higher-level interface that makes the subsystem easier to use.

✍️Let’s consider a computer system with various components, and we’ll create a ComputerFacade to simplify the interaction with these components:

  • CPU, Memory, and HardDrive are subsystem components with their specific functionalities.
  • ComputerFacade is the facade class that provides a simplified interface to start the computer, hiding the complexities of interacting with the subsystem components.

You would use the Facade Pattern when:

  1. You want to provide a simple interface to a complex system. Facades simplify the usage of a system by providing a higher-level interface that encapsulates the complexities of the subsystem.
  2. You want to decouple the client code from the subsystem components. Facades shield the client code from the details of the subsystem, reducing dependencies and making the system more maintainable.
  3. You want to improve code readability and maintainability. By providing a clear and concise interface, facades make it easier to understand and use a system.
  4. You want to wrap a poorly designed API with a better one. If you have a subsystem with a complex or poorly designed interface, a facade can provide a more elegant and streamlined interface for clients.

📝 The Facade Pattern is often used in large systems, libraries, or frameworks to simplify the usage of complex functionalities. It promotes a cleaner design by encapsulating the details of subsystem interactions, making it easier for clients to use the system without being overwhelmed by its complexity.

Factory Method Pattern

The Factory Method Pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It defines an interface for creating an object, but leaves the choice of its type to the subclasses, creating an instance of one of several possible classes.

✍️Let’s consider a scenario where we have different document types (e.g., PdfDocument and CsvDocument), and we want to create documents using a factory method:

  • Document is the product interface that defines the behavior of documents.
  • PdfDocument and CsvDocument are concrete products that implement the Document interface.
  • DocumentCreator is the creator interface with the factory method createDocument().
  • PdfDocumentCreator and CsvDocumentCreator are concrete creators that implement the factory method to create specific types of documents.

You would use the Factory Method Pattern when:

  1. A class cannot anticipate the class of objects it must create. The pattern allows a class to delegate the responsibility of instantiating its objects to its subclasses.
  2. A class wants its subclasses to specify the objects it creates. The pattern promotes flexibility by allowing subclasses to alter the type of objects created without modifying the client code.
  3. You want to provide a hook for subclasses to create objects. The factory method serves as a hook that subclasses can override to provide their own implementation.
  4. You want to avoid tight coupling between the client code and the concrete classes it uses. The pattern helps in keeping the client code independent of the classes it creates, promoting a more maintainable and extensible design.

📝 The Factory Method Pattern is commonly used in frameworks where the framework needs to instantiate different classes based on client requirements. It promotes loose coupling and allows for better customization and extension of a system.

Fluent Interface

The Fluent Interface pattern, also known as Method Chaining, is a design pattern that aims to create code that is more readable and expressive by allowing method calls to be chained together in a way that resembles a natural language.

✍️Let’s create a simple Person class with fluent methods for setting various attributes:

The Person class has fluent methods (setName, setAge, setAddress) that return the instance of the Person class, allowing method calls to be chained.

You would use the Fluent Interface pattern when:

  1. You want to improve the readability of your code. Fluent interfaces make code more expressive and can read like a natural language, which can enhance the understanding of the code.
  2. You want to provide a convenient and intuitive API. Fluent interfaces allow users of your API to chain method calls in a sequence that makes sense in the context of the problem domain.
  3. You want to reduce the need for temporary variables. With method chaining, you can perform a series of operations on the same object without the need for intermediate variables.
  4. You want to enforce a specific order of method calls. Fluent interfaces guide users through a sequence of method calls, ensuring that they follow a specific order.

It’s important to note that while Fluent Interfaces can improve code readability, they should be used judiciously. Overusing method chaining can lead to code that is difficult to maintain or understand. It’s crucial to strike a balance between readability and avoiding excessive complexity.

📝 Fluent Interfaces are commonly used in libraries, frameworks, or DSLs (Domain-Specific Languages) where providing a clear and expressive API is essential. They can be particularly beneficial in scenarios where method chaining aligns with the natural flow of operations.

Memento

The Memento pattern is a behavioral design pattern that provides the ability to restore an object to its previous state (undo functionality) without revealing its internal structure.

  • TextEditor is the originator class whose state we want to track.
  • Memento is the memento class that holds the state of the TextEditor.
  • History is the caretaker class responsible for keeping track of mementos.

The createMemento() method in the TextEditor class creates a memento object containing the current state, and restoreMemento() allows the state to be restored.

📝 You would use the Memento pattern in scenarios where you need to capture and restore the state of an object without violating encapsulation. It’s particularly useful for implementing features like undo/redo functionality in applications, or for saving and restoring checkpoints in a game or simulation. The pattern allows you to externalize an object’s internal state, making it possible to revert to a previous state without exposing the details of that state.

Monostate Pattern

The Monostate pattern, also known as the “Borg” pattern, is indeed similar to the Singleton pattern in the sense that there is a single shared state, but it differs in the way individual instances interact with that shared state. In the Monostate pattern, each instance shares the same state, so changing the state in one instance affects all instances.

  • SharedData is a class containing shared data in its companion object.
  • MonostateClient instances access and update the shared data.

The Monostate pattern can be useful when you want multiple instances to share the same state but maintain individual local states as well.

Use the Monostate pattern when:

  1. You want multiple instances to share the same state. All instances access and modify the same data.
  2. You want to avoid the limitations of traditional singletons. The Monostate pattern allows for multiple instances that share the same state, which can be useful in certain scenarios.

📝 It’s important to note that while the Monostate pattern achieves a similar outcome to the Singleton pattern in terms of shared state, it does so with a different mechanism, allowing for more flexibility in certain situations. However, it’s less common and can be confusing to developers who are not familiar with the pattern, so use it judiciously based on your specific requirements.

Observer Pattern

The Observer Pattern is a behavioral design pattern where an object, known as the subject, maintains a list of its dependents, called observers, that are notified of any changes in the subject’s state. It is a common pattern used for implementing distributed event handling systems.

✍️Let’s create a simple example where a Subject (publisher) maintains a list of Observer instances (subscribers) and notifies them when its state changes:

  • Observer is the interface that observers implement to receive updates.
  • ConcreteObserver is a concrete observer that prints updates to the console.
  • Subject is the interface that subjects implement. It has methods to add, remove, and notify observers.
  • ConcreteSubject is a concrete subject that maintains a list of observers and notifies them when its state changes.

You would use the Observer Pattern when:

  1. You have a one-to-many relationship between objects. When changes in one object should be reflected in multiple other objects, the Observer Pattern provides a flexible solution.
  2. You want to decouple the sender and receivers of updates. Observers are loosely coupled with the subject, and they can be added or removed without affecting the subject or other observers.
  3. You need to notify multiple objects about changes without specifying them explicitly. Observers can be dynamically added or removed, allowing for a more flexible and extensible system.
  4. You want to implement distributed event handling. The Observer Pattern is commonly used in GUI frameworks, event-driven architectures, and other scenarios where changes in one part of a system should trigger actions in other parts.

📝 The Observer Pattern is a powerful way to establish communication between objects in a decoupled manner, promoting maintainability and extensibility. It is widely used in various software development contexts, including graphical user interfaces, game development, and system monitoring.

Singleton Pattern

The Singleton Pattern is a creational design pattern that ensures a class has only one instance and provides a global point of access to that instance.

  • The Singleton class has a private constructor to prevent direct instantiation.
  • The companion object contains the lazy-initialized instance of the class.
  • The getInstance method provides the global point of access to the single instance.

You would use the Singleton Pattern when:

  1. You want to ensure that a class has only one instance. This is useful when exactly one object is needed to coordinate actions across the system.
  2. You need a global point of access to the instance. The Singleton pattern provides a well-known access point to the single instance.
  3. You want to control access to shared resources. For example, a database connection pool can be implemented as a Singleton to control access to a limited number of connections.
  4. You want to provide a single point for configuration settings. A Singleton can be used to store and provide access to configuration settings throughout the application.

When to be cautious:

  1. Global state: While Singletons are useful, they introduce global state, which can make it challenging to reason about the behavior of your system.
  2. Testing: Singletons can be difficult to unit test because they introduce a shared state that persists between tests. Consider using dependency injection or other approaches for testability.
  3. Concurrency: Be mindful of concurrency issues when using Singletons, especially in multithreaded environments. Ensure that the initialization of the Singleton is thread-safe.

📝 The Singleton Pattern is commonly used in scenarios where a single point of control or coordination is needed, such as managing resources, configuration settings, or logging. However, it’s important to use Singletons judiciously and be aware of the potential downsides associated with global state and testability.

Strategy Pattern

The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each algorithm, and makes them interchangeable. It lets the client choose the appropriate algorithm at runtime.

✍️Let’s create a system where different sorting algorithms can be used interchangeably:

  • SortingStrategy is the strategy interface defining the sort method.
  • BubbleSortStrategy, QuickSortStrategy, and MergeSortStrategy are concrete strategies that implement different sorting algorithms.
  • Sorter is the context class that uses a strategy. It can switch between different sorting strategies at runtime.

You would use the Strategy Pattern when:

  1. You want to define a family of algorithms. Strategies allow you to encapsulate algorithms in separate classes.
  2. You want to make algorithms interchangeable. Clients can choose different algorithms without altering their code.
  3. You want to avoid a proliferation of conditional statements. The Strategy Pattern eliminates conditional statements related to selecting algorithms.
  4. You have a class with a behavior that can vary. Instead of using subclasses or conditional statements, you can use the Strategy Pattern to encapsulate the behavior.
  5. You want to isolate the implementation details of an algorithm from the client code. Strategies encapsulate the details of algorithms, promoting better separation of concerns.

📝 The Strategy Pattern is particularly useful when you have a family of algorithms that share a common interface, and you want to allow clients to choose the most appropriate algorithm dynamically. It promotes flexibility, maintainability, and code reuse by encapsulating algorithms in separate classes.

Visitor Pattern

The Visitor Pattern is a behavioral design pattern that allows you to define new operations on the elements of an object structure without changing the classes of the elements themselves. It separates the algorithm from the object structure on which it operates.

✍️Let’s create a simple structure representing different shapes and a visitor that calculates the area of each shape:

  • Shape is the element interface that declares the accept method, allowing visitors to visit shapes.
  • Circle and Rectangle are concrete elements that implement the Shape interface.
  • ShapeVisitor is the visitor interface declaring methods for each concrete shape.
  • AreaCalculator is a concrete visitor that calculates the area of each shape and maintains a total area.

You would use the Visitor Pattern when:

  1. You have a set of classes with varying interfaces and you want to perform operations on these classes without modifying their code. The Visitor Pattern allows you to define external operations without altering the classes.
  2. You need to perform different operations on an object structure based on its elements. The Visitor Pattern allows you to encapsulate these operations in separate visitor classes.
  3. You want to keep related behavior in one place rather than spreading it across multiple classes. Visitors centralize the behavior related to a specific operation.
  4. You want to add new operations to the object structure without modifying existing code. You can introduce new visitors to extend the functionality.

📝 The Visitor Pattern is particularly useful when the structure of the elements is stable, but you want to introduce new operations on those elements without modifying their classes. It promotes a clean separation between the data structure and the algorithms that operate on it.

Chain of Responsibility

The Chain of Responsibility Pattern is a behavioral design pattern that lets you pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it along the chain.

✍️Let’s create a simple example where a request to approve a purchase order is processed by different authorities:

  • Approver is the handler interface that declares the processRequest method.
  • Manager, Director, and VicePresident are concrete handlers that implement the Approver interface. Each handler decides whether to process the request or pass it to the next approver in the chain.
  • PurchaseRequest represents a purchase order that needs approval.
  • The client sets up the chain of responsibility and initiates the processing of purchase requests.

You would use the Chain of Responsibility Pattern when:

  1. You want to decouple request senders from receivers. The pattern allows multiple objects to handle a request without the sender needing to know which object will handle it.
  2. You have a sequence of objects that can handle a request. Each object in the chain decides whether to process the request or pass it to the next object in the chain.
  3. You want to allow multiple objects to handle a request without specifying the receiver explicitly. The client initiates the request, and the chain determines the appropriate handler.
  4. You want to dynamically change the order of handlers or add new handlers. Handlers can be added, removed, or reordered without modifying the client code.

📝 The Chain of Responsibility Pattern is commonly used in scenarios where there are multiple objects that can handle a request, and the system needs flexibility in determining which object should handle the request. It promotes a flexible and scalable way of handling requests with varying degrees of complexity.

--

--