Structural Patterns
Creational
Structural
Behavioral
- Chain of Responsibility (opens in a new tab)
- Command (opens in a new tab)
- Interpreter (opens in a new tab)
- Iterator (opens in a new tab)
- Mediator (opens in a new tab)
- Memento (opens in a new tab)
- Observer (opens in a new tab)
- State (opens in a new tab)
- Strategy (opens in a new tab)
- Template (opens in a new tab)
- Visitor (opens in a new tab)
Objective
- Structural design patterns.
Design Patterns
Design patterns are proven, reusable solutions to common problems that occur in software design. They represent best practices that experienced developers have refined over time, and they can be adapted to fit various situations. Each pattern provides a standard template for solving specific design challenges, which helps streamline the development process and improve code maintainability and scalability.
Design patterns offer several benefits:
- Promote reusability: By using design patterns, developers can apply tried-and-true approaches to solve problems more efficiently without reinventing the wheel.
- Enhance code maintainability: Well-structured solutions simplify future modifications and reduce the risk of bugs when making changes.
- Improve communication: Design patterns provide a common vocabulary for developers, making it easier to discuss and share ideas.
- Facilitate scalability: Patterns often enable systems to be designed with future growth and additional functionality in mind.
- Enforce best practices: Using patterns encourages developers to adhere to proven coding principles, which results in more robust and reliable software.
Design patterns are generally categorized into three main types:
- Creational patterns: Focus on the process of object creation.
- Structural patterns: Deal with object composition and the relationships between entities to create larger, more complex structures.
- Behavioral patterns: Concerned with the interaction and responsibility between objects.
By applying the appropriate design patterns, developers can create more maintainable, flexible, and efficient code that aligns with established best practices.
Code refactoring refers to the process of restructuring an existing codebase without changing its external behavior, while ensuring that the corresponding UML diagrams, such as class, sequence, or activity diagrams, are updated to reflect these changes. This practice helps improve the design, structure, and maintainability of the software.
Refactoring might involve:
- Reorganizing class structures: Modifying class hierarchies or relationships (e.g., extracting a superclass, merging classes) to make the design clearer and reduce duplication.
- Improving method cohesion: Splitting or merging methods, adjusting their interactions in sequence diagrams to enhance code readability and reduce complexity.
- Renaming elements: Updating class, attribute, or method names in both the code and UML diagrams to make them more meaningful.
- Encapsulation adjustments: Modifying access controls or moving methods between classes while updating diagrams to match.
- Simplifying interactions: Streamlining complex process flows represented in activity or sequence diagrams by refactoring logic.
Overall, the goal of code refactoring is to maintain consistent documentation and improve the system’s design quality without altering the end functionality.
“There are so many variations on the “there are only two hard problems in computer programming…” joke that I’m starting to suspect that programming isn’t actually very easy.” —Nat Pryce
Structural Patterns
- Adapter: Interface compatibility
- Bridge: Implementation-abstraction separation
- Composite: Part-whole hierarchies
- Decorator: Dynamic behavior addition
- Façade: Simplified interface
- Flyweight: Shared state optimization
- Proxy: Access control
Adapter
How can we make incompatible interfaces work together? For example, a legacy system that needs to interact with a new system or a third-party library that needs to be integrated into an existing codebase such as a payment gateway.
- Need to integrate legacy code with new systems
- Must work with third-party libraries that have incompatible interfaces
- Want to reuse existing functionality with a different interface
Bridge
How can we separate an abstraction from its implementation so both can vary independently? For example, a drawing application that can render shapes in different ways or a database driver that can connect to multiple databases.
- Need to extend a class in several independent dimensions
- Want to avoid a permanent binding between interface and implementation
- Implementation details should be hidden from the client
Composite
How can we treat individual objects and compositions of objects uniformly? For example, a file system that can contain files and directories or a graphical user interface that can contain components and containers.
- Need to represent part-whole hierarchies
- Clients should treat individual objects and compositions identically
- Want to create tree-like structures of objects
Decorator
How can we add new behaviors to objects dynamically without altering their structure? For example, a text editor that can add spell-checking, a game that can add new abilities to characters, or a food ordering system that can add toppings or sides.
- Need to extend object functionality at runtime
- Want to avoid subclass explosion
- Responsibilities should be added flexibly
Façade
How can we provide a simplified interface to a complex subsystem? For example, a computer that can boot up with a single button press or a library that can handle multiple file formats.
- Need to provide a simple interface to a complex system
- Want to reduce dependencies between client and subsystem
- System complexity should be hidden from clients
Flyweight
How can we minimize memory usage by sharing common parts of state between multiple objects? For example, a text editor that reuses font objects or a game that reuses sprite and three-dimensional objects.
- Need to support large numbers of similar objects
- Memory is a constraint
- Object state can be divided into shared and unique parts
Proxy
How can we control access to an object by providing a surrogate or placeholder? For example, a remote proxy that can access objects in a different address space or a virtual proxy that can load objects on demand such as lazy-loading images.
- Need to control access to an object
- Want to add functionality when accessing an object
- Object creation and access should be managed
Exercise 1
Design class diagrams that use at least three structural design patterns.
Explore the 10 SOLID + GRASP guide to learn more about the SOLID principles and GRASP patterns.
Structure
adapter.patdecorator.patproxy.patstructural.vpp
Resources
- Visual Paradigm: Visual Paradigm tutorials (opens in a new tab), Using design pattern (opens in a new tab)
- Refactoring.Guru (opens in a new tab), SourceMaking (opens in a new tab), Patterns.dev (opens in a new tab)
- Gangs of Four (GoF) design patterns (opens in a new tab)
- GoF design patterns examples (opens in a new tab)
- Deceptive Patterns (opens in a new tab)
- Code refactoring using IntelliJ IDEA (opens in a new tab)
- Code refactoring (opens in a new tab), Principle of least astonishment (opens in a new tab), Law of Demeter (opens in a new tab), Separation of concerns (opens in a new tab), Aspect-oriented programming (opens in a new tab), POSA (opens in a new tab)