Demystifying GoF Design Patterns: Essential Techniques for Crafting Maintainable and Scalable Software Solutions
Binoy Vijayan
Posted on February 7, 2024
In software engineering, a design pattern is a general repeatable solution to a commonly occurring problem in software design. A design pattern isn't a finished design that can be transformed directly into code. It is a description or template for how to solve a problem that can be used in many different situations.
Why design patterns are needed?
Code readability:
They do help you write more understandable code with better names for what you are trying to accomplish.
Code maintainability:
Allows your code to be maintained easier because it is more understandable.
Communication:
They help you communicate design goals amongst programmers.
Intention:
They show the intent of your code instantly to someone learning the code.
Code re-use:
They help you identify common solutions to common problems.
Less code:
They allow you to write less code because more of your code can derive common functionality from common base classes.
Tested and sound solutions:
Most of the design patterns are tested, proven and sound.
Types of design pattern
As per the design pattern reference book ‘ Design Patterns - Elements of Reusable Object-Oriented Software’ (GoF) , there are 23 design patterns which are classified into three categories:
1 Creational Patterns
Creational patterns in software design focus on the process of object creation, providing various mechanisms for creating objects in a flexible and reusable manner.
1.1 Factory Method
Defines an interface for creating an object, but allows subclasses to alter the type of objects that will be created. It provides a way for creating objects without specifying their concrete classes.
Example: Document creation in a word processing application, where subclasses (like ResumeFactory, LetterFactory) define the specific type of document to be created.
1.2 Abstract Factory
Provides an interface for creating families of related or dependent objects without specifying their concrete classes. It is used when a system needs to be independent of how its objects are created, composed, and represented.
Example: GUI toolkit with different components like buttons, text fields, and menus, which can be created for different operating systems (Windows, macOS) using corresponding factories.
1.3 Singleton
Ensures that a class has only one instance and provides a global point of access to that instance. It is useful when exactly one object is needed to coordinate actions across the system.
Example: Logger classes, configuration managers.
1.4 Builder
Separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It provides a way to construct complex objects step by step.
Example: Building a complex object like a meal with different parts (burger, fries, drink) in a fast-food restaurant.
1.5 Prototype
Creates new objects by copying an existing object, known as the prototype, thus avoiding the need for subclasses to create new instances. It is useful when the cost of creating an object is more expensive than copying it.
Example: Creating multiple instances of a complex object with similar properties, like game characters with different attributes.
These creational patterns provide various ways to instantiate objects, allowing developers to choose the most appropriate method based on the specific requirements of the application. They help in promoting flexibility, reusability, and maintainability in object-oriented software design.
2 Structural Patterns
Structural patterns in software design deal with how classes and objects are composed to form larger structures. They help in ensuring that if one part of a system changes, the entire system doesn't need to do so.
2.1 Adapter
Also known as the Wrapper pattern, it allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces by converting the interface of one class into another interface that a client expects.
Example: Converting the interface of a legacy system into a new interface that a modern system requires.
2.2 Decorator
The Decorator pattern allows behaviour to be added to an individual object, either statically or dynamically, without affecting the behaviour of other objects from the same class. It's useful for adding features to objects without subclassing.
Example: Adding additional functionalities (such as encryption or compression) to streams.
2.3 Composite Pattern:
The Composite pattern is used when you need to treat a group of objects in a similar way as a single instance of an object. It composes objects into tree structures to represent part-whole hierarchies.
Example: Representing hierarchical structures like directories and files.
2.4 Facade Pattern:
The Facade pattern provides a unified interface to a set of interfaces in a subsystem. It defines a higher-level interface that makes the subsystem easier to use.
Example: Providing a simplified interface to a complex library or framework.
2.5 Proxy Pattern:
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. It can be used to add a level of indirection to support distributed, controlled, or lazy access.
Example: A virtual proxy that represents an expensive object and delays its creation until it is accessed.
2.6 Bridge Pattern:
The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently. It allows the abstraction and implementation to be developed independently and changed without affecting each other.
Example: Implementing different rendering engines for shapes in a drawing application.
2.7 Flyweight Pattern:
The Flyweight pattern is used to minimise memory usage or computational expenses by sharing as much as possible with similar objects. It's particularly useful when dealing with a large number of similar objects.
Example: Storing shared data such as strings or images once and referencing them across multiple instances.
These patterns help in organising classes and objects in a manner that promotes reusability, flexibility, and maintainability in software systems. Each pattern addresses specific structural concerns and can be applied based on the requirements of the system being designed.
3 Behavioural Patterns
Behavioural patterns in software design focus on defining communication between objects and the responsibilities and interactions between them. These patterns help in designing algorithms, assigning responsibilities, and managing collaborations between objects.
3.1 Chain of Responsibility
Allows multiple objects to handle a request without the client needing to know which object will handle it. Each handler decides either to process the request or to pass it to the next handler in the chain.
Example: In a help desk system, when a user submits a support request, it can be passed through a chain of support staff (tier 1, tier 2, etc.) until someone handles it.
3.2 Command
Encapsulates a request as an object, thereby allowing for parameterisation of clients with queues, requests, and operations. It provides a way to decouple sender and receiver of a request.
Example: A remote control in a smart home system sends commands (objects) to various devices (receivers) such as turning lights on/off or adjusting thermostat settings.
3.3 Interpreter
Defines a grammatical representation for a language and provides an interpreter to interpret sentences in the language. It's useful when dealing with grammatical representations of programming languages or domain-specific languages.
Example: SQL parsers that parse SQL queries and interpret them to execute database operations.
3.4 Iterator
Provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. It allows for uniform traversal of collections, regardless of their implementation.
Example: Iterating over elements in a list, array, or tree data structure using an iterator object.
3.5 Mediator
Defines an object that encapsulates how a set of objects interact. It promotes loose coupling by keeping objects from referring to each other explicitly and allows for variations in interaction independently.
Example: Air traffic control systems where the control tower acts as a mediator between aircraft, coordinating their movements and communication.
3.6 Memento
Captures and externalises an object's internal state so that the object can be restored to this state later, without violating encapsulation.
Example: Undo mechanisms in text editors or graphic design software, where users can revert to previous states of a document or design.
3.7 Observer
Defines a one-to-many dependency between objects so that when one object (the subject) changes state, all its dependents (observers) are notified and updated automatically.
Example: Implementing event handling in graphical user interfaces, where multiple UI elements observe changes in a model.
3.8 State
Allows an object to alter its behaviour when its internal state changes. The object will appear to change its class.
Example: A vending machine that behaves differently depending on its current state (e.g., if it has enough change to give back, if the selected item is in stock).
3.9 Strategy
Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Example: Implementing different sorting algorithms (e.g., bubble sort, merge sort) that can be switched at runtime depending on performance requirements.
3.10 Template Method
Defines the skeleton of an algorithm in the superclass but lets subclasses override specific steps of the algorithm without changing its structure.
Example: Defining a common workflow for generating reports, with subclasses implementing specific steps for different report formats.
3.11 Visitor
Defines a new operation to a set of objects without changing their class. It allows adding new behaviours to objects without modifying their structure.
Example: Implementing operations on a hierarchical structure of objects, such as calculating the total cost of items in a shopping cart.
These patterns provide solutions to common problems related to communication, control flow, and state management between objects in a software system. Each pattern addresses specific concerns and can be applied based on the requirements and characteristics of the system being developed.
Summary
These patterns are not only proven solutions to common design problems but also provide a shared vocabulary for developers to discuss designs and solutions. However, it's essential to use them judiciously and apply them where appropriate, considering factors such as the complexity of the problem, the maintainability of the solution, and the impact on system performance.
Posted on February 7, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 7, 2024