Design mode – responsibility chain mode

Time:2022-3-6

As an office worker, we may often hear complaints such as “chaotic management process” and “unclear responsibility boundary”, which are the problems that we have to face when a person or a department cannot complete a cause independently after the development of the organization or system. Take the usual leave as an example. Imagine that if it is a small company with only a few people, it is likely that it doesn’t even need to write a leave slip. Just tell the boss directly. However, if the company has a certain scale and is a small screw, it may not be qualified to meet the boss. At this time, it has to go through the approval process. We all know that under normal circumstances, leave forms need to be written on OA and submitted to leaders at all levels for approval. Leaders at different levels have different days of approval. For example, if the leave is no more than 1 day, it can be approved by the direct leader (TL), while if it is more than 1 day and no more than 3 days, it can be approved by the person in charge (PM) of the whole project. Of course, if it takes longer, such as 7 days, It may need to be approved by CTO or even higher leaders.

This example has reflected the division of responsibilities. Let’s first simulate the leave scene with code, and then lead to our protagonist – responsibility chain model this time.

Examples

Since it is a leave, of course, there must be a leave application form first, and the leave application form consists of two parts: application and approval results, which are filled in by the applicant and the approval leader respectively. It is not difficult to understand. The code is as follows:

public class LeaveContext
{
    /// 
    ///Apply for
    /// 
    public LeaveRequest Request { get; set; }

    /// 
    ///Approval results
    /// 
    public LeaveResponse Response { get; set; }
}

Then there are several leaders. Each leader has the ability to handle requests within a certain range. After the edification of so many design patterns, it is not difficult to think of abstracting a manager base class for leaders at different levels?

public abstract class Manager
{
    public string Name { get; set; }

    public Manager(string name)
    {
        Name = name;
    }

    public abstract void HandleRequest(LeaveContext context);
}

/// 
///Team leader
/// 
public class TL : Manager
{
    public TL(string name)
        : base(name)
    {
    }

    public override void HandleRequest(LeaveContext context)
    {
        if (context.Request.LeaveDays <= 1)
        {
            context.Response = new LeaveResponse
            {
                Approver = "TL:" + Name,
                IsAgreed = true
            };
        }
    }
}

/// 
///Project Manager
/// 
public class PM : Manager
{
    public PM(string name)
        : base(name)
    {

    }

    public override void HandleRequest(LeaveContext context)
    {
        if (context.Request.LeaveDays > 1
            && context.Request.LeaveDays <= 3)
        {
            context.Response = new LeaveResponse
            {
                Approver = "PM:" + Name,
                IsAgreed = true
            };
        }
    }
}

/// 
///Chief technology officer
/// 
public class CTO : Manager
{
    public CTO(string name)
        : base(name)
    {

    }

    public override void HandleRequest(LeaveContext context)
    {
        if (context.Request.LeaveDays > 3
            && context.Request.LeaveDays <= 7)
        {
            context.Response = new LeaveResponse
            {
                Approver = "CTO:" + Name,
                IsAgreed = true
            };
        }
    }
}

Each leader can handle the same request, but they also perform their own duties, only do things within their own ability, and the angles and methods of handling requests are also very different (that is, there are great differences in code implementation. This example is too simple, so it is not obvious).

Let’s take a look at the place called:

static void Main(string[] args)
{
    LeaveContext context = new LeaveContext
    {
        Request = new LeaveRequest
        {
            Applicant = "Zhang San",
            Reason = "the world is so big, I want to see it",
            LeaveDays = new Random().Next(1, 10)
        }
    };

    TL TL = new TL ("Li Si");
    PM PM = new PM ("Wang Wu");
    CTO CTO = new CTO ("Zhao Liu");
    if (context.Request.LeaveDays <= 1)
    {
        tL.HandleRequest(context);
    }
    else if (context.Request.LeaveDays <= 3)
    {
        pM.HandleRequest(context);
    }
    else if (context.Request.LeaveDays <= 7)
    {
        cTO.HandleRequest(context);
    }

    if (context.Response == null)
    {
        Console. Writeline ($"{context. Request. Leavedays} Day holiday is too long, no one processes the leave application, leave failed");
    }
    else
    {
        Console. Writeline ($"{context. Response. Approver} approved {context. Request. Leavedays} leave application of {context. Request. Application}");
    }
}

