Pipeline design pattern is not until I came into contact with the new company, it is also widely used in projects. Feel very interesting, so write an article to introduce to you, I hope you like it.

demand

A few days ago, Xiao Ming received a demand to develop a “simple” payment processing process, which can handle a series of processing processes after users place an order. This processing process has many links, including: order calculation (including discount calculation), amount verification, inventory verification, coupon verification, payment execution, deduction of coupons, deduction of inventory, notification of third-party logistics, notification of successful payment, notification of merchant delivery, etc.

After xiao Ming received this demand, he thought that this demand is not difficult, that is, simple calculation, verification, interface adjustment, sending messages and so on. If-else who can’t do that? So I started writing 300 lines of code, just like the one below, in one go.

noodles

refactoring

The code is finished. However, in the Code Review, I was scolded by the boss: “How can others maintain your Code in the future because it is all in a pile? Can you read it by yourself in two months?”

Xiao Ming understood what the elder brother meant. The readability of the code is very important, so that it can be easily maintained in the future. So a few methods were extracted, and the main entry kept only the core flow. Incidentally, unit tests are added to ensure the correctness of internal logic.

Extraction method

“Xiao Ming, the readability of the code is very important, and maintainability is also very important. You see, some of these links might be useful in other scenarios, like checking inventory, executing payments, sending notifications, etc.”

“I get it. So, I’ll split these processes into separate classes that I can reuse later.”

Extract the class

Write it and show it to the boss again. The eldest son looked at it and nodded, “Yes, it looks much better now. However, as you know, the business of XX Mall is developing rapidly, and the payment scene has changed a lot. For example, we are planning to launch a number of virtual products in the near future, such as memberships or game skins, which are not in stock, so we don’t need inventory checks and deductions for these products. In addition, we plan to develop the takeout industry, so the final notification will be a little different. We may need to notify the takeout boy. In addition, we plan to hold an operation activity recently. Some products can be recommended and rewarded. After the successful payment of users, we will give a rebate to the recommender. See if you can find a way to support it and make the process as flexible as possible to support new business as cheaply in the future.”

Xiao Ming’s head grew big after hearing this. What the hell! The process is now written in code, although it is divided into classes, but the call is still written in line by line.


The use of Pipeline

So Xiao Ming searched the Internet and learned that there was something called Pipeline design pattern, which felt very fit with his own needs!

Pipeline translates to the meaning of water pipe, Pipeline design mode is actually very simple, just like the CI/CD Pipeline we commonly use, one link to do one thing, and finally series into a complete Pipeline.


The Pipeline design pattern has three concepts: Pipeline, Valve, and Context. Their relationship goes something like this:

Pipeline diagram

A Pipeline has a Context and multiple Valves. These valves are small and unitized, and a Valve does one simple thing. The communication between the front and back Valves is carried by the Context. Context is a simple POJO class that holds data from the Pipeline.

public interface Pipeline {
    void init(PipelineConfig config);
    void start(a);
    Context getContext(a);
}
 public class Context {  }  public interface Valve {  void invoke(Context context);  void invokeNext(Context context);  String getValveName(a); } Copy the code

Configuration change

The essence of the Pipeline design pattern is its configurability. Pipelines can be configured externally if you want to switch the order of the valves, or if some business does not use a particular Valve. In this way, it can flexibly adapt to diversified services and configure different processing flows for different services.

Take a closer look at the Pipeline, does it look like the Filter we requested on the Web? In web.xml, we configure which filters to use, resulting in a Filter chain. Its Context is request and response. If, and when, the next Filter is called explicitly:

filterChain.doFilter(request, response); 
Copy the code

Pipeline design patterns are also widely used by Tomcat.

tomcat

In fact, there are many ways to implement configuration. You can put them in XML or YML files, you can put them in Json, you can put them in a unified configuration center or database. You can even write groovy code to run pipelines, as Jenkins did, depending on your implementation.

It looks something like this:

{
    "scene_a": {
        "valves": [
            "checkOrder".            "checkPayment". "checkDiscount". "computeMount". "payment". "DeductInventory" ]. "config": {  "sendEmail": true. "supportAlipay": true  }   } } Copy the code

If your business is relatively stable, there are many lines of business, but relatively little change. You can also use the Pipeline design pattern, but if you don’t want to configure it, you can also write it in code and call it explicitly.

Pipeline variation and evolution

A Pipeline is not static and can vary and evolve depending on your needs.

Design patterns

Pipeline actually uses the idea of the chain of responsibility pattern. But it also works well with other design patterns.

The strategy pattern

We at a Valve may need to have different logic for different lines of business. For example, the same text, some are sent in an email, some are sent in a text message, some are sent in a pin. In this case, you can write the channel to send the current line of business in the configuration, and then use the policy mode in Valve to decide which channel to send, so that there is no need to write a lot of if-else in Valve, which is easy to expand in the future.

Template method pattern

Sometimes it’s possible that some valves have logic in common. For example, the following pseudocode logic:


At this point, the template method pattern can be used to define an abstract Valve, extract the common logic, and make each Valve’s different logic into an abstract method, which can be implemented by Valve itself.

