[OOP]Common signs and symptoms of a rotting design
Nguồn: Click here
- Rigidity: The code is difficult to change, and any small change in one part of the code can result in widespread impacts and breakages in other parts of the code.
- Fragility: The code is prone to breaking when changes are made, even if those changes are unrelated to the functionality being changed.
- Immobility: The code is difficult to reuse, and it is difficult to extract components for use in other parts of the system or in other projects.
- Viscosity: It is difficult to make changes that align with good design principles, because the easiest and quickest way to make a change is not necessarily the best way to make the change.
- Needless complexity: The code is more complicated than it needs to be, with extra features, classes, or layers that do not contribute to the overall functionality.
- Needless repetition: There is a lot of repeated code or functionality, and changes need to be made in multiple places throughout the codebase.
- Poor organization: The codebase is difficult to navigate, with no clear separation of concerns, and no clear hierarchy of components or modules.
- Coupling and cohesion problems: The code has a high degree of coupling between components, making it difficult to isolate and test individual pieces of functionality. Additionally, components may be poorly organized or designed, leading to low cohesion.
These signs and symptoms are often indicative of a rotting design, and they can make it difficult to maintain, scale, and evolve the software over time. It's important to keep an eye out for these problems and to proactively address them to keep the design of the software healthy and sustainable.
1. Example of Rigidity
Suppose we have a system for processing customer orders. The system is divided into three main classes: Order, OrderProcessor, and Customer. The Order class contains information about the order, such as the order number, customer name, and product details. The OrderProcessor class handles processing the order, such as validating payment and shipping the product. The Customer class contains information about the customer, such as their name and shipping address.
Now, suppose we want to add a new feature to the system where customers can specify a gift message to be included with the order. We decide to add a new field to the Order class called giftMessage. However, when we make this change, we realize that the OrderProcessor class is tightly coupled to the Order class, and it relies on the structure of the Order class for its validation and processing logic. As a result, we need to make changes to the OrderProcessor class as well to accommodate the new giftMessage field.
Furthermore, we realize that the Customer class is also tightly coupled to the Order class, and it relies on the structure of the Order class to retrieve the customer name and shipping address. As a result, we need to make changes to the Customer class as well to accommodate the new giftMessage field.
This example illustrates the rigidity of the system's design. Even a small change to the Order class has resulted in widespread impacts and breakages in other parts of the code. This can make it difficult to make changes to the system, and it can slow down the development process. To avoid rigidity, it's important to design software systems that are modular, flexible, and easy to change.
2. Example of Fragility (mong manh)
Suppose we have a system for processing employee payrolls. The system is divided into two main classes: Payroll and Employee. The Payroll class handles calculating employee salaries, taxes, and deductions. The Employee class contains information about the employee, such as their name, address, and salary.
Now, suppose we want to add a new feature to the system where we can record the employee's phone number. We decide to add a new field to the Employee class called phoneNumber. However, when we make this change, we realize that the Payroll class is tightly coupled to the Employee class, and it relies on the structure of the Employee class for its calculations. As a result, we need to make changes to the Payroll class as well to accommodate the new phoneNumber field.
However, when we make these changes, we realize that the Payroll class is also used by another system that we had not considered. This other system uses the Payroll class in a slightly different way, and the changes we made for the new phoneNumber field have unintended consequences in this other system. As a result, the other system breaks and we need to spend time fixing it.
This example illustrates the fragility of the system's design. Even a seemingly innocuous change to the Employee class has resulted in unexpected breakages in other parts of the code. This can make it difficult to maintain and evolve the system over time, and it can lead to time-consuming and error-prone bug fixes. To avoid fragility, it's important to design software systems that are loosely coupled and that have clearly defined boundaries between components.
3. Example of Immobility:
Suppose we have a system that reads data from a database and presents it to the user through a web interface. The system is divided into two main classes: DatabaseReader and WebInterface. The DatabaseReader class handles querying the database and retrieving the data, and the WebInterface class handles displaying the data to the user.
Now, suppose we want to use the DatabaseReader class in a different system that has a different user interface. However, when we try to use the DatabaseReader class in the new system, we realize that it is tightly coupled to the WebInterface class, and it relies on the structure of the WebInterface class for its data display logic. As a result, we cannot easily reuse the DatabaseReader class in the new system without making significant changes to it.
Furthermore, we realize that the DatabaseReader class is also tightly coupled to the specific database schema that we are using in the original system. As a result, we cannot easily reuse the DatabaseReader class in a different system that has a different database schema without making significant changes to it.
This example illustrates the immobility of the system's design. The code is tightly coupled to other parts of the system and to specific implementation details, which makes it difficult to reuse the code in other parts of the system or in other systems altogether. To avoid immobility, it's important to design software systems that are modular, flexible, and have well-defined interfaces that can be easily reused in other parts of the system or in other systems altogether.
4. Example of Viscosity:
Suppose we have a system that processes user input from a web form and saves it to a database. The system is divided into two main classes: FormProcessor and DatabaseSaver. The FormProcessor class handles validating the user input, and the DatabaseSaver class handles saving the data to the database.
Now, suppose we want to add a new feature to the system where we can send an email to the user after their data has been saved to the database. We decide to add a new class called EmailSender to handle this functionality. However, when we try to add the EmailSender class to the system, we realize that it is more difficult to use the correct, well-designed solution than to use a quick-and-dirty hack.
The correct solution would be to refactor the system to use a messaging system, such as a message queue, to handle sending the email asynchronously. However, this would require significant changes to the system's architecture, and it would take a lot of time and effort to implement. Instead, it is easier to simply add the email-sending code to the FormProcessor class, which already has access to the user's email address.
However, by doing so, we are violating the Single Responsibility Principle (SRP), as the FormProcessor class is now responsible for both validating the user input and sending the email. This can make the code harder to maintain and evolve over time.
This example illustrates the viscosity of the system's design. It is more difficult to use the correct, well-designed solution than to use a quick-and-dirty hack, which can result in violated design principles and a harder-to-maintain codebase. To avoid viscosity, it's important to design software systems that are flexible, modular, and easy to change, so that it's always easier to use the correct solution than to use a hack.
5. Example of Needless complexity:
Suppose we have a system that manages a library of books. The system is divided into several classes, including Book, Library, and LibraryCatalog. The Book class represents a single book, with properties such as title, author, and ISBN. The Library class represents a collection of books, with methods for adding and removing books, and the LibraryCatalog class represents a catalog of libraries, with methods for searching for libraries by location, name, or other criteria.
Now, suppose we want to add a new feature to the system where we can track the number of pages in each book. We decide to add a new property to the Book class called numPages. However, when we try to add the new property, we realize that the code is more complicated than it needs to be.
The Book class is already responsible for representing a single book, so it makes sense to add the numPages property to this class. However, because the Library class is responsible for managing a collection of books, we also need to update the Library class to handle the new property. This involves updating the addBook() and removeBook() methods to include the numPages property, as well as updating the LibraryCatalog class to handle the new property.
While this approach works, it adds needless complexity to the codebase. We are modifying multiple classes to handle a single new property, which violates the Single Responsibility Principle (SRP). Additionally, it makes the code harder to understand and maintain over time.
To avoid needless complexity, it's important to design software systems that are simple, easy to understand, and easy to change. This means keeping each class focused on a single responsibility and avoiding unnecessary dependencies between classes.
6. Example of needless repetition:
Suppose we have a system that manages a collection of products, and we need to implement a feature that calculates the total price of all products in the collection. We start by creating a Product class with properties such as name, price, and quantity. We then create a ProductCollection class that represents a collection of products, with methods for adding and removing products.
To calculate the total price of all products, we create a new method called getTotalPrice() in the ProductCollection class. Here's what the initial implementation might look like:
public class ProductCollection {
private List<Product> products;
public double getTotalPrice() {
double totalPrice = 0.0;
for (Product product : products) {
totalPrice += product.getPrice() * product.getQuantity();
}
return totalPrice;
}
// other methods for adding and removing products...
}
This implementation works fine, but we soon realize that we need to calculate the total price of products in other parts of the system as well. For example, we might need to calculate the total price of products in a shopping cart, or in an order history.
To handle this, we copy the getTotalPrice() method to other classes in the system and modify it as needed. Here's an example of what the modified method might look like in a shopping cart:
public class ShoppingCart {
private List<Product> products;
public double getTotalPrice() {
double totalPrice = 0.0;
for (Product product : products) {
totalPrice += product.getPrice() * product.getQuantity();
}
return totalPrice;
}
// other methods for adding and removing products...
}
As we can see, the same code is repeated in multiple places throughout the system, which violates the DRY (Don't Repeat Yourself) principle. If we need to change the way the total price is calculated in the future, we'll need to make the same change in multiple places, which increases the risk of introducing bugs.
To avoid needless repetition, it's important to create reusable code components that can be shared across the system. This might involve creating utility classes, defining common interfaces, or using design patterns to encapsulate common behaviors. By doing so, we can reduce the amount of code duplication in the system and make it easier to maintain and evolve over time.
7. Example of poor organization:
Suppose we have a system that manages a collection of users, and we need to implement a feature that retrieves a list of users from a database. We start by creating a User class with properties such as id, name, and email. We then create a UserDao class that handles the database interactions, with methods for retrieving and saving users.
To retrieve a list of users, we create a new method called getUsers() in the UserDao class. Here's what the initial implementation might look like:
public class UserDao {
private Connection connection;
public UserDao(Connection connection) {
this.connection = connection;
}
public List<User> getUsers() {
List<User> users = new ArrayList<>();
try {
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM users");
while (resultSet.next()) {
User user = new User(resultSet.getInt("id"), resultSet.getString("name"), resultSet.getString("email"));
users.add(user);
}
resultSet.close();
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
return users;
}
// other methods for saving users...
}
This implementation works fine, but as the system grows, the UserDao class becomes more complex and harder to navigate. To make matters worse, other parts of the system start to depend on the UserDao class for database access, which creates a tight coupling between different components.
To improve the organization of the code, we can introduce a new layer of abstraction that separates the database access from the application logic. This might involve creating a UserService class that handles the business logic for managing users, and a UserRepository interface that defines the database interactions. Here's an example of what the modified code might look like:
public interface UserRepository {
List<User> getUsers();
void saveUser(User user);
}
public class JdbcUserRepository implements UserRepository {
private Connection connection;
public JdbcUserRepository(Connection connection) {
this.connection = connection;
}
@Override
public List<User> getUsers() {
List<User> users = new ArrayList<>();
try {
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT * FROM users");
while (resultSet.next()) {
User user = new User(resultSet.getInt("id"), resultSet.getString("name"), resultSet.getString("email"));
users.add(user);
}
resultSet.close();
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
return users;
}
@Override
public void saveUser(User user) {
// implementation for saving users...
}
}
public class UserService {
private UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
public List<User> getUsers() {
return repository.getUsers();
}
public void saveUser(User user) {
repository.saveUser(user);
}
// other methods for managing users...
}
As we can see, the code has been reorganized into smaller, more focused components that are easier to understand and maintain. The UserRepository interface defines the database interactions, which allows us to easily swap out different implementations if needed. The JdbcUserRepository class handles the database access, and the UserService class handles the business logic for managing users. By separating the concerns and creating a clear boundary.
Nguồn: chatGPT
Không có nhận xét nào