There is no wake up in the morning, do not understand the topic, but the confusion, only you dare not chase the dream
preface
In the previous articles, the design principles we’ve talked about are basically how to design a class. SRP tells us that a class should have a single source of change; OCP says, don’t tinker with a class; LSP teaches us to design class inheritance relationships.
In the object – oriented design, interface design is also a very important part. We’ve been talking a lot about interface oriented programming, and whether we want to implement OCP, or we’ll talk about DIP in the next lecture, it depends on interface implementation.
You might say, isn’t an interface just a syntax? Put all the methods you need into the interface, and the interface will come out, right? At best, Java interfaces and C++ are declared as pure virtual functions. This understanding of interfaces is clearly still at the level of syntax. Such a design can only be counted as having an interface, but to design a good interface, there should be thinking in the design dimension.
What is a good interface? This requires an understanding of the interface isolation principle.
Interface Isolation Principle
Users should not be forced to rely on methods they do not use.
No client should be forced to depend on methods it does not use.
This seems to be an easy statement, meaning that you should not place methods in an interface that are not needed by the user. From the user’s point of view, this makes perfect sense. Everyone thinks, why should I rely on methods I don’t use? As a designer, you’ll agree. However, not everyone can remember this when it comes to design.
So how do you understand this interface isolation principle? I think there are three ways to understand this.
1. Reasonable division of roles
Understanding “interface” as a set of characteristics of all the methods provided by a class is a concept that only exists logically. In this way, the division of interfaces is actually a direct division of types.
Actually can think so, an interface is equivalent to play a role, and the role in the process of performance, which decided by the actors to show is quite so the implementation of the interface, therefore, an interface should be representative of a character instead of multiple roles, if system involves multiple roles, Each role should be represented by a specific interface.
To avoid confusion, the interface isolation principle can be understood as the role isolation principle.
2. Customized services
If we think of interfaces as narrow JAVA interfaces in our development, in this way, the principle of interface isolation is to provide different interfaces for the same role to deal with different client content. Let me draw a simple diagram to make it completely clear.
In the figure above, there is a role service and three different clients. The three clients need different services, so I divide it into three interfaces, namely Service1, Service2 and Service3. It is obvious. Each JAVA interface only exposes the behavior Cilent requires to the Client, but does not expose methods that are not required.
In fact, if you know design patterns, it’s easy to think that this is an application scenario of the adapter pattern, and I’m not going to talk about the adapter pattern, but design patterns are covered in Planet Knowledge.
3. Interface contamination
Overstaffed interfaces pollute interfaces.
Because each interface represents a role, the implementation of an interface object, in its entire life cycle, play this role, so to distinguish the role is an important work of system design. Therefore, a logical decision should not be to give several different roles to one interface, but rather to different interfaces for processing.
The accurate and appropriate division of the role and the role of the corresponding interface, is an important part of our object-oriented design, if there is no relationship or not the relationship of the interface integrated together, that is the role and interface pollution.
Fat interface to lose weight
Suppose you have a bank system that provides the ability to make deposits, withdrawals, and transfers. It exposes these capabilities to external systems through an interface, and the differences between these capabilities are differentiated by the content of the request. So, here we design an object representing a business request like this:
class TransactionRequest {
// Get the operation type
TransactionType getType(a) {... }// Get the deposit amount
double getDepositAmount(a) {... }// Get the withdrawal amount
double getWithdrawAmount(a) {... }// Get the transfer amount
double getTransferAmount(a) {... }}Copy the code
Each operation type corresponds to a business processing module, which obtains the required information according to its own needs, like the following:
interface TransactionHandler {
void handle(TransactionRequest request); } class DepositHandler implements TransactionHandler{
void handle(final TransactionRequest request) {
doubleamount = request.getDepositAmount(); . }}class WithdrawHandler implements TransactionHandler {
void handle(final TransactionRequest request) {
doubleamount = request.getWithdrawAmount(); . }}class TransferHandler implements TransactionHandler {
void handle(final TransactionRequest request) {
doubleamount = request.getTransferAmount(); . }}Copy the code
In this way, we just need to do a business distribution after receiving the request:
TransactionHandler handler = handlers.get(request.getType());
if(handler ! =null) {
handler.handle(request);
}
Copy the code
It all looks good, and many people would write similar code in real life. However, there is one interface that is too “fat” in this implementation: TransactionRequest.
The TransactionRequest class contains the relevant request content, although this is understandable. But here, it’s easy to intuitively pass it as a parameter to the TransactionHandler. It becomes part of the business processing interface as a request object.
As I said earlier, although you didn’t design a specific interface, concrete classes can become interfaces. However, TransactionRequest is “fat” as an interface in business processing:
- The getDepositAmount method is only used in the DepositHandler;
- The getWithdrawAmount method is only used with WithdrawHandler;
- GetTransferAmount is only used by TransferHandler.
However, the TransactionRequest passed to them contains all of these methods.
You might be thinking, what’s wrong with that? The problem is that a “fat” interface is often unstable. For example, to add a living expenses function, TransactionRequest would add a method to get the living expenses amount:
class TransactionRequest {...// Get the amount of living expenses
double getLivingPaymentAmount(a) {... }}Copy the code
Accordingly, there is a need to increase the business processing methods:
class LivingPaymentHandler implements TransactionHandler {
void handle(final TransactionRequest request) {
doubleamount = request.getLivingPaymentAmount(); . }}Copy the code
While this may seem OCP compliant, in fact, several of the previously written business processing classes DepositHandler, WithdrawHandler, and TransferHandler will be affected due to the change in TransactionRequest. Why do you say that?
If we were using some modern programming language, this might not be obvious. If the code was written in a C/C++ language that requires compiling links, the TransactionRequest changes would cause several other business processing classes to be recompiled, since they all reference TransactionRequest.
In fact, C/C++ programs often spend a lot of time compiling links. In addition to the nature of the language, it is almost common to see files that do not need to be recompiled because of poor design.
You can understand that if an interface changes, all code that depends on it is affected, and that code often has code that depends on its implementation, so that the impact of a change is propagated. From this perspective, you can see that unstable “fat” interfaces have a very wide impact, so we say “fat” interfaces are bad.
How do I modify this code? Since this interface is caused by “fat”, give it weight. According to the ISP, each consumer is only provided with the methods they care about. So, we can introduce some “thin” interfaces:
interface TransactionRequest {}interface DepositRequest extends TransactionRequest {
double getDepositAmount(a);
}
interface WithdrawRequest extends TransactionRequest {
double getWithdrawAmount(a);
}
interface TransferRequest extends TransactionRequest {
double getTransferAmount(a);
}
class ActualTransactionRequest implements DepositRequest.WithdrawRequest.TransferRequest {... }Copy the code
Here, we turn TransactionRequest into an interface to unify subsequent business processes, and ActualTransactionRequest corresponds to the original implementation class. We introduce “thin” interfaces such as DepositRequest, Deposite Request, and TransferRequest, which are interfaces for different business processing methods.
With this foundation, we can also transform the corresponding business processing method:
interface TransactionHandler<T extends TransactionRequest> {
void handle(T request); } class DepositHandler implements TransactionHandler<DepositRequest>{
void handle(final DepositRequest request) {
doubleamount = request.getDepositAmount(); . }}class WithdrawHandler implements TransactionHandler<WithdrawRequest> {
void handle(final WithdrawRequest request) {
doubleamount = request.getWithdrawAmount(); . }}class TransferHandler implements TransactionHandler<TransferRequest> {
void handle(final TransferRequest request) {
doubleamount = request.getTransferAmount(); . }}Copy the code
With this transformation, each business process is concerned only with its own related business requests. So, how should new life capture expends be handled? As you probably already know, add a new interface:
interface LivingPaymentRequest extends TransactionRequest {
double getLivingPaymentAmount(a);
}
class ActualTransactionRequest implements DepositRequest.WithdrawRequest.TransferRequest.LivingPaymentRequest {}Copy the code
Then, add a new business processing method:
class LivingPaymentHandler implements TransactionHandler<LivingPaymentRequest> {
void handle(final LivingPaymentRequest request) {
doubleamount = request.getLivingPaymentAmount(); . }}Copy the code
We can compare the two designs. Only ActualTransactionRequest is modified, and since this class represents the actual request object, under the current structure, it would have to be modified anyway. The rest of the market is not affected by this increase in demand because there are no dependencies. The new design changes are less influential than the original approach.
conclusion
The principle of interface isolation is as follows: Create a single interface instead of a large and bloated interface, refine the interface as much as possible, and contain as few methods as possible. That is, instead of trying to create a huge interface for all the classes that depend on it, we need to create interfaces that are specific to each class.
In fact, the principle of interface isolation is actually “depending on who the guests are,” which means providing different levels of food.
From the perspective of interface isolation principle, you need to specify different services according to different customer requirements. This is the recommended method in interface isolation principle.