Chapter #6 Working with Design Patterns & Anti-patterns — Software Design and Architecture Specialization University of Alberta
The Model-View-Controller (MVC) design pattern is widely utilized for developing user interfaces, effectively dividing the responsibilities of a system into three components: the model, the view, and the controller. The model, representing the back end, encapsulates the data, state, and logic of the system, ensuring self-containment. The view, serving as the front end or the presentation layer, allows users to visualize and interact with the model. It is responsible for presenting the model to users in an expected manner. Multiple views may present different facets of the model. The controller interprets user requests from the view and orchestrates changes both in the view and the model.
The observer design pattern is instrumental in facilitating communication between the model and the view, ensuring that the view is updated upon changes in the model. In this pattern, the view acts as an observer and is notified when the state of the model changes. The controller serves as the intermediary between the view and the model, enabling user interactions to affect the underlying data without directly manipulating the model. By adhering to the principles of the MVC pattern, the system achieves loose coupling between the views and the model, maintaining a clear distinction between presentation, data management, and user interaction.
For instance, in the context of a grocery store interface, the MVC pattern can be employed to allow cashiers to enter orders, display the item list, scan barcodes, and view the total bill. Cashiers can make necessary adjustments, while customers can access the displayed information. By implementing the MVC pattern, the grocery store interface can effectively manage the complex interactions between the various components, ensuring a robust and maintainable system design.
Design Principles Underlying Design Patterns
All design patterns follow a basic set of design principles. This next lesson will examine some of these underlying design principles. These principles address issues such as flexibility and reusability.
Open/Closed Principle
The Open/Closed Principle (OCP) is a fundamental principle in object-oriented programming that emphasizes the idea that classes should be open for extension but closed to modification. When a class is considered “closed,” it signifies that it has been thoroughly tested, encapsulates all its attributes and behaviors, and is stable within the system, meaning it does not cause any harm or disruptions.
While the term “closed” may suggest an immutability that prohibits changes, it actually indicates that the class has reached a point in development where most design decisions have been finalized, and the majority of the system has been implemented. Although closed classes should not be altered, they should still be fixed if bugs or unexpected behaviors arise.
The “open” aspect of the principle comes into play when the system needs to be extended or have additional features added. There are two primary ways to adhere to the open principle. First, inheritance can be utilized to extend a closed class by creating subclasses that inherit the original functions while adding extra features. The “final” keyword can be used to limit a class’s extension. Second, an abstract class or interface can enforce the open/closed principle through polymorphism, allowing for various implementations while preserving the integrity of the original structure.
By adhering to the open/closed principle, stable components of a system are separated from the varying parts, enabling the addition of new features without disrupting the functioning elements. This practice facilitates the evolution of a system while minimizing the risk of unwanted side effects. While it may not always be feasible to strictly adhere to the principle, it serves as a guiding principle for designing robust and extensible software systems.
All design patterns incorporate the open/closed principle in some manner, emphasizing the idea that certain parts of a system should be extendable through inheritance or interfaces. Adhering to this principle supports the stability and maintainability of the software over time, enabling seamless adaptation and expansion as needed.
Dependency Inversion Principle
The Dependency Inversion Principle (DIP) is a crucial concept in software design that aims to address issues related to software dependency or coupling, which refers to how much components in a system rely on each other. High coupling indicates a high degree of reliance, while low coupling signifies a lower level of dependency. The level of dependency significantly influences how easily changes can be made to a software system. When different parts of a system are highly dependent on each other, substituting one class or resource for another becomes challenging or even impossible.
The Dependency Inversion Principle aims to mitigate these dependency issues by promoting robustness and flexibility in systems. It asserts that high-level modules should depend on high-level generalizations rather than low-level details. This means that client classes should rely on interfaces or abstract classes, rather than specific concrete resources, and that concrete resources should have their behaviors generalized into interfaces or abstract classes. Interfaces and abstract classes represent high-level resources that define a general set of behaviors, while concrete classes represent low-level resources that provide the implementation for these behaviors.
This principle forms the foundation of all the design patterns covered in this course, emphasizing the importance of designing systems with low coupling and high cohesion.
In a system with low-level dependency, client classes directly reference concrete classes, creating a tight coupling between subsystems. In contrast, a system with high-level dependency ensures that client classes depend on high-level generalizations, offering a level of indirection by allowing behaviors to be invoked through interfaces or abstract classes. This approach prevents client classes from being tightly coupled to specific concrete implementations, instead relying on expected behaviors.
By adhering to the Dependency Inversion Principle, the overall system architecture begins to resemble the design patterns explored earlier in this course. It encourages the use of method calls through a level of indirection, insulating parts of the system from specific implementation details. This best practice involves programming to generalizations, such as interfaces and abstract classes, rather than directly to concrete classes.
While implementing the Dependency Inversion Principle may require additional effort, especially in larger systems, it is a worthwhile endeavor. Systems with heavy coupling make changes and maintenance difficult, while DIP ensures a more robust and adaptable software solution by decoupling high-level modules from low-level details and promoting high-level dependency over low-level concrete dependency.
Composing Object Principle
The Composing Objects Principle serves as a solution to the common problem of tight coupling in system design. While inheritance is valuable for achieving code reuse, it often results in tightly coupled superclasses and subclasses, potentially causing issues when changes are made to the superclass affecting the entire inheritance hierarchy. The Composing Objects Principle addresses this challenge by promoting code reuse through aggregation rather than inheritance. This approach encourages a design where classes are composed of other classes, fostering an “arms-length” relationship that reduces the level of dependency and coupling between components.
Design patterns such as the composite design pattern and the decorator design pattern embody the Composing Objects Principle, enabling the construction of complex objects at runtime through the composition of multiple simpler objects. By delegating certain tasks to other objects, these patterns facilitate the creation of an overall behavior that is a sum of the behaviors of the composed objects. The flexibility provided by this principle allows for dynamic changes in the behavior of objects at runtime, which is not possible with inheritance.
While the Composing Objects Principle offers advantages such as flexibility and reduced coupling, it comes with the requirement of providing implementations for all behaviors, without the benefit of code sharing inherent in inheritance. Similar implementations across multiple classes might lead to redundant code and increased resource consumption. In contrast, inheritance allows the sharing of common implementations within the superclass, reducing the need for redundant code in each subclass.
In practice, it is crucial to strike a balance between inheritance and composition, considering the specific needs of the system. While composition provides more flexibility and lower coupling, it requires more careful design and implementation of behaviors for each class. In contrast, inheritance facilitates code reuse and simplifies the implementation process but can lead to tight coupling and challenges when making changes to the superclass.
Overall, understanding the strengths and limitations of both inheritance and composition is essential in making informed design choices that best fit the requirements and constraints of a particular system. By carefully considering the trade-offs between these two principles, software designers can create systems that are both flexible and maintainable, ensuring efficient and effective long-term development and maintenance.
Interface Segregation Principle
The Interface Segregation Principle (ISP) is a crucial concept in software design that addresses the issue of having overly large interfaces that force clients to depend on methods they do not use. While programming to interfaces is a best practice for reducing dependency on concrete classes and facilitating changes in the system, a single large interface with all methods can lead to unnecessary implementation of methods that are not relevant to a particular client. This can result in code bloat and unnecessary coupling, hindering the flexibility and maintainability of the system.
To adhere to the Interface Segregation Principle, it is essential to avoid creating monolithic interfaces that encompass all possible methods for all clients. Instead, interfaces should be split into smaller, more focused generalizations, each catering to the specific requirements of a subset of clients. By doing so, classes are not burdened with implementing methods they do not use, leading to more precise interfaces and reducing unnecessary dependencies.
For instance, in the context of a grocery store checkout system that offers both automated machine and human cashier payment options, a poor interface design might involve a single interface containing all methods for both payment options. This would result in the implementation of irrelevant methods for each type of payment, causing unnecessary coupling and potential issues during system changes or updates.
A good interface design, on the other hand, would apply the Interface Segregation Principle by segregating the larger interface into two smaller interfaces, each tailored to the specific behaviors of the automated machine and the human cashier. This approach ensures that each interface accurately represents the expected behaviors of its corresponding client, allowing for more precise implementation and minimizing unnecessary dependencies.
By adhering to the Interface Segregation Principle, software designers can create more maintainable and flexible systems that are not burdened with unnecessary code or dependencies. This practice encourages a modular and focused approach to interface design, enabling a clearer representation of the expected behaviors for each client and promoting a more efficient and manageable software development process.
Principle of Least Knowledge
public class Friend {
public void N() {
System.out.println("Method N invoked");
}
}
public class O {
public Friend I = new Friend();
public void M() {
this.I.N();
System.out.println("Method M invoked");
}
}
The Principle of Least Knowledge, also known as the Law of Demeter, is a crucial concept in software design that emphasizes reducing the coupling between classes by limiting their knowledge and interaction with other classes. By restricting the awareness of a class to a narrow set of related classes, the complexity of the system is effectively managed, leading to a more maintainable and comprehensible codebase.
The Law of Demeter defines a set of rules that govern how method calls should be made, aiming to minimize the dependency between objects and reduce the risk of tightly coupled designs. The four rules of the Law of Demeter guide the permissible method calls within an object, ensuring that the interactions between classes are structured in a way that promotes loose coupling and encapsulation:
1. A method M within an object O can call any other method encapsulated within O itself. This means that methods within the same class can interact freely without violating the Law of Demeter.
2. A method M can call the methods of any parameter P. As the method parameter is considered local to the method, the methods of the class associated with the parameter can be accessed without violating the principle.
3. A method M can call a method N of an object I if I is instantiated within M. This allows a method to call methods of an object it creates, treating the object as a local component.
4. In addition to local objects, any method M in object O can invoke methods of any type of object that is a direct component of O. This means that a method within a class can call methods of other classes linked to it through instance variables, promoting a limited but permissible level of interaction between related classes.
By adhering to the Principle of Least Knowledge, software designers can develop systems with reduced coupling, increased encapsulation, and improved maintainability. This principle helps manage the complexity of a system by encouraging a more focused and restricted interaction between classes, leading to a more comprehensible and efficient software design. Following the Law of Demeter ensures that method calls are structured in a way that maintains a clear and manageable relationship between different classes, contributing to the overall robustness and maintainability of the software system.
Anti-Patterns and Code Smells
Refactoring is the process of making incremental changes to the internal structure of code to improve its maintainability and readability while preserving its external behavior. It involves making small, focused adjustments to the codebase and frequently testing to ensure that these changes do not alter the code’s behavior. Refactoring is ideally performed as new features are added, not when the code is complete, as it saves time and makes the addition of new features more straightforward.
Code smells, also known as anti-patterns, are recurring patterns of bad code identified by Martin Fowler in his book “Refactoring.” They are indicative of suboptimal code and can be addressed through refactoring. Code smells help identify areas where code quality can be improved, leading to more maintainable and comprehensible software.
Here are some common code smells discussed in Fowler’s book:
- Comments: Excessive comments in code can indicate a problem. If there are too few comments, it may be challenging for developers to understand the code’s purpose and functionality. Conversely, too many comments can be a sign of complex design or code being used to cover up bad code.
- Duplicate Code: Duplicate code occurs when similar blocks of code appear in multiple places within the software. This can lead to maintenance challenges, as changes need to be made in multiple locations, increasing the risk of missing an update.
- Long Method: Long methods can indicate complexity or excessive code in a single method. While there is no strict rule for method length, very long methods can be challenging to understand and maintain. Context and programming language specifics should be considered.
- Large Class: Large classes, often referred to as “God classes,” are classes that grow over time, accumulating numerous responsibilities. They become difficult to maintain, and extensive comments are required to document the code’s functionalities. Classes should have a specific purpose and be kept cohesive.
- Data Class: Data classes are the opposite of large classes, with minimal functionality and mostly data. They lack meaningful abstractions and may not be necessary. Consider whether some behaviors could be moved into these classes to make the code more structured and useful.
- Long Parameter List: Having a method with a long parameter list can make it challenging to use and increase the likelihood of errors. To address this, developers often resort to global variables, which come with their own set of issues and should generally be avoided. Long parameter lists require extensive comments to explain each parameter’s purpose, which is another code smell as excessive commenting can obscure code clarity. When changes are needed in the code, especially changes that affect many classes, it may indicate a lack of encapsulation, and changes might be scattered throughout the codebase. A better approach to address long parameter lists is to use parameter objects, which encapsulate related parameters into a single object. This approach can provide context and simplify method calls.
- Divergent Change: Divergent change occurs when a class needs to be modified in numerous ways and for various reasons, indicating a lack of cohesive responsibility. It is often associated with the large class code smell, where a single class takes on multiple, unrelated responsibilities. To resolve divergent change, classes should have a single, specific purpose, and responsibilities should be separated into different classes. Separation of concerns is a key principle for addressing both the large class and divergent change code smells.
- Shotgun Surgery: Shotgun surgery is a common code smell that happens when changes in code require modifications across multiple code locations, increasing the chances of overlooking changes or introducing errors. While modular code can help alleviate this issue, some changes, like updates to copyright statements or licensing, may necessitate widespread modifications.Resolving the shotgun surgery smell often involves reorganizing methods to consolidate related functionalities into a smaller number of classes. However, this should be done thoughtfully to avoid creating a large class code smell. Identifying related methods and finding a better organization strategy can simplify code maintenance.
- Feature Envy: This code smell suggests that a method in one class is overly focused on the details of another class, potentially indicating a need for a reevaluation of class responsibilities and the reorganization of code to improve cohesion and reduce coupling.
- Inappropriate Intimacy: When two classes are tightly coupled through bidirectional communication, it can lead to a lack of encapsulation and an increase in the complexity of the code. Encouraging one-way communication and separating concerns between classes can help address this issue.
- Message Chains: Lengthy message chains in the code can make the codebase rigid, complex, and challenging to test independently. Breaking down these chains and reorganizing the dependencies can improve the design and ensure compliance with the Law of Demeter.
- Primitive Obsession: Overreliance on primitive types can lead to a lack of proper abstraction and hinder the readability and maintainability of the code. By introducing suitable classes and encapsulating behaviors, developers can address this issue and improve the overall design quality.
- Switch Statements: Excessive usage of switch statements can scatter logic throughout the code, making it difficult to maintain and update. Replacing switch statements with polymorphism or other design patterns can enhance the flexibility and extensibility of the code.
- Speculative Generality: Unnecessary abstractions or superfluous code that is not immediately required can introduce unnecessary complexity and overhead. Adopting an Agile approach and focusing on the current requirements can help developers avoid over-engineering and maintain a more streamlined and efficient development process.
- Refused Request: This code smell occurs when a subclass inherits behavior from a superclass that it does not need or use. This can indicate issues with the inheritance hierarchy and may call for a reassessment of the class structure to ensure that classes maintain clear and distinct responsibilities.
Ibrahim Can Erdoğan