Design patterns are typical solutions to recurring design problems in software engineering.
Essence of Design Patterns
Patterns: Not Just Code
Algorithms: Step-by-step procedures for solving problems, like cooking recipes.
Design Patterns: Abstract, flexible schemes for software structure, similar to architectural blueprints.
Recognition: Identifying recurring solutions.
Naming & Sharing: Naming and documenting solutions for others to use.
Community Involvement: A community-driven process with practitioner contributions.
Patterns emerge when a solution is repeated across various projects, eventually gaining a name and detailed description.
Problem-Solving Toolkit: Offers proven solutions for common problems, improving problem-solving skills.
Design Vocabulary: Creates a common language for developers, enhancing communication and collaboration.
Best Practices: Promotes best practices in software design for maintainable and scalable code.
If all you have is a hammer, everything looks like a nail.
Understanding the limitations and appropriate use of design patterns is crucial.
Creational patterns are fundamental for object creation in software design. They provide mechanisms that increase flexibility and reuse of existing code.
| Pattern | Description | Covered |
|---|---|---|
| Factory Method | Delegates the creation of objects to subclasses, promoting flexibility and integration. | ✅ |
| Abstract Factory | Creates families of related or dependent objects without specifying their concrete classes. | ✅ |
| Builder | Constructs complex objects step by step, allowing the creation process to create different types and representations of an object. | ❌ |
| Prototype | Creates new objects by copying an existing object, known as the prototype. | ❌ |
| Singleton | Ensures a class has only one instance while providing a global point of access to it. | ✅ |
Note: Simple Factory is not a true design pattern
When the system needs to be independent of how its objects are created
When the family of related objects is designed to be used together
The Factory Method Pattern defines an interface for creating an object but lets subclasses decide which class to instantiate.
It allows a class to defer instantiation to subclasses.
Key Components:
Creator (Interface or Abstract Class)
Concrete Creator
Product (Interface or Abstract Class)
Concrete Product
The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
It’s a step above the Factory Method Pattern.
Useful for creating a suite of related products.
Factory Method: Creates objects through inheritance.
Abstract Factory: Creates families of related objects without specifying their concrete classes.
Focus on group of products rather than one.
public interface AbstractFactory {
AbstractProductA createProductA();
AbstractProductB createProductB();
}
public class ConcreteFactory1 implements AbstractFactory {
public AbstractProductA createProductA() {
return new ConcreteProductA1();
}
public AbstractProductB createProductB() {
return new ConcreteProductB1();
}
}
// Similar implementation for ConcreteFactory2, ConcreteProductA2, and ConcreteProductB2Abstract Factory is ideal for complex creation processes involving multiple steps.
It can manage dependencies between different products.
Example: Creating a cohesive UI theme (Dark, Light) with compatible components (Buttons, Labels).
Abstract Factory assists in managing dependencies between objects.
Ensures that objects which are meant to work together are compatible.
Example: Ensuring that MacOS Alert Dialogues use MacOS Buttons, not Windows Buttons.
Use Abstract Factory for platform-independent UI creation.
Factories for each platform (MacOS, Windows, Linux) ensure compatible UI elements.
Streamlines development for multi-platform applications.
Guarantees that created objects are consistent with each other.
Prevents mixing incompatible components.
Example: A MacOS alert box will not mistakenly use a Windows button.
Offers flexibility in creating families of objects.
Factories can be easily switched to change the family of created objects.
Useful in scenarios like switching themes or platforms.
Ideal for scenarios like theme switching in an application.
Dark and Light theme factories create UI components with consistent styling.
Simplifies dynamic theme changes in the application.
Consistency: Ensures products from a family are compatible.
Flexibility: Easy to introduce new families of products.
Scalability: Simplifies adding new products to existing families.
Complexity: Can be overkill for simple scenarios.
Modifiability: Changing one part of the system can affect others.
Abstractness: High level of abstraction can be challenging to understand.
Abstract Factory can be combined with Dependency Injection for greater flexibility.
Factories are injected into classes that need to create objects.
This approach decouples the creation logic even further from usage.
UIControlFactory interface with methods like createButton(), createWindow().public interface UIControlFactory {
Button createButton();
Window createWindow();
}
public class DarkThemeUIControlFactory implements UIControlFactory {
public Button createButton() {
return new DarkThemeButton();
}
public Window createWindow() {
return new DarkThemeWindow();
}
}
public class LightThemeUIControlFactory implements UIControlFactory {
public Button createButton() {
return new LightThemeButton();
}
public Window createWindow() {
return new LightThemeWindow();
}
}The Singleton Pattern is a design pattern that:
This pattern is useful for coordinating actions across a system.
Thread safety is crucial in Singleton implementation, especially in multithreaded applications.
Imagine a chat application where you initially think there’s only one chat room.
This example illustrates how the assumption of a single instance can limit application scalability.
Singleton assumes you’ll never need more than one instance, but this isn’t always true.
Let’s delve deeper into the Singleton pattern with an advanced implementation.
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
// Private Constructor
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}This approach uses a static inner class for lazy initialization and is thread-safe without synchronization.
Instead of using Singleton, consider Dependency Injection (DI) for better testability and flexibility.
Singleton often violates the Single Responsibility Principle (SRP) by:
Consider splitting these responsibilities for better design.
Singleton introduces globals, which can lead to:
Singletons pose challenges for unit testing:
Singletons are often used in scenarios like:
Singleton pattern faces several criticisms:
The Singleton pattern and its impact on global state: