When we talked about DDD before, we mentioned that many of today's object-oriented languages are implemented as process-oriented. Here's how to think about object-oriented design from a coding design perspective. In fact, as I am sorting out the design patterns, are some clichéd things, but often in practice, these clichés will be "regurgitated", there will always be a kind of often see the feeling of always new.
object-oriented thinking
In fact, if you want to practice DDD, you will inevitably have to perform OOA and OOD, and the main purpose here is to sort out some design guidelines and ideas of OOD.
abstraction
The core technology of object-oriented is abstraction, compared to the process-oriented data structure based on step-by-step command development thinking, object-oriented is a human thinking mode to think, which, the commonality of things, the essence of the extraction is abstraction.
Let's say, people as an entity in real life, we can see very intuitively, people will have gender, age, height, weight and so on some of the public attributes, in addition to this, people will also use language to communicate, will eat, will drive and a series of behaviors, and so we summarize, people are a kind of gender, age, weight... ...and can talk, sleep..., and this summary, which applies to almost all human beings, so the concept of a human being is generalized. Through this process it becomes clear that our thinking process is that we first have a vague object class, and then we extract the common parts in that object class for rectification, and finally the whole vague object class is materialized, and the whole process is induction and summary. This is abstraction.
Corresponding to programming, OOP needs to extract, then summarize, and finally clearly define the characteristics of some of the subjects of our programs and businesses, which is the first step of object orientation, i.e., generalize the things that exist in the actual scenario or in the requirements, extract the public parts, and perform the definition of types. The result of the abstraction is the type, or class.
boyfriend
After we abstract a definition, we can arbitrarily produce a concrete instance based on this definition, this is the Class in programming and the concrete new Object, the object is based on the instantiation of the type of the abstraction, we define the characteristics and behavior of human beings (i.e., write a Class), then we can be based on the Class, the output of a concrete individual to ( new out of an object), just as each of us lives in the environment of the Earth to communicate. Programs are the same, in the object-oriented program world, the object is the protagonist, the program is a runtime, obviously not by the abstract class to work, but by the abstract class materialized in a concrete object to communicate, communication.
There are several object-oriented realizations that need to be cared about:
- Everything is an object.: In a program, any transaction is an object, and an object can be thought of as a peculiar variable that can be stored, communicated with, and operated on in its own right from itself, and you can always abstract conceptual components from the problem to be solved, and then represent it as an object in the program.
- Programs are collections of objects that send messages to tell each other what needs to be donePrograms are like natural environments, a person, a pig, a tree, an axe, are all concrete objects in this environment, objects communicate with each other and operate to accomplish something, this is a process in the program, to request to call a method of an object, you need to send a message to the object.
- Each object has its own storage space for other objects: A person will have a cell phone, a person is an object, a cell phone is also an object, and a cell phone can be a part of a person's object, or new types of objects can be made by encapsulating existing objects. So, although the concept of an object is very simple, it can reach an arbitrarily high level of complexity in a program.
- Each object has its own type: In common parlance, any object is an instance of a "Class", and every object must have an abstraction on which it depends.
- All objects of the same class receive the same message: This is actually a different way of putting it, as you will soon understand. Since an object of type Circle is also an object of type Shape, a circle is perfectly capable of receiving messages sent to Shape. This means that the program code can be made to command Shape in a unified way, so that it automatically controls all objects that fit the description of Shape, including, naturally, Circles. This feature is called "replaceability" of objects and is one of the most important concepts of OOP.
Process and object thinking
Simply put, process thinking is data structures plus operations; object thinking is a whole, containing both data structures and operations, i.e., attributes and behaviors in object-oriented.
object-oriented design principles
On the road to object-oriented design and coding, many well-known predecessors combined their own practice and knowledge of highly abstract overview of the guiding ideological significance of the design principles. Each of these principles is meaningful in its own right, but it is important to note that, like the database paradigm, it is a guideline, not a "code" that needs to be followed to the letter.
SRP - Single Responsibility Principle (SRP)
Official definition of single duty:
A class should have only one cause for its changes
The cause of the change is what is called a "responsibility". If a class has more than one cause of change, then it means that the class has more than one responsibility, and furthermore, more than one responsibility is coupled together. This can lead to interactions between responsibilities, where a change in one responsibility can affect the implementation of other responsibilities, or even cause other responsibilities to change along with it, which is a very fragile design.
This principle seems to be the simplest and best understood, but in reality it is very difficult to fully realize, the difficulty lies in how to distinguish "responsibilities". There is no standard quantification of what counts as a duty, how granular the duty is, how it is refined, etc., for example:
public class FileUtil { public void readFile(String filePath) { // Code to read the file } public void writeFile(String filePath, String content) { // Code to write to a file } public void encryptFile(String filePath) { // Code for encrypting files } public void decryptFile(String filePath) { // Code to decrypt the file } }
Our development habit is often to define a Util based on an object or concept + operation, this Util will be used as a public processing code to help us deal with the file-related operations in the system. However, strictly speaking, this is against the single-responsibility principle, because if you need to modify the file reading logic or encryption algorithm in the future, it may affect other functions, which violates the single-responsibility principle. If you want to strictly adhere to the single duty, it should be changed to:
// Classes responsible for file reading public class FileReader { public void readFile(String filePath) { // Code to read the file } } // Classes responsible for file writes public class FileWriter { public void writeFile(String filePath, String content) { // Code to write to a file } } // Classes responsible for file encryption public class FileEncryptor { public void encryptFile(String filePath) { // Code for encrypting files } public void decryptFile(String filePath) { // Code to decrypt the file } }
Now, each class has only one responsibility:
-
FileReader
class is only responsible for reading files. -
FileWriter
class is only responsible for writing to the file. -
FileEncryptor
class is responsible for the encryption and decryption of files.
In this way, there is only one cause of change for each class, in line with the single responsibility principle. If you need to modify the file reading logic, you only need to modify theFileReader
class; if you need to modify the encryption algorithm, just modify theFileEncryptor
class without affecting other classes. But in practice, if you are so strict as to refine each operation into a class, you will most likely be called SB.
Therefore, in actual development, this principle is most likely to be violated, because the degree of control is very difficult. What we can do is based on the actual situation of the project to control the granularity of the operation of this "responsibility", if the project for the operation of the file, change and involve a wide range of strict adherence to a single responsibility will bring good scalability and maintainability, but if the project is very simple, based on the public Util and the same for many years, there is no need for single responsibility transformation. But if the project is very simple, based on the public Util and unchanged for many years, then there is no need to carry out a single responsibility for the transformation of a single project a Util is enough.
OCP - Open-Closed Principle (OCP)
Classes should be open to extensions and closed to modifications.
The open-closed principle requires that the behavior of the class is extensible, and that it is extended without modifying the existing code, and without having to change the existing source or binary code.
This may seem contradictory, but this refers to the actual coding process, after all, this is a guiding principle, standing in the guiding principle point of view, it is not necessarily contradictory; the key to realizing the principle of open and closed lies in the reasonable abstraction, separation of the changing and non-changing parts, for the changing part of the reserved extensible way, for example, hook methods or dynamic combination of objects, and so on.
This principle also seems very simple. But in fact, it is almost impossible, and unnecessary, for a system to do all of its work to comply with the principle of openness and closure. Moderate abstraction can increase the flexibility of a system, making it scalable and maintainable, but overdoing it can greatly increase the complexity of a system. It should be sufficient to apply the principle of openness and closure where changes are needed, without having to use it everywhere, thus falling into overdesign.
LSP - Liskov Substitution Principle (LSP)
Subclass objects should be able to replace their parent class objects without affecting the behavior of the program.
Simply put, a subclass can replace the parent class in the program without affecting the use of the program, which is a kind of object-oriented polymorphism based on the use of. It avoids some hidden errors in the use of polymorphism.
public abstract class Account { private String accountNumber; private double balance; public Account(String accountNumber, double balance) { this.accountNumber = accountNumber; this.balance = balance; } public String getAccountNumber() { return accountNumber; } public double getBalance() { return balance; } public void deposit(double amount) { balance += amount; ("Deposited: " + amount + ", New Balance: " + balance); } public abstract void withdraw(double amount); } //Derived Classes of Accounts public class CheckingAccount extends Account { private double overdraftLimit; public CheckingAccount(String accountNumber, double balance, double overdraftLimit) { super(accountNumber, balance); this.overdraftLimit = overdraftLimit; } @Override public void withdraw(double amount) { if (amount <= balance + overdraftLimit) { balance -= amount; ("Withdrew: " + amount + ", New Balance: " + balance); } else { ("Insufficient funds for withdrawal: " + amount); } } public double getOverdraftLimit() { return overdraftLimit; } } //Scenarios for the use of Richter's replacement public class Bank { private List<Account> accounts = new ArrayList<>(); public void addAccount(Account account) { (account); } public void processTransactions() { for (Account account : accounts) { (100); // Suppose each account tries to take out $100 (50); // Assuming $50 is deposited in each account } } } public class Main { public static void main(String[] args) { Bank bank = new Bank(); (new Account("123456", 1000)); (new CheckingAccount("789012", 500, 300)); (); } }
The key point of this sample, which conforms to the principle of Richter's substitution, is that neither the ordinaryAccount
Object orCheckingAccount
objects, all of which can be used by theAccount
types of variables are handled without any special logic to distinguish between them. This is where the principle of Richter substitution comes into play:CheckingAccount
Objects can be seamlessly replacedAccount
object without destroying theBank
type of behavior.
In fact, when a class inherits from another class, the subclass has properties and operations that can be inherited from the parent class. Theoretically, using a subtype to replace the parent should not cause an error in the program that originally used the parent.
However, there are certain situations where problems can arise. For example, if a subtype overrides some of the methods of the parent type, or if a subtype modifies the value of some attributes of the parent type, then the program that originally used the parent type may have an error, because during the runtime, on the surface it appears that it is calling the methods of the parent type, and it needs the functionality implemented by the parent type's methods, but in reality the actual runtime calls the method implemented by the subtype override, which is not the same as the parent type's methods, thus leading to an error. The method is not the same as the parent method, which causes the error.
From another point of view, the Richter substitution principle is one of the main principles for realizing openness and closure. The principle of openness requires openness to extensions, and one means of implementing extensions is to use inheritance: the principle of Richter's substitution ensures that subtypes can correctly substitute for their parents, and that extensions can be implemented only if they can be correctly substituted; otherwise, they will be extended, and will also be incorrect.
DIP - Dependence Inversion Principle (DIP)
Higher level modules should not depend on lower level modules, both should depend on abstractions; abstractions should not depend on details, details should depend on abstractions
The principle of dependency inversion refers to the idea of relying on abstractions, not concrete classes. To achieve dependency inversion you should typically do the following.
- Higher level modules should not depend on the underlying module, both should depend on the abstraction.
- Abstractions should not depend on concrete implementations, concrete implementations should depend on abstractions
Many people think that when hierarchical calls are made, it is a typical misunderstanding that the higher level should call "the interface owned by the lower level". This is a typical misunderstanding. In fact, the high-level modules, which contain the processing of business functions and the selection of business strategies, should be reused, and it is the high-level modules that influence the concrete implementation of the bottom layer.
Therefore, this underlying interface should be proposed by the high level and then implemented by the bottom level. This means that the ownership of the underlying interface is in the high level module, and is therefore an inversion of ownership.
A more classic case would be the COLA that mentions data embalming layer design, for which you can see my
ISP-Interface Segregation Principle (ISP)
Clients should not be forced to rely on methods they don't use. A class should not implement interfaces that it does not need.
This principle is used to deal with interfaces that are "large", which usually have a lot of operation declarations and involve a lot of responsibilities. When a client uses such an interface, there are usually a lot of methods that he doesn't need, and these methods are a kind of interface pollution for the client, which is equivalent to forcing the user to look for the methods he needs in a pile of "junk methods". In fact, there is a little "interface of a single responsibility" meaning.
Therefore, such interfaces should be separated and should be separated into customer-specific interfaces according to different customer needs. Such interfaces contain only the operation declarations needed by the customer, which facilitates the use of the customer and avoids errors caused by misuse of the interface.
The way to separate interfaces, in addition to direct code separation, you can also use delegates to separate interfaces, and in languages that can support multiple inheritance, you can also use multiple inheritance to separate.
To experience this through a positive and negative example, suppose we have a banking system that includes two types of accounts: savings accounts (SavingsAccount) and checking accounts (CheckingAccount). SavingsAccounts provide the ability to make deposits and earn interest, while CheckingAccounts provide the ability to make deposits, withdrawals, and overdrafts.
Counterexample:
interface BankAccount { void deposit(double amount); void withdraw(double amount); double getInterestRate(); } class SavingsAccount implements BankAccount { private double balance; public SavingsAccount(double initialDeposit) { this.balance = initialDeposit; } @Override public void deposit(double amount) { balance += amount; } @Override public void withdraw(double amount) { // Overdrafts are not allowed on savings accounts if (amount <= balance) { balance -= amount; } else { throw new IllegalArgumentException("Insufficient funds"); } } @Override public double getInterestRate() { return 0.03; // Assuming an interest rate of 3% } } class CheckingAccount implements BankAccount { private double balance; private double overdraftLimit; public CheckingAccount(double initialDeposit, double overdraftLimit) { this.balance = initialDeposit; this.overdraftLimit = overdraftLimit; } @Override public void deposit(double amount) { balance += amount; } @Override public void withdraw(double amount) { if (amount <= balance + overdraftLimit) { balance -= amount; } else { throw new IllegalArgumentException("Insufficient funds for overdraft"); } } @Override public double getInterestRate() { // Checking accounts usually have no interest return 0.0; } }
here areBankAccount
interface forces all accounts to implement thegetInterestRate()
method, which violates the ISP because not all types of accounts have interest. If you want to comply with the ISP, you should separate the user public operations into two interfaces to further ensure that the interfaces are functionally homogeneous.
public interface Account { void deposit(double amount); void withdraw(double amount); } public interface InterestBearing { double getInterestRate(); } public class SavingsAccount implements Account, InterestBearing { private double balance; public SavingsAccount(double initialDeposit) { this.balance = initialDeposit; } @Override public void deposit(double amount) { balance += amount; } @Override public void withdraw(double amount) { if (amount <= balance) { balance -= amount; } else { throw new IllegalArgumentException("Insufficient funds"); } } @Override public double getInterestRate() { return 0.03; // Assuming an interest rate of 3% } } public class CheckingAccount implements Account { private double balance; private double overdraftLimit; public CheckingAccount(double initialDeposit, double overdraftLimit) { this.balance = initialDeposit; this.overdraftLimit = overdraftLimit; } @Override public void deposit(double amount) { balance += amount; } @Override public void withdraw(double amount) { if (amount <= balance + overdraftLimit) { balance -= amount; } else { throw new IllegalArgumentException("Insufficient funds for overdraft"); } } }
Account
interface contains operations common to all accounts, while theInterestBearing
The interface contains only interest-related operations.SavingsAccount
class implements theAccount
cap (a poem)InterestBearing
interface because it has interest earnings. And theCheckingAccount
class only implements theAccount
interface because it has no interest earnings. In this way, we avoid having to force theCheckingAccount
Realize that it doesn't need togetInterestRate()
method, thus following the principle of interface isolation.
LKP-Least Knowledge Principle (LKP)
also known asLaw of Demeter (LoD)By minimal knowledge, I mean, only talk to your friends.
This principle is used to guide us in the design of the system, we should try to minimize the interaction between objects, the object only talk to their friends, that is, only interact with their friends, so as to loosen the coupling between classes. By loosening the coupling between classes to reduce the interdependence of classes, so that when modifying one part of the system, it will not affect other parts, thus making the system has better maintainability.
So what exactly are the objects that can be treated as friends? The principle of least knowledge provides some guidance.
- The current object itself.
- Objects passed in through the method's arguments.
- The object created by the current object.
- The object referenced by the instance variable of the current object.
- The object created or instantiated within the method.
In short, the principle of least knowledge requires that we keep our method calls within certain boundaries and minimize object dependencies.
Design Principles and Design Patterns
Through the previous content, we can probably have a rough answer, that is, design principles are abstract, design patterns are a bit like "objects". In fact, design principles and design patterns are a bit like that.
Most of the design principles give us the right direction for object-oriented analysis and design from the ideological level, and they are the guidelines that we should try to follow when we do object-oriented analysis and design. It is an "abstraction".
Design patterns are already solutions to certain problems in certain scenarios. That is to say, these design principles are the ideological guidance, while the design pattern is the means of implementation, so the design pattern should also comply with these principles, in other words, the design pattern is the embodiment of some of these design principles. It's an "object".
The main points about the recognition and selection of design principles and design patterns are as follows:
- The design principles themselves are guided from the level of thought, and are themselves highly generalized and principled. It is just a general direction for design, and its concrete realization is not only design pattern. Theoretically, many different implementations can be made under the guidance of the same principles.
- Each design pattern does not embody a single design principle. In fact, many design patterns incorporate the ideas of many design principles, and it is not good to emphasize the embodiment of a certain design principle or certain design patterns. Moreover, there are many considerations when applying each design pattern, and the design principles highlighted in different usage scenarios may be different.
- These design principles are only a suggested guide. In fact, in actual development, they are seldom followed at all, and some or all of them are always violated, consciously or unconsciously. Design work is a constant trade-off, and there is a good saying:"Design is a dangerous art of balancing". The design principles are only a guide, and in some cases, there are many other aspects to consider, such as business function, difficulty of implementation, system performance, time and space, and so on.