The above codes are a little too many, but the basic idea is to instantiate the leave form and several leader objects, then judge which leader to deal with according to the leave days, and finally print out the processing results. If you are interested, you might as well download the source code and run it several times to see the results. The logic is still quite rigorous.

State mode transformation

However, although the logic is rigorous, as an elegant programmer, it is not difficult for us to find some problems. On the one hand,if...elseToo many, poor scalability; On the other hand, asking for leave is too difficult and easy to make mistakes. In fact, the person asking for leave just wants to ask for a leave, and he doesn’t know who has the right to deal with it. When asking for a leave, he always feels that the leaders are throwing the pot at each other, and the management is chaotic?
However, for these two problems, we will find that we have encountered them before. Yes, it is the state mode, but the state mode is that the caller does not want to pay attention to the changes in the internal state of the system, but here does not want to pay attention to the changes in the internal approval process. The solution of state mode is to transfer the setting of state to the inside of the system, that is, after processing the corresponding business logic in a specific state class, set the next state. Here we might as well follow suit.

public class TL : Manager
{
    public TL(string name)
        : base(name)
    {
    }

    public override void HandleRequest(LeaveContext context)
    {
        if (context.Request.LeaveDays <= 1)
        {
            context.Response = new LeaveResponse
            {
                Approver = "TL:" + Name,
                IsAgreed = true
            };
            return;
        }

        PM PM = new PM ("Wang Wu");
        pM.HandleRequest(context);
    }
}

Several other manager objects are handled similarly. In this way, the caller is simple. The code is as follows:

static void Main(string[] args)
{
    ...

    TL TL = new TL ("Li Si");
    tL.HandleRequest(context);

    ...
}

However, although the caller is simple, he has left the pot to the manager. You might as well take a look at the aboveTLClass, it is not difficult to see that implementation oriented programming violatesDimitri principle, which in turn violatesOpening and closing principleRemember that this problem also exists in the state mode. We solved it through the meta sharing mode at that time. The reason is that the state can be shared, and the state is internal to the system and should not be known externally. The situation here is different. The manager object can not be shared and can be accessed externally. Therefore, the processing methods are different. We areManagerAdd to base classNextManagerAttribute, which is also a means of dependency injection. In the previous design pattern, we have used method injection and constructor injection. This is the third injection method – attribute injection.

public abstract class Manager
{
    public Manager NextManager { get; set; }

    ...
}

Then, in the specific implementation class, throughNextManagerPoint to the next manager. Below withTLClass as an example:

public class TL : Manager
{
    ...

    public override void HandleRequest(LeaveContext context)
    {
        if (context.Request.LeaveDays <= 1)
        {
            context.Response = new LeaveResponse
            {
                Approver = "TL:" + Name,
                IsAgreed = true
            };
            return;
        }

        NextManager?.HandleRequest(context);
    }
}

In this way, all manager classes are oriented to abstract programming and can be easily extended. Let’s see how to call:

static void Main(string[] args)
{
    ...

    TL TL = new TL ("Li Si");
    PM PM = new PM ("Wang Wu");
    CTO CTO = new CTO ("Zhao Liu");
    tL.NextManager = pM;
    pM.NextManager = cTO;

    tL.HandleRequest(context);

    ...
}

At first glance, my heart is getting cold and complicated again. The pot that I finally threw out is thrown back. However, although there are defects, it is much better than the initial implementation. At least, when asking for leave, you only need to find the direct leader, and you don’t need to pay attention to the approval details. This is the responsibility chain mode. Let’s take a look at the class diagram below.

Example class diagram

definition

Multiple objects have the opportunity to process a request, connect these objects into a chain, and pass the request along the chain until the processing is completed. The core of the responsibility chain model is the “chain”, which is composed of multiple processors.

Class diagram

  • Handler: Abstract handler role, which is an interface or abstract class for processing requests;
  • ConcreteHandler: the specific processor role. After receiving the request, the specific processor can choose to process the request or pass the request to the next processor.

Responsibility chain mode + template method mode

