Introduction
Imagine you're an architect tasked with designing a skyscraper. You wouldn't reinvent the wheel by concocting new ways to lay the foundation or structure the steel beams. Instead, you'd lean on tried-and-true blueprints and construction methodologies honed over decades or even centuries. Just like architecture, the world of software engineering benefits from collective wisdom. This wisdom is encapsulated in what are known as design patterns.
Design patterns are akin to architectural blueprints for software; they offer guiding principles and evolved solutions for common challenges encountered in software design. Far from being one-size-fits-all templates, design patterns provide general outlines that can be adapted to solve specific problems in your code. When applied judiciously, these patterns enhance code maintainability, facilitate scalability, and improve readability. They serve as a common language that can bridge gaps between developers, easing communication and expediting problem-solving. By mastering design patterns, you're not just writing code; you're crafting software with the best practices and insights that the field of computer science has to offer. Below, you’ll find a list of the 3 categories of design patterns and their most important forms.
Creational Patterns
Creational design patterns deal with object creation mechanisms, aiming to create objects in a manner suitable to the situation. Instead of having a system create objects directly using constructors, these patterns use a separate method, ensuring that the system remains independent of how its objects are composed or represented. This abstraction of the instantiation process helps manage object creation by choosing the appropriate method based on the current context, leading to a more flexible and scalable system. Examples of creational patterns include the Singleton, Factory Method, and Builder patterns. Each of these patterns provides unique methods for object creation, allowing developers to encapsulate and abstract the instantiation logic, making the system more adaptable to future changes and variations.
Singleton
The Singleton design pattern ensures that a particular class has only one instance and provides a global access point to that instance. The pattern is particularly useful when one object controls access to a resource, such as a database or a file. The idea is to prevent the instantiation of a class more than once, which is achieved by making the constructor private and defining a public static method that returns the unique instance of the class. The first time this method is called, it creates a new instance of the class and stores it in a static variable. Subsequent calls to the method return the stored instance, ensuring that there is only ever one instance of the class in existence. By providing a single point of access to the resource, the Singleton pattern allows for greater control and ease in managing the resource while also promoting efficient use of system resources.
class DatabaseConnection: """A singleton class representing a connection to a database.""" _instance = None def get_instance(cls): if cls._instance is None: cls._instance = DatabaseConnection() return cls._instance def __init__(self): # Connect to the database pass def close(self): # Close the connection to the database pass # Example usage: database_connection = DatabaseConnection.get_instance() # Use the database connection database_connection.close()
In the above example, the
DatabaseConnection class is a singleton. The get_instance() class method can be used to get the single instance of the DatabaseConnection class. The client code can then use the DatabaseConnection object to access the database.Factory Method
The Factory Method design pattern is a design pattern that provides an interface for creating objects in a super class, but allows subclasses to alter the type of objects that will be created. Instead of calling a constructor directly to instantiate an object, a method is used to create the object, hence the name "Factory Method". This pattern is particularly useful when the exact type of the object isn't known until runtime or when the creation process is complex or involves multiple steps. By delegating this responsibility to subclasses, the Factory Method allows for greater flexibility and decoupling in code. The client code interacts with the factory method to get an instance of a class, without needing to know the specific subclass that's being instantiated, promoting the principle of programming to an interface rather than an implementation.
class Vehicle: def __init__(self, type): self.type = type def drive(self): pass class Car(Vehicle): def drive(self): print("Driving a car") class Truck(Vehicle): def drive(self): print("Driving a truck") class VehicleFactory: def create_vehicle(self, type): if type == "car": return Car(type) elif type == "truck": return Truck(type) else: raise ValueError("Invalid vehicle type") vehicle_factory = VehicleFactory() car = vehicle_factory.create_vehicle("car") truck = vehicle_factory.create_vehicle("truck") car.drive() truck.drive() #Output #Driving a car #Driving a truck
In the above example, the
VehicleFactory class is a factory for creating Vehicle objects. The create_vehicle() method takes a vehicle type as input and returns a Vehicle object of that type. The client code can then use the VehicleFactory class to create Vehicle objects without having to know the specific classes of the Vehicle objects that it is creating.Builder
The Builder design pattern allows for the step-by-step construction of complex objects. The primary purpose of the Builder pattern is to separate the construction of a complex object from its representation, thus allowing for the same construction process to create different representations. Instead of using a single monolithic constructor with numerous parameters, or multiple constructors for various combinations of parameters, the Builder pattern breaks down the object construction into a set of distinct steps. Each step is represented by a method in the 'Builder' interface, and a concrete builder implements this interface to provide specific details for each step. Typically, a "Director" class defines the sequence in which to execute the building steps. Once the building process is complete, the Builder returns the constructed object. This approach offers a more readable, maintainable, and flexible way to create complex objects, especially when the objects consist of multiple components or configurations.
class Pizza: def __init__(self, crust_type, sauce_type, toppings): self.crust_type = crust_type self.sauce_type = sauce_type self.toppings = toppings def __str__(self): return f"Pizza with {self.crust_type} crust, {self.sauce_type} sauce, and {self.toppings}" class PizzaBuilder: def __init__(self): self.pizza = Pizza(None, None, []) def set_crust_type(self, crust_type): self.pizza.crust_type = crust_type def set_sauce_type(self, sauce_type): self.pizza.sauce_type = sauce_type def add_topping(self, topping): self.pizza.toppings.append(topping) def build(self): return self.pizza # Example usage: pizza_builder = PizzaBuilder() pizza_builder.set_crust_type("thin") pizza_builder.set_sauce_type("tomato") pizza_builder.add_topping("pepperoni") pizza_builder.add_topping("mushrooms") pizza = pizza_builder.build()
In the above example, the
PizzaBuilder class is responsible for constructing a Pizza object. The client code can then use the PizzaBuilder class to create different types of pizzas, such as a pepperoni pizza, a mushroom pizza, or a custom pizza with any combination of toppings.Structural Patterns
Structural design patterns focus on the composition of classes or objects, allowing them to form larger structures in a flexible and efficient manner. These patterns ensure that the system's structure remains scalable, maintainable, and adaptable as it evolves over time. By defining clear ways in which objects can be composed or combined, structural patterns help developers build systems that are more organized, cohesive, and versatile. Examples of structural patterns include the Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy patterns. Each of these patterns addresses specific challenges associated with object and class composition, leading to cleaner, more modular, and easily extensible architectural designs.
Adapter
The Adapter design pattern allows objects with incompatible interfaces to work together. It acts as a bridge between two interfaces. In scenarios where you want to integrate a legacy component with a new system, or when you want to use a third-party library but its interface doesn't match the one you expect, the Adapter pattern can be very useful. The pattern involves introducing an additional layer—an "adapter"—that translates or converts method calls from the expected interface to the one provided by the legacy or third-party component.
class MediaPlayer: def play(self, filename): pass class MP3Player(MediaPlayer): def play(self, filename): print(f"Playing MP3 file: {filename}") class VLCPlayer(MediaPlayer): def play(self, filename): print(f"Playing VLC file: {filename}") class VideoMediaPlayerAdapter(MediaPlayer): def __init__(self, video_player: VLCPlayer): self.video_player = video_player def play(self, filename): self.video_player.play(filename) # Example usage: mp3_player = MP3Player() mp3_player.play("my_song.mp3") vlc_player = VLCPlayer() vlc_player.play("my_movie.mp4") # Use the adapter to play a video file in an MP3 player video_media_player_adapter = VideoMediaPlayerAdapter(vlc_player) video_media_player_adapter.play("my_movie.mp4") Output ---------------------- Playing MP3 file: my_song.mp3 Playing VLC file: my_movie.mp4 Playing VLC file: my_movie.mp4
Decorator
The Decorator design pattern allows you to dynamically attach new responsibilities to objects without modifying their structure. This pattern involves a set of decorator classes that mirror the type of the objects they enhance, but add or override behavior. The main advantage of this pattern is that it promotes the Single Responsibility Principle, enabling functions to be divided between classes with unique areas of concern. Decorators provide an alternative to subclassing for extending functionality.
# Base Component class Beverage: def cost(self): pass # Concrete Component class Coffee(Beverage): def cost(self): return 5 # Base Decorator class AddonDecorator(Beverage): def __init__(self, beverage): self.beverage = beverage # Concrete Decorator A class Milk(AddonDecorator): def cost(self): return self.beverage.cost() + 2 # Concrete Decorator B class Sugar(AddonDecorator): def cost(self): return self.beverage.cost() + 1 # Example usage: # Ordering just a coffee coffee = Coffee() print(f"Cost of coffee: ${coffee.cost()}") # Decorating coffee with milk coffee_with_milk = Milk(coffee) print(f"Cost of coffee with milk: ${coffee_with_milk.cost()}") # Decorating coffee with milk and sugar coffee_with_milk_and_sugar = Sugar(coffee_with_milk) print(f"Cost of coffee with milk and sugar: ${coffee_with_milk_and_sugar.cost()}")
In this example,
Coffee is the concrete component. We have decorators like Milk and Sugar that can enhance the Coffee object. The beauty of the decorator pattern here is that you can stack decorators dynamically and in any combination without having to create a myriad of subclasses for each possible combination.Composite
The Composite design pattern allows you to compose objects into tree structures to represent part-whole hierarchies. This pattern treats both individual objects and their compositions uniformly. This way, clients can treat single objects and compositions of objects consistently. Composite is particularly useful when you want to represent hierarchies of objects.
# Component class Graphic: def render(self): pass # Leaf class Circle(Graphic): def render(self): print("Rendering a circle") # Leaf class Square(Graphic): def render(self): print("Rendering a square") # Composite class GraphicGroup(Graphic): def __init__(self): self.graphics = [] def render(self): for graphic in self.graphics: graphic.render() def add(self, graphic): self.graphics.append(graphic) def remove(self, graphic): self.graphics.remove(graphic) # Example usage: circle1 = Circle() circle2 = Circle() square = Square() graphic_group1 = GraphicGroup() graphic_group1.add(circle1) graphic_group1.add(square) graphic_group2 = GraphicGroup() graphic_group2.add(circle2) # Creating a composite of composites main_group = GraphicGroup() main_group.add(graphic_group1) main_group.add(graphic_group2) main_group.render()
In the usage example, we're creating a hierarchy of graphics, where we have both individual graphics (
Circle, Square) and groups of graphics (GraphicGroup). The main_group is a composite of composites, demonstrating the tree structure and part-whole hierarchy. When we call render() on the main group, it cascades down the hierarchy and renders all elements, treating individual elements and their compositions in a uniform manner.Behavioral Patterns
Behavioral design patterns are a subset of design patterns that deal with the assignment of responsibilities between objects, ensuring that they collaborate and communicate effectively. These patterns focus on the interactions and relationships between objects, rather than their individual internal structures or the process of their creation. By abstracting common communication patterns, they decouple objects, making them more modular and facilitating easier and more maintainable system design. Examples of behavioral patterns include the Observer and Iterator patterns. Each of these patterns provides a unique solution to specific object communication and interaction challenges, ultimately leading to more efficient and adaptable software designs.
Observer
The Observer design pattern defines a one-to-many dependency between objects. When one object (usually referred to as the "subject") changes state, all its dependents (observers) are notified and updated automatically. This pattern is particularly useful in situations where an action on one object requires an immediate update in others. It promotes the decoupling of a subject from its observers, allowing them to interact without strong coupling.
# Observer Interface class Observer: def update(self, message): pass # Concrete Observer class ConcreteObserver(Observer): def __init__(self, name): self.name = name def update(self, message): print(f"Observer {self.name} received message: '{message}'") # Subject Interface class Subject: def __init__(self): self._observers = [] def add_observer(self, observer): self._observers.append(observer) def remove_observer(self, observer): self._observers.remove(observer) def notify_observers(self, message): for observer in self._observers: observer.update(message) # Concrete Subject class ConcreteSubject(Subject): def __init__(self): super().__init__() self._state = None def set_state(self, state): self._state = state self.notify_observers(f"State has been updated to {state}") # Example usage: observer1 = ConcreteObserver("A") observer2 = ConcreteObserver("B") observer3 = ConcreteObserver("C") subject = ConcreteSubject() subject.add_observer(observer1) subject.add_observer(observer2) subject.add_observer(observer3) subject.set_state("ON")
Observeris the interface for objects that need to be informed of changes in theSubject.
ConcreteObserveris the concrete class that implements the Observer interface. It acts upon notifications from the subject.
Subjectis an abstract class that maintains a list of observers and can notify them.
ConcreteSubjectis a class that has some state and notifies its observers when this state changes.
When
set_state("ON") is called on the ConcreteSubject, all registered observers are notified of the change in state, and they act accordingly by printing the message they received.Iterator
The Iterator design pattern provides a way to access the elements of an aggregate object (like a list, tree, or graph) sequentially without exposing its underlying representation. The main idea is to abstract the iteration mechanism and provide a uniform way to traverse different aggregate structures. This pattern typically involves two main components: the
Iterator and the Aggregate.# Iterator Interface class Iterator: def has_next(self): pass def next(self): pass # Concrete Iterator class ConcreteIterator(Iterator): def __init__(self, aggregate): self._aggregate = aggregate self._index = 0 def has_next(self): return self._index < len(self._aggregate) def next(self): if self.has_next(): value = self._aggregate[self._index] self._index += 1 return value return None # Aggregate Interface class Aggregate: def create_iterator(self): pass # Concrete Aggregate class ConcreteAggregate(Aggregate): def __init__(self): self._items = [] def add(self, item): self._items.append(item) def __len__(self): return len(self._items) def __getitem__(self, index): return self._items[index] def create_iterator(self): return ConcreteIterator(self) # Example usage: aggregate = ConcreteAggregate() aggregate.add("item1") aggregate.add("item2") aggregate.add("item3") iterator = aggregate.create_iterator() while iterator.has_next(): print(iterator.next())
Iteratoris the abstract interface for creating iterators.
ConcreteIteratoris the concrete iterator for iterating over items of theConcreteAggregate.
Aggregateis the abstract interface for creating aggregates.
ConcreteAggregateis the concrete aggregate containing items and provides a method to create its corresponding iterator.
The example showcases a simple list-based aggregate, but the same pattern can be applied to more complex structures like trees or graphs. The primary advantage is that clients can iterate over different aggregate structures in a consistent way, without needing to know the internal details of these structures.