在本章中,我们将从著名的四人帮(GoF中探索两种新的设计模式。这些是行为模式,这意味着它们有助于简化系统行为的管理。
本章将介绍以下主题:
- 实现模板方法模式
- 实施责任链模式
- 如何混合两者
模板方法是一种 GoF 行为模式,使用继承在基类及其子类之间共享代码。这是一个非常强大但简单的设计模式。
模板方法模式的目标是将算法的概要封装在基类中,同时保留该算法的某些部分供子类修改。
正如前面提到的,设计简单,但可扩展。首先,我们需要定义一个包含TemplateMethod()
的基类,然后定义一个或多个需要由其子类(abstract
实现的子操作,或者可以重写的子操作(virtual
。使用 UML,它看起来像这样:
图 10.1–表示模板方法模式的类图
这是怎么回事?
-
AbstractClass
实现共享代码:算法。 -
ConcreteClass
实现算法的具体部分。 -
Client
calls theTemplateMethod()
, which calls the subclass implementation of one or more specific algorithm elements.笔记
我们还可以从
AbstractClass
中提取接口,以允许更大的灵活性,但这超出了模板方法模式的范围。
现在让我们深入了解一些代码,看看模板方法模式的作用。
让我们从一个简单的经典示例开始,演示模板方法的工作原理。
上下文:我们希望根据要搜索的集合使用不同的搜索算法。对集合进行排序时,我们希望使用二进制搜索,但如果不是,则希望使用线性搜索。
让我们首先按类(参与者)分解完整的代码列表:
namespace TemplateMethod
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<SearchMachine>(x => new LinearSearchMachine(1, 10, 5, 2, 123, 333, 4));
services.AddSingleton<SearchMachine>(x => new BinarySearchMachine(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IEnumerable<SearchMachine> searchMachines)
{
app.Run(async (context) =>
{
context.Response.ContentType = "text/html";
var elementsToFind = new int[] { 1, 10, 11 };
await context.WriteLineAsync("<pre>");
foreach (var searchMachine in searchMachines)
{
var heading = $"Current search machine is {searchMachine.GetType().Name}";
await context.WriteLineAsync("".PadRight(heading.Length, '='));
await context.WriteLineAsync(heading);
foreach (var value in elementsToFind)
{
var index = searchMachine.IndexOf (value);
var wasFound = index.HasValue;
if (wasFound)
{
await context.WriteLineAsync($"The element '{value}' was found at index {index.Value}.");
}
else
{
await context.WriteLineAsync ($"The element '{value}' was not found.");
}
}
}
await context.WriteLineAsync("</pre>");
});
}
}
internal static class HttpContextExtensions
{
public static async Task WriteLineAsync(this HttpContext context, string text)
{
await context.Response.WriteAsync(text);
await context.Response.WriteAsync (Environment.NewLine);
}
}
Startup
类是消费者,即Client
。HttpContextExtensions
类是一个与模板方法无关的助手。在Startup
类中,我们配置了两个SearchMachine
服务(即AbstractClass
。一个作为LinearSearchMachine
类的实例,另一个作为BinarySearchMachine
类的实例。两个实例都使用不同的数字集合进行初始化。
然后,我们将所有注册的SearchMachine
服务注入Configure
方法(突出显示的代码)。然后我们注册一个委托来处理 HTTP 请求。该处理程序迭代所有SearchMachine
实例,并在输出结果之前尝试查找elementsToFind
数组的所有元素。
接下来是AbstractClass
,SearchMachine
类本身:
public abstract class SearchMachine
{
protected int[] Values { get; }
protected SearchMachine(params int[] values)
{
Values = values ?? throw new ArgumentNullException(nameof(values));
}
public int? IndexOf(int value)
{
var result = Find(value);
if (result < 0) { return null; }
return result;
}
public abstract int Find(int value);
}
SearchMachine
类表示AbstractClass
。它公开了IndexOf()
模板方法,该方法使用abstract``Find()
方法表示的所需钩子(请参见突出显示的代码)。钩子是必需的,因为每个子类都必须实现该方法,从而使该方法成为必需的扩展点(或钩子)。
接下来,我们将探讨ConcreteClass
类的第一个实现LinearSearchMachine
类:
public class LinearSearchMachine : SearchMachine
{
public LinearSearchMachine(params int[] values)
: base(values) { }
public override int Find(int value)
{
var index = 0;
foreach (var item in Values)
{
if (item == value) { return index; }
index++;
}
return -1;
}
}
LinearSearchMachine
类是ConcreteClass
类,表示SearchMachine
使用的线性搜索实现。这是由Find
方法实现的算法的一部分。
最后,我们进入BinarySearchMachine
课程:
public class BinarySearchMachine : SearchMachine
{
public BinarySearchMachine(params int[] values)
: base(values) { }
public override int Find(int value)
{
return Array.BinarySearch(Values, value);
}
}
}
BinarySearchMachine
类是ConcreteClass
类,代表SearchMachine
的二进制搜索实现。正如您可能已经注意到的,我们跳过了二进制搜索算法的实现,将其委托给Array.BinarySearch
内置方法。感谢.NET 团队!
重要的
要使二进制搜索算法工作,必须对集合进行排序。
现在我们已经定义了参与者并探索了代码,让我们看看client
中发生了什么:
client
使用注册的SearchMachine
实例并搜索一组值(1、10 和 11)。- 完成后,
client
向用户显示是否找到了号码。
在这种情况下,当找不到值时,模板方法返回null
,而实现类使用其Find
方法返回负数。
通过运行程序,我们得到以下输出:
=============================================
Current search machine is LinearSearchMachine
The element '1' was found at index 0.
The element '10' was found at index 1.
The element '11' was not found.
=============================================
Current search machine is BinarySearchMachine
The element '1' was found at index 0.
The element '10' was found at index 9.
The element '11' was not found.
瞧!我们已经介绍了模板方法,就这么简单。我们可以在基类中添加一个或多个虚拟方法,这些方法将成为可选的扩展点,由子类实现或不实现,以增加灵活性。这将允许支持更复杂、更通用的场景。
模板方法是一种功能强大且易于实现的设计模式,允许子类在实现(abstract
或重写(virtual
部分子部分时重用算法的框架。
现在,让我们看看模板方法模式如何帮助我们遵循坚实的原则:
- S:模板方法将特定于算法的代码部分推送到子类,同时将核心算法保留在基类中。通过这样做,它遵循单一责任原则(SRP),分配责任。
- O:打开扩展钩子,打开扩展模板(允许子类扩展),关闭修改模板(不需要修改基类,因为子类可以扩展)。
- L:由于子类是实现,没有基本行为来确保它们在子类中工作相同,所以在实现模板方法时遵循Liskov 替换原则(LSP)应该不是问题。这就是说,这个原则是很棘手的,因此有可能创建一个子类(或子类的子类)来破坏 LSP,从而改变程序逻辑。在使用模板方法模式时,请注意这一原则。
- I:只要基类实现尽可能最小的内聚面,使用模板方法模式就不会对程序产生负面影响。
- D:模板方法是基于抽象的,所以只要消费者依赖于该抽象,就应该有助于符合依赖倒置原则(DIP)。
现在我们将讨论责任链设计模式,以及如何将两者结合起来以改进我们的项目。
责任链是一种 GoF 行为模式,用于链接类以有效地处理复杂场景,只需付出有限的努力。同样,我们的目标是把一个复杂的问题分解成多个更小的单元。
责任链模式背后的目标是链接多个处理程序,每个处理程序解决有限数量的问题。如果处理程序无法解决特定问题,它会将解决方案传递给链的下一个处理程序。可能有一个默认处理程序在链的末尾执行某些逻辑,例如抛出异常(例如,OperationNotHandledException
),或者有一个处理程序确保相反的情况(换句话说,没有发生任何事情,特别是没有异常)。
设计
最基本的责任链始于定义一个处理请求的接口(IHandler
。然后我们添加处理一个或多个场景的类(Handler1
和Handler2
:
图 10.2–表示责任链模式的类图
提示
创建责任链时,您可以对处理程序进行排序,以便请求最多的处理程序更靠近链的开头,请求最少的处理程序更靠近链的结尾。这有助于限制每个请求在到达正确的处理程序之前访问的“链链接”数量。
责任链和许多其他模式之间的一个巨大区别是没有中央调度员知道处理者;所有处理程序都是独立的。消费者收到一个处理程序并告诉它处理请求,因此不再复杂。每个处理程序也很简单,不管是否处理请求,然后将其传递给链中的下一个处理程序。足够的理论。让我们看一些代码。
上下文:我们需要创建消息传递应用的接收端,每个消息都是唯一的,因此不可能创建一个单独的算法来处理所有消息。
在分析了问题之后,我们决定构建一个责任链,每个处理程序都可以管理一条消息。这个图案看起来非常完美!
出身背景
这个项目是基于我几年前建立的东西。由于带宽有限,物理(IoT)设备正在发送字节(消息)。然后,在 web 应用中,我们必须将这些字节与实际值关联起来。每条消息都有相同的标题,但正文不同。标头在基本处理程序(模板方法)中处理,链中的每个处理程序都在管理不同的消息类型。对于当前的示例,我们保持它比解析字节更简单,但概念是相同的。
对于我们的演示应用,消息如下所示:
public class Message
{
public string Name { get; set; }
public string Payload { get; set; }
}
Name
属性用作鉴别器来区分消息,每个处理程序的职责是对Payload
属性进行处理。我们不会对有效负载做任何事情,因为它与模式本身无关,但从概念上讲,这就是应该发生的事情。
处理程序也非常简单:
public interface IMessageHandler
{
void Handle(Message message);
}
处理程序只能处理消息。我们的初始应用可以处理以下消息:
AlarmTriggeredHandler
类处理AlarmTriggered
消息。AlarmPausedHandler
类处理AlarmPaused
消息。AlarmStoppedHandler
类处理AlarmStopped
消息。
这三个处理程序非常相似,并且有很多相同的逻辑,但是我们稍后会解决这个问题。同时,我们有以下几点:
public class AlarmTriggeredHandler : IMessageHandler
{
private readonly IMessageHandler _next;
public AlarmTriggeredHandler(IMessageHandler next = null)
{
_next = next;
}
public void Handle(Message message)
{
if (message.Name == "AlarmTriggered")
{
// Do something cleaver with the Payload
}
else if (_next != null)
{
_next.Handle(message);
}
}
}
public class AlarmPausedHandler : IMessageHandler
{
private readonly IMessageHandler _next;
public AlarmPausedHandler(IMessageHandler next = null)
{
_next = next;
}
public void Handle(Message message)
{
if (message.Name == "AlarmPaused")
{
// Do something cleaver with the Payload
}
else if (_next != null)
{
_next.Handle(message);
}
}
}
public class AlarmStoppedHandler : IMessageHandler
{
private readonly IMessageHandler _next;
public AlarmStoppedHandler(IMessageHandler next = null)
{
_next = next;
}
public void Handle(Message message)
{
if (message.Name == "AlarmStopped")
{
// Do something cleaver with the Payload
}
else if (_next != null)
{
_next.Handle(message);
}
}
}
每个处理程序做两件事:
- 它允许将可选的“下一个处理程序”注入其构造函数(在代码中突出显示)。
- 它只处理它知道的请求,将其他请求委托给链中的下一个处理程序。
在这种情况下,如果下一个处理程序为 null,则不会发生任何事情。在实际场景中,您可能希望知道缺少处理程序或消息无效。让我们添加第四个处理程序,通知我们无效请求:
public class DefaultHandler : IMessageHandler
{
public void Handle(Message message)
{
throw new NotSupportedException($"Messages named '{message.Name}' are not supported.");
}
}
新的默认处理程序抛出一个异常,通知消费者有关错误的责任链。
笔记
我们可以创建自定义异常,以便更容易区分系统错误和应用错误。但有时,抛出一个系统异常就足够了。例如,这里有一些异常经常被抛出*,就像*:NotSupportedException
、NotImplementedException
和ArgumentNullException
一样。
让我们使用Startup
作为客户端,使用 HTTP 请求来构建消息。通常,使用GET
方法读取数据,而使用POST
、PUT
、PATCH
等其他方法创建、替换或更新数据。出于测试目的,使用GET
向我们的责任链发送任意数据更容易(不要在你的应用中这样做),因此我们在这一点上作弊:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Create the chain of responsibility,
// ordered by the most called handler (or the one
// that must be executed the faster)
// to the less called handler (or the one that can
// take more time to be executed),
// followed by the DefaultHandler.
services.AddSingleton<IMessageHandler>(new AlarmTriggeredHandler(new AlarmPausedHandler(new AlarmStoppedHandler(new DefaultHandler()))));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IMessageHandler messageHandler)
{
app.Run(async (context) =>
{
var message = new Message
{
Name = context.Request.Query["name"],
Payload = context.Request.Query["payload"]
};
try
{
// Send the message into the chain of responsibility
messageHandler.Handle(message);
await context.Response.WriteAsync($"Message '{message.Name}' handled successfully.");
}
catch (NotSupportedException ex)
{
await context.Response.WriteAsync (ex.Message);
}
});
}
}
在Startup
中,我们通过将以下实例注册为IMessageHandler
的单例,在ConfigureServices
方法中手动创建责任链:
new AlarmTriggeredHandler(
new AlarmPausedHandler(
new AlarmStoppedHandler(
new DefaultHandler())))
在该代码中,每个处理程序都被手动地注入到前面的构造函数中(使用new
关键字创建)。
然后,在Configure
方法中,我们注入IMessageHandler messageHandler
实例,对于每个请求,我们执行以下操作:
- 根据查询字符串创建一个
Message
。 - 将该消息传递给责任链的第一个处理程序(注入
Configure
方法):messageHandler.Handle(message);
。 - 在抛出
NotSupportedException
时写入错误消息,否则写入成功消息。
如果我们运行应用,我们将获得以下消息:
URL: https ://localhost:10001/
Messages named '' are not supported.
通过指定有效名称,如AlarmTriggered
,我们应该得到以下结果:
URL: https ://localhost:10001/?name=AlarmTriggered
Message 'AlarmTriggered' handled successfully.
通过指定一个无效的名称,例如SomeUnhandledMessageName
,我们应该得到以下结果:
URL: https ://localhost:10001/?name=SomeUnhandledMessageName
Messages named 'SomeUnhandledMessageName' are not supported.
瞧。我们建立了一个简单的责任链来处理信息。接下来,让我们使用模板方法和责任链模式来封装处理程序的重复逻辑。
既然我们已经了解了责任链和模板方法模式,现在是干燥处理程序的时候了,方法是使用模板方法模式将共享逻辑提取到抽象基类中,并为子类提供扩展点。
干的
我们在第三章架构原则中介绍了D关于RepeatY我们自己的原则。
好的,那么复制了什么?
next
处理程序注入代码已经被复制,并且作为模式的重要部分,可以封装到基类中。- 测试当前处理程序是否可以处理消息的逻辑已被复制。
新的基类如下所示:
public abstract class MessageHandlerBase : IMessageHandler
{
private readonly IMessageHandler _next;
public MessageHandlerBase(IMessageHandler next = null)
{
_next = next;
}
public void Handle(Message message)
{
if (CanHandle(message))
{
Process(message);
}
else if (HasNext())
{
_next.Handle(message);
}
}
private bool HasNext()
{
return _next != null;
}
protected virtual bool CanHandle(Message message)
{
return message.Name == HandledMessageName;
}
protected abstract string HandledMessageName { get; }
protected abstract void Process(Message message);
}
基于这些变化,模板方法是什么,扩展点(钩子)是什么?
MessageHandlerBase
类增加了Handle
模板方法。该模板方法的算法比以前更易于阅读。然后,MessageHandlerBase
公开了以下扩展点:
CanHandleMessage(Message message)
测试HandledMessageName
是否等于message.Name
。如果处理程序需要更复杂的比较逻辑,则可以重写此操作。HandledMessageName
必须由所有子类实现,驱动CanHandleMessage
的默认逻辑。Process(Message message)
必须由所有子类实现,允许它们针对消息运行逻辑。
现在让我们看一下三个简化的报警处理程序:
public class AlarmTriggeredHandler : MessageHandlerBase
{
protected override string HandledMessageName => "AlarmTriggered";
public AlarmTriggeredHandler(IMessageHandler next = null) : base(next) { }
protected override void Process(Message message)
{
// Do something clever with the Payload
}
}
public class AlarmPausedHandler : MessageHandlerBase
{
protected override string HandledMessageName => "AlarmPaused";
public AlarmPausedHandler(IMessageHandler next = null) : base(next) { }
protected override void Process(Message message)
{
// Do something clever with the Payload
}
}
public class AlarmStoppedHandler : MessageHandlerBase
{
protected override string HandledMessageName => "AlarmStopped";
public AlarmStoppedHandler(IMessageHandler next = null) : base(next) { }
protected override void Process(Message message)
{
// Do something clever with the Payload
}
}
正如我们从更新的报警处理程序中看到的,它们现在仅限于一项职责:处理它们可以处理的消息。相比之下,MessageHandlerBase
现在处理责任链的管道。
通过混合这两种模式,我们创建了一个复杂的消息传递系统,将责任划分为处理程序。每个消息有一个处理程序,链逻辑被推送到基类中。这样一个系统的美妙之处在于,我们不必同时考虑所有的信息;我们可以一次只关注一条信息。在处理一种新类型的消息时,我们可以专注于该精确消息并实现其处理程序,而忽略N其他类型。消费者也可能是超级哑巴,在不知道责任链的情况下将请求发送到管道中,就像魔术一样,正确的处理者将占上风!
在上一个示例中,我们使用HandledMessageName
和CanHandleMessage
来确定处理程序是否可以处理请求。该代码有一个问题:如果子类决定重写CanHandleMessage
,然后决定不再需要HandledMessageName
,那么我们的系统中就会有一个挥之不去的、未使用的属性。
笔记
还有更糟糕的情况,但我们在这里讨论的是组件设计,所以为什么不把系统推向更好的设计呢。
解决此问题的一个解决方案是创建更细粒度的类层次结构,如下所示:
图 10.3–表示实现责任链和模板方法模式的细粒度项目设计的类图
这看起来比实际情况要复杂得多,真的。在深入研究实际操作之前,让我们看看重构代码:
namespace FinalChainOfResponsibility
{
public interface IMessageHandler
{
void Handle(Message message);
}
public abstract class MessageHandlerBase : IMessageHandler
{
private readonly IMessageHandler _next;
public MessageHandlerBase(IMessageHandler next = null)
{
_next = next;
}
public void Handle(Message message)
{
if (CanHandle(message))
{
Process(message);
}
else if (HasNext())
{
_next.Handle(message);
}
}
private bool HasNext()
{
return _next != null;
}
protected abstract bool CanHandle(Message message);
protected abstract void Process(Message message);
}
public abstract class SingleMessageHandlerBase : MessageHandlerBase
{
public SingleMessageHandlerBase(IMessageHandler next = null)
: base(next) { }
protected override bool CanHandle(Message message)
{
return message.Name == HandledMessageName;
}
protected abstract string HandledMessageName { get; }
}
public abstract class MultipleMessageHandlerBase : MessageHandlerBase
{
public MultipleMessageHandlerBase(IMessageHandler next = null)
: base(next) { }
protected override bool CanHandle(Message message)
{
return HandledMessagesName.Contains (message.Name);
}
protected abstract string[] HandledMessagesName { get; }
}
}
省略了AlarmPausedHandler
、AlarmStoppedHandler
和AlarmTriggeredHandler
类,现在继承自SingleMessageHandlerBase
而不是MessageHandlerBase
,但其他内容没有改变。DefaultHandler
也没有改变。出于演示目的,我添加了SomeMultiHandler
,它模拟了一个可以处理"Foo"
、"Bar"
和"Baz"
消息的消息处理程序。这一个看起来如下所示:
namespace FinalChainOfResponsibility
{
public class SomeMultiHandler : MultipleMessageHandlerBase
{
public SomeMultiHandler(IMessageHandler next = null)
: base(next) { }
protected override string[] HandledMessagesName
=> new string[] { "Foo", "Bar", "Baz" };
protected override void Process(Message message)
{
// Do something cleaver with the Payload
}
}
}
现在我们已经看到了类层次结构的代码和 UML 表示,让我们分析新结构的参与者:
-
MessageHandlerBase
manages the chain of responsibility by handling the next handler logic and by exposing two hooks (Template Method pattern) for subclasses to extend:a)
bool CanHandle(Message message)
b)
void Process(Message message)
-
SingleMessageHandlerBase
继承MessageHandlerBase
并实现override
的bool CanHandle(Message message)
方法。它实现了与之相关的逻辑,并添加了子类必须定义的abstract string HandledMessageName { get; }
属性(override
),以便CanHandle
方法工作(一个必需的扩展点)。 -
SingleMessageHandlerBase
的子类实现HandledMessageName
属性,该属性返回它们可以处理的消息名称,并通过重写void Process(Message message)
方法实现处理逻辑。 -
MultipleMessageHandlerBase
与SingleMessageHandlerBase
相同,但它使用字符串数组而不是单个字符串,支持多个处理程序名称。
这听起来可能很复杂,但我们所做的是允许扩展性,而不需要在过程中保留任何不必要的代码,让每个类都有一个单独的责任:
MessageHandlerBase
手柄_next
。SingleMessageHandlerBase
处理处理程序的CanHandle
方法,只支持一条消息。MultipleMessageHandlerBase
处理支持多条消息的处理程序的CanHandle
方法。- 其他类必须实现其版本的
void Process(Message message)
。
瞧!这是另一个示例,展示了模板方法和责任链模式组合在一起的优势。最后一个示例还强调了 SRP 的重要性,它允许更大的灵活性,同时保持代码的可靠性和可维护性。
这种设计的另一个优点是顶部的接口。任何不符合类层次结构的东西都可以直接从接口实现,而不是试图从不适当的结构中调整逻辑,欺骗代码来执行您的命令,通常会导致难以维护的半生不熟的解决方案。
责任链是另一个伟大的 GoF 模式。它允许将一个大的问题划分为更小的有凝聚力的单元,每个单元只做一项工作:处理其特定的请求。与模板方法模式相结合,处理链会变得更加简单,使每个部分更接近单个职责。
现在,让我们看看责任链模式如何帮助我们遵循坚实的原则:
- S:责任链正是针对这一原则,使其成为完美的 SRP 倡导者:单一逻辑单元!
- O:责任链只通过改变链的组成,允许在不接触代码的情况下添加、删除和移动处理程序。
- L:不适用。
- I:通过创建具有多个处理程序(实现)的小型接口,责任链应该对 ISP 有所帮助。处理程序接口不限于单个方法;它可以公开多种方法,只要它们的目标是相同的责任。凝聚力是关键。
- D:通过使用 handler 接口,链中的任何元素或消费者都不依赖于特定的 handler;它们只依赖于接口。
在本章中,我们介绍了两种 GoF 行为模式。这些模式可以帮助我们创建一个灵活但易于维护的系统。正如其名称所述,行为模式旨在将应用行为封装到内聚的软件片段中。
首先,我们看一下模板方法,它允许我们将算法的核心封装在基类中。然后,它允许其子类“填补空白”,并在预定义的位置扩展该算法。这些位置可以是必需的(abstract
)或可选的(virtual
)。
然后,我们探索了责任链模式,它打开了将多个小处理程序链接到处理链中的可能性,在链的开头输入要处理的消息,并等待一个或多个处理程序针对该消息执行与该消息相关的实际逻辑。这是一个重要的细微差别:您不必在第一个处理程序处停止链的执行。在某些情况下,责任链可能更像一条管道,而不是一条消息与一个处理程序的明确关联。
最后,使用 TemplateMethod 模式封装责任链的链接逻辑使我们在不牺牲任何代价的情况下实现了一个更简单的实现。
在下一章中,我们将深入研究操作结果设计模式,以发现管理返回值的有效方法。
让我们来看看几个练习题:
- 在实现模板方法设计模式时,我们是否只能添加一个
abstract
方法? - 我们可以将策略模式与模板方法模式结合使用吗?
- 在责任链中有 32 个处理者的限制是真的吗?
- 在责任链中,多个处理程序能否处理相同的消息?
- 模板方法可以以何种方式帮助实现责任链模式?