From the previous codes and definitions, we can see that the responsibility chain model is not perfect. First, there are logical and code repetitions in the implementation of the manager subclass. For example, we need to judge whether we have the right to process the request. After processing, we need to hand it over to the next handler, and this process is fixed. Therefore, we can make the following transformation to extract the common fixed part into the base class:

public abstract class Manager
{
    public Manager NextManager { get; set; }

    public string Name { get; set; }

    public Manager(string name)
    {
        Name = name;
    }

    public void HandleRequest(LeaveContext context)
    {
        if (CanHandle(context))
        {
            Handle(context);
            return;
        }

        NextManager?.HandleRequest(context);
    }

    protected abstract bool CanHandle(LeaveContext context);

    protected abstract void Handle(LeaveContext context);
}

The above code encapsulates the algorithm skeleton intoHandleRequest(LeaveContext context)Method, and then the algorithm sub stepCanHandle(LeaveContext context)andHandle(LeaveContext contextDelay to subclass implementation. Of course, since the substep should not be directly called externally, the access modifier isprotected, see? This is the standard template method pattern.
Let’s see how to implement specific subclasses, orTLFor example:

public class TL : Manager
{
    public TL(string name)
        : base(name)
    {
    }

    protected override bool CanHandle(LeaveContext context)
    {
        return context.Request.LeaveDays <= 1;
    }

    protected override void Handle(LeaveContext context)
    {
        context.Response = new LeaveResponse
        {
            Approver = "TL:" + Name,
            IsAgreed = true
        };
    }
}

Subclasses are cleaner and refreshing, and their responsibilities are more single. They only need to care about their own processing logic, and don’t even care who should be handed over after they are finished. It’s easier to expand.

Responsibility chain mode + Builder mode

In addition, another problem is actually more obvious, that is, the above-mentioned tossing of the pot is still back to the caller. Although the caller no longer needs to know the approval scope of each leader, in addition to specifying his own leader, he has to specify the leader of the leader, the leader of the leader, which is actually unreasonable. What is the reason for this problem? The reason is that it is not in line with common sense. A very important department we ignore – human resources administration department (HR), the leave process should be formulated in advance, not temporarily during each leave.

Therefore, to solve this problem, we must first add oneHRClass, used to manage leave approval process:

public class HR
{
    public Manager GetManager()
    {
        TL TL = new TL ("Li Si");
        PM PM = new PM ("Wang Wu");
        CTO CTO = new CTO ("Zhao Liu");
        tL.NextManager = pM;
        pM.NextManager = cTO;
        return tL;
    }
}

Then, let’s look at the place called:

static void Main(string[] args)
{
    ...

    HR hR = new HR();
    Manager manager = hR.GetManager();
    manager.HandleRequest(context);

    ...
}

It’s simpler and more reasonable. But on the whole, it’s still a little self deceptive, because the newHRClass is also oriented to implementation programming and becomes difficult to maintain. Therefore, it needs to be improved. The improvement method is still the old routine, oriented to abstract programming, and then manage multiple instances through collection. The specific code is as follows:

public class HR
{
    private List _managers = new List();

    public void AddManager(Manager manager)
    {
        _managers.Add(manager);
    }

    public Manager GetManager()
    {
        Manager currentManager = null;
        for (int i = _managers.Count - 1; i >= 0; i--)
        {
            if (currentManager != null)
            {
                _managers[i].NextManager = currentManager;
            }

            currentManager = _managers[i];
        }

        return currentManager;
    }
}

Here is a direct step in place, but it should be able to understand. However, do you see that this is the builder model? It doesn’t matter if you don’t see it. We’ll improve it again later. After allHRIt’s not oriented to abstract programming, and it’s uncomfortable to look naked. But before that, let’s take a look at the place called:

static void Main(string[] args)
{
    ...

    HR hR = new HR();
    hR. Addmanager (New TL ("Li Si");
    hR. Addmanager (new PM ("Wang Wu");
    hR. Addmanager (New CTO ("Zhao Liu");

    Manager manager = hR.GetManager();
    manager.HandleRequest(context);

    ...
}

I’m afraid my friends here are going to scold. What do you want to do? Package and package, come and go back several times, and finally return to the origin. But in fact, it has been very different. For the first time, the caller has a deep coupling with the business logic. For example, the caller must know the approval authority of each leader; The second time, the coupling is reduced, but you still need to know the relationship of the call chain. The third time, this time, you don’t need to know anything else. You just need to create an object, which can’t be bypassed anyway. Moreover, we seem to have returned to the original point after improvement, but in fact, we have thrown the change step by step from the inside of the program to the outermost layer of the program, which can be further decoupled through dependency injection. We might as well replace it withASP. Net core applicationLook, at the same time, let’s make one last transformation:

public interface IManagerBuilder
{
    void AddManager(Manager manager);

    Manager Build();
}

public class ManagerBuilder: IManagerBuilder
{
    private readonly List _managers = new List();

    public void AddManager(Manager manager)
    {
        _managers.Add(manager);
    }

    public Manager Build()
    {
       ...
    }
}

In fact, there is no substantive change in this transformation. It just changes its name and adds an abstract interface. The purpose is to make it easy to see that it is indeed a builder mode. The focus is on how to use it. First, add the following forIServiceCollectionExtension method of.

public static class ManagerServiceCollectionExtensions
 {
    public static IManagerBuilder AddManagers(this IServiceCollection services)
    {
        IManagerBuilder managerBuilder = new ManagerBuilder();
        managerBuilder. Addmanager (New TL ("Li Si");
        managerBuilder. Addmanager (new PM ("Wang Wu");
        managerBuilder. Addmanager (New CTO ("Zhao Liu");
        services.AddSingleton(managerBuilder);
        return managerBuilder;
    }
}

Then inStartupThe code is as follows:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddManagers();
}

Well, it’s that simple. Then it can be used by dependency injection anywhere in the project. Of course,AddManagers(this IServiceCollection services)There are still flaws, but this can be solved by configuring files or reading databases, so we won’t go further here.

Advantages and disadvantages

advantage

The biggest advantage of the responsibility chain model is to separate the request and processing. The requester can not know who handled it, and the processor can not know the whole picture of the request. The two are decoupled to improve the flexibility of the system.

shortcoming

Low performance

Since the core of the responsibility chain mode is “chain”, it means that each request needs to traverse the whole chain, which will inevitably lead to large performance loss. However, in fact, the responsibility chain mode does not have to use the chain. We know that there are two structures in the data structure: array and linked list, and the observation mode we just learned earlier has a similar relationship with the responsibility chain mode, In the observer mode, all observers are saved through the set, and then the set is traversed. The responsibility chain mode can also use the same method. However, after the responsibility chain mode uses the set to save all processors, it may become the observer mode, but is this important?

Inconvenient debugging

The responsibility chain mode adopts a recursive method, and the logic may be complex during debugging.

summary

The responsibility chain model can usually be used in businesses with processes, such as workflow, pipeline, request flow, etc. of course, a large function block can also be divided into several small blocks, and then connected in series through the responsibility chain model. The responsibility chain model is common in various frameworks and is a sharp tool for code reconstruction. However, due to its low performance and relatively complex logic, And if the division of responsibilities is not clear, it is easy to misuse, which may lead to disaster, so it also needs to be used carefully. Moreover, scenes that can be realized through the responsibility chain mode can often be replaced by other modes, such as strategy mode, state mode, observer mode, etc. In addition, each handler in the responsibility chain mode can only process part of the request, ASP The middleware in net core is a typical example, as well as the previous example of asking for leave. In some companies, no matter how many days of leave, all leaders may need to approve level by level, and all leaders agree to pass. As long as one does not agree, it will not pass. This is still the responsibility chain model.

The responsibility chain mode can be used very flexibly and implemented in more than one way, but it is rarely used alone. More often, it needs to be used together with other modes. Therefore, if you want to make good use of the responsibility chain mode, don’t forget to review other design modes!

Source code link

Recommended Today

Tilt model data and LIDAR point cloud data are linked to produce high-precision 1:500 topographic map in EPS

Friends who have produced LIDAR point cloud data must know that if we produce 1:500 high-precision data, only using lidar data can only meet the demand of accurate elevation data in many cases, and many ground objects can only be drawn with the help of Orthophoto images. For the survey area with housing area in […]