Most of the books and articles on design patterns are written at the code level, which is understandable when you read them, but confusing when you actually use them.

This article attempts to talk about design patterns at an architectural level. See the impact of design patterns on an architecture by placing code that uses design patterns and code that does not.

General mode explains routine

The usual way to explain design patterns is:

  • State the intent of the pattern
  • This section describes the applicable scenarios of the mode
  • Gives the class structure of the schema
  • Give corresponding code examples

Take the policy pattern as an example:

Intent: Define a set of algorithms, encapsulate them one by one, and make them interchangeable. This pattern allows the algorithm to change independently of the customers using it.

Applicability:

  • Many of the related classes simply differ in behavior. Policy provides a way to configure a class with one of several behaviors.
  • You need to use different variations of an algorithm. For example, you might define algorithms that reflect different spatial/temporal trade-offs. When these variants are implemented as a class hierarchy of an algorithm, the policy pattern can be used.
  • Algorithms use data that customers shouldn’t know about. Policy patterns can be used to avoid exposing complex, algorithm-specific data structures.
  • A class defines multiple behaviors, and these behaviors occur in the form of multiple conditional statements in the class’s operations. Move the related conditional branches into their respective Strategy classes to replace these conditional statements.

Class structure:

Sample code:

Public class Context {// The object that holds a specific Strategy private Strategy; Public Context(strategy){this.strategy = strategy; /** * public Context(strategy){this.strategy = strategy; Public void invoke(){strategy.doinvoke (); Public void doInvoke(); public void doInvoke(); } public class StrategyA implements Strategy { @Override public void doInvoke() { System.out.println("InvokeA"); } } public class StrategyB implements Strategy { @Override public void doInvoke() { System.out.println("InvokeB"); }}Copy the code

Can you understand the strategic pattern from the above? Do you have any of the following questions?

  • What are the specific advantages of using the policy mode versus I just write if-else?
  • If-else is not easy, why write so many classes?
  • How do I set Strategy into Context?
  • How do I determine which implementation to set to the Context? Or ifelse? ! Isn’t that all those classes that we’re tearing our pants off and farting?

Put the pattern into the schema

The reason for these questions is that we are looking at design patterns in isolation rather than in the context of the actual scenario.

When we put it into a real project, we actually need a client to assemble and invoke the design pattern, as shown below:

public class Client { public static void main(String[] args) { Strategy strategy; if("A".equals(args[0])) { strategy = new StrategyA(); } else { strategy = new StrategyB(); } Context context = new Context(strategy); context.invoke(); }}Copy the code

For comparison, here is also the structure and code for using ifelse directly:

public class Client { public static void main(String[] args) { Context context = new Context(args[0]); context.invoke(); } } public class Context { public void invoke(String type) { if("A".equals(type)) { System.out.println("InvokeA"); } else if("B".equals(type)) { System.out.println("InvokeB"); }}}Copy the code

At first glance, using ifElse seems more straightforward, but don’t worry, let’s compare the two implementations and see the benefits of the design pattern in detail.

The boundary of different

First, using the policy pattern makes the boundaries of the schema different from those of an ifelse encoding. The policy pattern breaks the code into three parts, called here:

  • Invocation layer: Assembles the underlying business logic into a complete executable process
  • Logic layer: The concrete business logic process
  • Implementation layer: Implements the concrete implementation of the substitutable logic in the business logic

Ifelse splits the code into two parts:

  • Invocation layer: Assembles the underlying business logic into a complete executable process
  • Logic layer: specific business logic process and specific logic

The decoupling

In the IFelse implementation, the “logical flow” and the “logical implementation” are hard-coded together and clearly tightly coupled. The policy pattern decouples “logical flow” from “logical implementation”.

With decoupling, the “logical flow” and “logical implementation” can evolve independently without affecting each other.

Independent evolution

Suppose you now want to adjust the business process. For the policy pattern, it is the “logical layer” that needs to be modified; For ifelse, it is also the “logical layer” that needs to be modified.

Suppose you now want to add a new policy. For the policy pattern, it is the “implementation layer” that needs to be modified; For ifelse, it is the “logical layer” that needs to be modified.

In software development, there is a principle called the single responsibility principle, which applies not only to classes or methods, but also to packages, modules, and even subsystems.

So here, you see that ifelse is implemented in a way that violates the single responsibility principle. Using the ifelse implementation, the logic layer has more responsibilities. When business processes need to be adjusted, the code in the logical layer needs to be adjusted; When specific business logic implementations need to be adjusted, the logic layer needs to be adjusted as well.

However, the strategy mode separates business processes and specific business logic into different layers, which makes the responsibilities of each layer relatively single and can evolve independently.

Object to gather

Take a look at the architecture diagram of the policy pattern and compare it to the calling code above. Do you see anything missing?

In the Client, we instantiate the StategyA or StategyB object based on arguments. That is, the “calling layer” uses the code of the “implementation layer”, and the actual calling logic should look like this:

As you can see, the Client is strongly dependent on StategyA and StategyB. This leads to two problems:

  • Object dispersion: If the instantiation method of StategyA or StategyB needs to be adjusted, all the instantiation code needs to be adjusted. Or if StategyC is added, all code that sets Stategy to the Context needs to be adjusted.
  • Stable layer depends on unstable layer: in general, the change frequency of “realization layer” is high; For the “call layer”, the call flow is determined and basically does not change. It is clearly problematic to have a layer that is essentially unchanged rely heavily on a layer that is constantly changing.

Let’s solve the problem of “object dispersion” first, and the next section will solve the problem of “stable layer dependent on unstable layer”!

For the “object dispersion” problem, the creation design pattern can basically solve this problem, corresponding to here, can directly use the factory method!

With the factory method, the build code is confined to the factory method, and when the construction logic of the policy object adjusts, we only need to adjust the corresponding factory method.

Dependency inversion

The invocation layer now only has a direct relationship to the Implementation layer’s StategyFactoryImpl, solving the object dispersion problem. But even if you rely on only one class, the calling layer is still strongly dependent on the implementation layer.

How to solve this problem? We need to rely on inversion. The common approach is to use interfaces, such as the logic layer and the implementation layer here, to achieve dependency inversion: the logic layer is not strongly dependent on either class of the implementation layer. The direction of the arrows is from the implementation layer to the logic layer, so it is called dependency inversion

For the “calling layer,” however, this approach is not appropriate because it requires instantiation of concrete objects. So how do we deal with that?

I believe you have already thought of it, which is IOC that we have been using! By way of injection, make dependency inversion! We can just replace the factory method.

As you can see, dependency injection makes both the calling layer and the implementation layer dependent on the logical layer. Since the logic layer is also relatively stable, the invocation layer does not change as often, and now only the implementation layer needs to change.

Logic to manifest

The final difference is that design patterns make logic explicit. What does that mean?

When you use ifElse, you actually need to drill down into the actual ifelse code so that you know what the logic is.

For code that uses design patterns, go back to the architecture diagram above and you can see the logic:

  • All Strategy implementations are instantiated by StrategyFactory
  • The Client retrieves the Strategy instance from the StrategyFactory and sets it into the Context
  • The Context delegates to a specific Strategy to perform specific logic

As for what the Strategy logic looks like, you can specify it by class name or method name!

conclusion

This paper compares the influence of design patterns on architecture by placing code that uses design patterns and code that does not.

  • Divide the boundary
  • The decoupling
  • Independent evolution
  • Object to gather
  • Dependency inversion
  • Logic to manifest

The resources

  • The Way to Clean Architecture
  • Deep Dive into Design Patterns

The original link: mp.weixin.qq.com/s/8eKNo6\_t…