责任链 - 重新解释






4.60/5 (5投票s)
责任链模式被G.O.F定义为行为设计模式,用于将命令与其处理对象或处理器解耦。
引言
在这里,我们将了解责任链模式。本文将详细解释
- 它是什么?
- 它为什么存在?
- 它如何运作?
为什么写这篇文章? 嗯,我想写一篇帮助开发人员的文章,不仅要学习实现,还要理解核心概念,以便他们能够识别实际场景,将这种模式应用于其中。在本文的最后,我希望您能够自己描述责任链模式的用途和好处。
背景
在您的编程生涯中,您可能遇到过这样的场景:您有多个命令/请求,每个命令都必须以特定的方式处理。为此,我们创建了多个处理程序对象,其工作是处理这些命令。我们以一对一的映射方式创建这些命令处理程序,即一个处理程序处理一个命令/请求。
通常,处理程序是通过请求类型选择的,这由一个方法完成,该方法可能通过if或switch语句处理选择,在某些情况下,也使用typeOf或type检查。问题在于if条件。这种常见的问题可以通过基于良好的面向对象设计原则设计对象层次结构来轻松解决。G.O.F给出的一种最适用的模式是责任链模式。
这是什么?
责任链模式属于行为模式,因为它关注对象之间的关系。它关注它们之间的责任委托,并简化它们的内部通信,从而有助于动态组织对象行为的控制流。
通过让多个对象有机会处理请求,避免请求发送者与其接收者之间的耦合。将接收对象链接起来,并沿着链传递请求,直到有对象处理它。
此模式将多个命令处理程序对象放入链中。每个命令处理程序都包含链中下一个对象的引用,基本上它创建了一个处理程序链表。现在,请求通过这个对象链传递,每个对象检查请求以确定它是否可以处理该请求。如果当前对象无法处理请求,它只会忽略请求并将原始请求推送到链中的下一个对象,从而给另一个对象处理请求的机会。并且,如果当前对象能够处理请求,那么它就开始处理请求。一旦处理完成,当前对象可以退出链,或者如果它可以,则将原始请求传递给队列/链中的下一个处理程序进行处理。
因此,这种模式旨在提供一种机制,允许许多对象尝试处理请求,而与链中的任何其他对象无关。一旦请求被处理,它要么在该点从链中退出,要么沿着完整的链传播。
为什么存在?
如前所述,处理程序通常通过 if-else 或 switch 语句选择。如果使用 if-else / switch 语句,则存在以下问题
- 客户端必须了解命令处理程序对象,即您必须在代码中显式定义处理程序。因此,客户端和处理程序对象是紧密耦合的。
- 由于它是在设计时决定的,因此,如果多个对象(在运行时确定)是处理请求的候选者而没有代码重复,那几乎是不可能的。
- 以这种方式设计的系统可扩展性较差,并且在维护过程中进行代码更改时需要进行大量的影响分析,因为处理程序对象的任何更改都可能轻易破坏客户端代码。
由于上述复杂性,解决此问题的众所周知常见解决方案是应用责任链模式。优点是
- 它将请求/命令与其处理程序对象解耦。无需直接显式代码来映射两者。
- 可以使多个处理程序处理一个命令,并且客户端甚至不需要了解此类行为更改的信息。因此,此类更改可以从客户端抽象出来。
- 由于所有处理程序对象都已添加到链中,因此可以动态地将其与链关联/解除关联。
它是如何实现的?
在此模式中,一个或多个请求处理程序关联起来以创建一个单一的链。每个请求处理程序对象都保留对列表中下一个处理程序的单个引用。然后将请求推送到列表中的第一个处理程序,该处理程序通过一些业务逻辑检查它是否可以处理它,然后将其推送到列表中的下一个处理程序,依此类推。
它适用于存在多种不同类型请求,并且每个请求都有特定处理要求,并且处理逻辑包含在另一个对象中,并且我们希望请求和处理程序对象不紧密耦合(或不在代码中显式一起使用)的场景。
让我们从一个场景开始。我有一个应用程序(服务),它与运行在多个平台(即iPhone、Android、Windows等)上的多个设备通信。每个移动设备都有不同的应用程序与我们的应用程序服务通信。为了简单起见,我将不关注这些移动应用程序或应用程序服务。这只是一个真实的场景,我将把它作为本文示例的基础。
应用程序服务允许用户对任何设备执行以下操作
- 获取/搜索联系人列表。
- 获取消息
- 获取内存信息。
现在,上述只是3个要求,将来肯定会增加(可能会超过10-20个)。这些基本上只是我们的应用程序服务将发送到设备应用程序的请求。设备应用程序理解这些请求并以数据作为响应返回给服务,然后服务将收集此响应并开始处理它。
我们的示例流程可以通过下图轻松理解。
我希望你能理解这个例子的主题。让我们开始吧.....
注意 - 为了使此示例简单明了,我将重点放在模式实现上,不应将其视为实际生产中应遵循的实现。
定义了以下枚举,用于存储应用程序将处理的每个请求的符号常量。
/// <summary> /// Enum to define different types of Requests. /// </summary> internal enum RequestType { Contact, Messages, Memory }
首先,定义一个抽象类,它将成为我们应用程序中所有具体请求类的基类。它只是为了抽象而存在,因此对象的通信只依赖于抽象,而不依赖于核心实现对象。它只包含那些抽象行为,其具体实现必须由派生类提供。在此示例中,定义了一个无参数构造函数,用于跟踪请求的类型,即它是“联系人”、“消息”还是“内存”类型。
/// <summary> /// Base class for all Requests. /// </summary> internal abstract class RequestBase { public readonly RequestType type ; protected RequestBase(RequestType type) { this.type = type; } }
现在,定义了实现RequestBase类的具体请求类。具体请求类将包含与其自身要求相关的数据。基本上,这些类是DTO(数据传输对象),它们只包含数据,它们通常不知道数据的性质以及将如何使用/处理。处理算法/逻辑包含在另一个称为请求处理程序(在当前上下文中)的单独类中,我们将在本文后面看到这些处理程序的实际应用。
以下代码演示了所有三个请求的具体实现,即联系人请求、消息请求和内存请求。
#region Request Implementation /// <summary> /// Contact Request object to hold contact information. /// </summary> internal class ContactRequest : RequestBase { public int DeviceID { get; private set; } public string ContactName { get; private set; } public ContactRequest(int ID, string name) : base(RequestType.Contact) { this.DeviceID = ID; this.ContactName = name; } } /// <summary> /// Message Request object to hold Messages information. /// </summary> internal class MessagesRequest : RequestBase { public int DeviceID { get; private set; } public string Message { get; private set; } public MessagesRequest(int ID, string message) : base(RequestType.Messages) { this.DeviceID = ID; this.Message = message; } } /// <summary> /// Memory Request object to hold Memory information. /// </summary> internal class MemoryRequest : RequestBase { public int DeviceID { get; private set; } public int TotalMemory { get; private set; } public int MemoryLeft { get; private set; } public MemoryRequest(int ID, int totalMemory, int memoryLeft) : base(RequestType.Memory) { this.DeviceID = ID; this.TotalMemory = totalMemory; this.MemoryLeft = memoryLeft; } } #endRegion "End of Request Implementation"
就是这样。我们已经定义了应用程序将处理的所有请求类。
现在,让我们定义另一个抽象类“RequestHandlerBase”,它将作为所有请求处理程序的基类,因为所有处理程序都共享一些共同的行为。这个基类还将用于创建队列/列表的连接链接,这些链接将包含列表中另一个对象的引用,在我们的示例中,NextHandler将持有该引用。
/// <summary> /// Base class for all Request Handlers. /// </summary> internal abstract class RequestHandlerBase { private RequestHandlerBase NextHandler { get; set; } public void Execute(RequestBase request) { this.Process(request); PushNext(request); } protected abstract void Process(RequestBase request); public RequestHandlerBase SetNextHandler(RequestHandlerBase handler) { this.NextHandler = handler; return this.NextHandler; } private void PushNext(RequestBase request) { if (this.NextHandler != null) this.NextHandler.Execute(request); } }
一旦定义了处理程序的抽象类,我们现在就可以为所有三个请求处理程序(即联系人、消息和内存请求)定义具体实现了。这些请求处理程序知道如何处理特定请求,它们包含请求特定的业务逻辑,即ContactRequestHandler对象知道如何处理ContactRequest对象。
#region Request Handler Implementation /// <summary> /// Contact Handler object to Handle Contact Request information. /// </summary> internal class ContactRequestHandler : RequestHandlerBase { protected override void Process(RequestBase request) { if (request.type == RequestType.Contact) { var contact = (ContactRequest)request; Console.WriteLine("Processing Contact Request. \n Device ID-> {0} - Contact Name ->{1}", contact.DeviceID, contact.ContactName); } } } /// <summary> /// Messages Handler object to Handle Message Request information. /// </summary> internal class MessagesRequestHandler : RequestHandlerBase { protected override void Process(RequestBase request) { if (request.type == RequestType.Messages) { var message = (MessagesRequest)request; Console.WriteLine("Processing Messages Request. \n Device ID-> {0} - Message ->{1}", message.DeviceID, message.Message); } } } /// <summary> /// Memory Handler object to Handle Memory Request information. /// </summary> internal class MemoryRequestHandler : RequestHandlerBase { protected override void Process(RequestBase request) { if (request.type == RequestType.Memory) { var memory = (MemoryRequest)request; Console.WriteLine("Processing Messages Request. \n Device ID-> {0} - Total Memory ->{1} - Memory Left -> {2}", memory.DeviceID, memory.TotalMemory, memory.MemoryLeft); } } } #endregion "End of Request Handler Implementation"
一切就绪...现在让我们执行它。在主部分,我们将创建并关联所有处理程序以形成一个链,然后创建需要处理的不同请求。
注意:在实际场景中,处理程序的创建可能会委托给某些构建器对象,这些对象可以根据特定规则动态创建处理程序列表的机制。为了使此示例简单明了,我只是在主部分本身创建了这些对象。
static void Main(string[] args) { // Create Request Handler Lists. var handler = new ContactRequestHandler(); handler.SetNextHandler(new MessagesRequestHandler()) .SetNextHandler(new MemoryRequestHandler()); // Create Multiple requests. List<RequestBase> request = new List<RequestBase>(); request.Add(new ContactRequest(2342244, "Nokia-X23")); request.Add(new MessagesRequest(2342244, "Hello Everyone ! how r u?")); request.Add(new MemoryRequest(2342244,2048,543)); request.ForEach(x => { handler.Execute(x); }); }
一旦我们执行,输出将如下所示。
所以,这里发生的是,我们创建了不同请求的列表,每个请求都被推送到队列中的第一个处理程序对象,现在,该处理程序对象检查它收到的对象的类型,以确定它是否应该处理此对象。如果它发现适合处理,那么它就开始处理,否则它只会忽略请求对象,并通过它持有的引用将其传递给下一个处理程序。这样整个过程就运作起来了。
那么,它如何影响应用程序的可扩展性和可维护性问题呢???
现在,将来,我们再次被要求为我们的示例应用程序实现新需求。这是关于位置的。客户非常关心位置功能。他希望应用程序现在也能处理位置信息。那么,让我们来做吧……
由于这是一个新请求,因此将其添加到RequestType枚举中。
/// <summary> /// Enum to define different types of Requests. /// </summary> internal enum RequestType { Contact, Messages, Memory, Location }
现在,定义一个LocationRequest类来表示位置请求,以保存位置特定数据。请记住,它应该派生自RequestBase类。
/// <summary> /// Location Request object to hold Location information. /// </summary> internal class LocationRequest : RequestBase { public int DeviceID { get; private set; } public int Latitude { get; private set; } public int Longitude { get; private set; } public LocationRequest(int ID, int latitude, int longitude) : base(RequestType.Location) { this.DeviceID = ID; this.Latitude = latitude; this.Longitude = longitude; } }
现在,为这个新定义的请求定义一个处理程序。我们将其命名为LocationRequestHandler类。使其派生自RequestHandlerBase类。
/// <summary> /// Location Handler object to Handle Location Request information. /// </summary> internal class LocationRequestHandler : RequestHandlerBase { protected override void Process(RequestBase request) { if (request.type == RequestType.Location) { var location = (LocationRequest)request; Console.WriteLine("Processing Location Request. \n Device ID-> {0} - Latitude->{1} - Longitude -> {2}", location.DeviceID, location.Latitude, location.Longitude); } } }
现在,一切都完成了……我们必须将此LocationReqeustHandler附加到处理程序列表,就像我们在主部分所做的那样。
static void Main(string[] args) { // Create Request Handler Lists. var handler = new ContactRequestHandler(); handler.SetNextHandler(new MessagesRequestHandler()) .SetNextHandler(new MemoryRequestHandler()) .SetNextHandler(new LocationRequestHandler()); // Create Multiple requests. List<RequestBase> request = new List<RequestBase>(); request.Add(new ContactRequest(2342244, "Nokia-X23")); request.Add(new MessagesRequest(2342244, "Hello Everyone ! how r u?")); request.Add(new MemoryRequest(2342244,2048,543)); request.Add(new LocationRequest(2134324, 23434, 4645654)); request.ForEach(x => { handler.Execute(x); }); }
执行此操作后,控制台上将显示以下结果。
这是多么简单明了!我们已经成功地以请求的形式为我们的示例添加了一个新功能,而没有对现有代码进行太多更改,这使得系统更具可扩展性和可维护性,同时没有破坏现有代码的风险。
关注点
当您的应用程序中出现以下任何一种情况时,建议使用此模式
- 当有多个对象可以处理特定请求时,即您不想在代码中明确指定处理程序。
- 当处理程序需要动态决定时,即在运行时确定哪个候选者(处理程序对象)应该被允许处理请求对象。
- 如果请求未被处理,则这是可接受的场景,即在任何给定时间都不会抛出异常,它只是被系统忽略。
- 当您想将请求与其发送者与其接收者或处理器对象解耦时。
请记住.....
- 许多其他模式,如中介者、命令和观察者模式,也像责任链模式一样,将请求的发送者与接收者对象解耦。但它们都以不同的方式实现这一点。如上所示,责任链模式使用处理程序链来传递请求。
- 模式可以混合使用以达到最佳效果,即命令模式可用于包装请求,而责任链可用于将它们与其处理程序解耦。