Factory method pattern

The advantage of Pipeline lies in the flexible realization of different business flow through configuration. Achieve the perfect combination of unification and differentiation. It is better to use factory mode to generate a Pipeline by reading the configuration.

Read the current line of business, then instantiate and assemble the entire Pipeline through this line of business and configuration, which can be done by calling a Pipeline Factory.

Pipeline pipeline = PipelineFactory.create(pipelineConfig);
pipeline.start();
Copy the code

combination

Although we say that a Valve only does one simple thing. But that’s relative to the whole process. Sometimes too detailed is not good, not convenient management. The right thing to do is to abstract and group. For example, if we have a “validation” phase, we don’t need to separate the validation of each field into Valve’s main process. We can put a “verification” Valve in the main process, and then generate a “verification Pipeline” within the “verification” Valve. This makes the main process clear and the responsibilities of each Valve clear.

Multiple pipeline

Note that a sub-pipeline should have its own Context, but it should also have the main Pipeline Context. Should this be implemented by inheritance?

The tree in figure

A Pipeline, as described above, is essentially a chain. But if you go in a more general (and complex) direction, it could be a graph or a tree.

Suppose we have a conditional branch in a link, and we use the data state in the context at the time to determine which Valve to go next, forming a tree. You might end up with a Valve, and that’s a diagram.

Figure shape Pipeline

Trees and graphs can significantly increase the complexity of a Pipeline, and need to be combined with visual configuration to give full play to its power, but the cost is relatively high, unless the real business is very large and complex, it is not recommended.

Tree and graph pipelines also need to be designed specifically with data structures, how nodes go down.

Parallel execution

As we saw earlier, Valve executes one by one in a chain. Sometimes, however, multiple Valves are independent of each other and can run in parallel at the same time. For example, sending messages can be done in parallel by multiple Valves.

At this time, we can transform the Pipeline, just as Jenkins designed the Pipeline, dividing a complete Pipeline into phases, stages, steps, etc. We can set a Phase or a Step to be executed in parallel. This requires writing another Pipeline that executes in parallel, using tools like CountDownLatch to wait for all the valves to execute, and moving down.

Logging and Visualization

Logging and visualization are necessary. For a Pipeline, it is recommended to generate a traceId in the Context, and then use AOP and other technologies to print logs or drop libraries. Finally, the interface displays the information of which valves each invocation goes through, the time, and the Context before and after each Valve execution.

Exceptions are also important. If the Pipeline design pattern is used, it is recommended to define a set of exceptions, which can be divided into “interruptible Pipeline exceptions” and “non-interruptible Pipeline” exceptions. This determines whether the Pipeline needs to be interrupted based on actual business requirements. In our previous example, if we fail the validation phase, we should throw an exception that interrupts the Pipeline and keeps it from going down. However, if an exception occurs while sending an email, you only need to catch the exception, print a WARN log, and continue. Uninterrupted Pipeline is determined by the business.

Using ThreadLocal

In theory, we should use Context anywhere to pass data through the Pipeline. But Context is sometimes a little bit more cumbersome to use. For example, when we extract private methods inside Valve, we often pass the Context as a method parameter, which is not particularly convenient to use. Valve is supposed to be stateless, and it’s not appropriate to put Context inside a Valve as an attribute.

Instead of using the Context, Valve uses ThreadLocal to access data. But there are three caveats to using ThreadLocal.

If your Pipeline is intended to support parallelism, it is not appropriate to use ThreadLocal in parallel valves.

When using ThreadLocal, remember to Clear in the final stage to avoid affecting the next Pipeline execution of the current thread.

Do not place individual attributes in a ThreadLocal, because a thread can only place one value in a ThreadLocal of the same type. Our context may have multiple String, Boolean equivalents. If you use ThreadLocal, you can wrap all the attributes into a Context class and put them into ThreadLocal.

The disadvantage of Pipeline

The Pipeline design pattern is powerful, but it also has significant drawbacks.

The first disadvantage is poor readability. Because it is configurable, the configuration is often external (such as a JSON in a database). So it’s not very readable. Especially when we read Valve code, “you don’t really know how it’s called before and after unless you check the configuration.”

The second disadvantage is that data is passed between pipelines through Context rather than a simple function call. So a Pipeline is stateful, and “method calls modify the Context internally” instead of returning a value, which has side effects.

Application scenario of Pipeline mode

The Pipeline design mode is suitable for business scenarios with complex processes and long links. This is especially applicable to the scenario where there are many service lines but different service lines have some commonalities and some differences. Reuse logic can be realized through a single Valve, and differentiation of different lines of business can be realized through configuration.

The essence of the Pipeline

The essence of Pipeline is the “flexible application of data structures and design patterns” to cope with complex and changing business scenarios. It is not a new thing, nor a fixed design mode, but should be a flexible design idea.

Of course, it’s not a silver bullet. It doesn’t solve every problem. It still has to be “appropriate.”

About the author

I’m Yasin, a dish chicken that keeps getting better.

Wechat public number: made up a process

Personal website: https://yasinshaw.com

Pay attention to my public number, grow up with me ~

The public,

This article is formatted using MDNICE