相信大家都知道设计模式,很多人都能背出一些常用的设计模式,但是很少有人知道怎么去使用它。甚至有很多人觉得设计模式没有用,哪怕在很多高水平的程序员之间,面对设计模式到底是不是一种“屠龙术”也有着很激烈的争论。
今天文章主题我们不争论设计模式有没有用,而是通过一次具体问题的实践,来看看我们到底怎么去使用设计模式,怎么让一段代码演变得更有扩展性。
问题的开始 我们假设这样一个场景,现在有一台物联人脸识别设备,不断地往消息队列MQTT发送消息,比如心跳包、上下线通知、人脸抓拍事件等等,我们系统要做的是:连接消息队列并监听,根据不同的消息主题及消息内容做出不同的处理。那么我们一开始的代码可能会这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class MqttClientService { public IManagedMqttClient MqttClient { get ; set ; } public void Start ( ) { MqttClient.UseApplicationMessageReceivedHandler(HandleReceivedMessage); } private Task HandleReceivedMessage (MqttApplicationMessageReceivedEventArgs e ) { var topic = e.ApplicationMessage.Topic; if (topic.EndsWith("heatbeat" )){ } else if (topic.EndsWith("basic" )){ } else if (topic.EndsWith("recognize" )){ } else if (topic.EndsWith("snap" )){ } else if (topic.EndsWith("ack" )){ } }
引入策略模式 我们可以看到,在最开始的代码中,我们使用简单的条件控制结构来处理业务逻辑,这样就会出现一个弊端,当我们的业务逻辑非常复杂,或者消息种类非常多的情况下,代码就会变得非常冗长,我们经常能看到一个代码文件达到几千行甚至上万行。那么这时候大家就会想到策略模式,我们就会定义一个Handler接口,并为每一种业务写一个Handler实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class MqttMessage { public MqttMessage (string topic, string message ) { Message = message; Topic = topic; } public string Message { get ; set ; } public string Topic { get ; set ; } } public interface IMessageHandler { public Task Handle (MqttMessage message ) ; } public class HeartbeatHandler : IMessageHandler { public Task Handle (MqttMessage message ) { } } public class RecognizeHandler : IMessageHandler { public Task Handle (MqttMessage message ) { } } ......
这时候,我们最开始的代码会变成这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private Task HandleReceivedMessage (MqttApplicationMessageReceivedEventArgs e ) { var topic = e.ApplicationMessage.Topic; var message = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); IMessageHandler handler; if (topic.EndsWith("heatbeat" )){ handler = new HeartbeatHandler(); } else if (topic.EndsWith("basic" )){ handler = new BasicHandler(); } else if (topic.EndsWith("recognize" )){ handler = new RecognizeHandler(); } else if (topic.EndsWith("snap" )){ handler = new SnapHandler(); } else if (topic.EndsWith("ack" )){ handler = new AckHandler(); } handler.Handle(new MqttMessage(topic, message)); }
引入工厂模式 到这里,大家又会发现,创建Handler的过程还是很长,并且具有明显的业务特征:它是一个生产Handler的流水线,我们可以把这一部分代码抽出来,于是就引出了简单工厂模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class MessageHandlerFactory { public IMessageHandler CreateHandler (string topic ) { IMessageHandler handler; if (topic.EndsWith("heatbeat" )){ handler = new HeartbeatHandler(); } else if (topic.EndsWith("basic" )){ handler = new BasicHandler(); } else if (topic.EndsWith("recognize" )){ handler = new RecognizeHandler(); } else if (topic.EndsWith("snap" )){ handler = new SnapHandler(); } else if (topic.EndsWith("ack" )){ handler = new AckHandler(); } return handler; } }
这时,原来的业务代码块就成了:
1 2 3 4 5 6 7 8 9 private readonly MessageHandlerFactory _handlerFactory = new MessageHandlerFactory();private Task HandleReceivedMessage (MqttApplicationMessageReceivedEventArgs e ) { var topic = e.ApplicationMessage.Topic; var message = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); IMessageHandler handler = _handlerFactory.CreateHandler(topic); handler.Handle(new MqttMessage(topic, message)); }
控制反转 到这里,我在原来的代码块已经几乎看不到业务代码了,很清爽。但是问题又来了,业务代码是清爽,我们的工厂类还是不清爽啊,每次有一种新的消息类型,都要去工厂类里添加一段,不仅繁琐还不好找,万一新入职的员工找了半天都不知道在哪加怎么办呢?这设计模式用了还不如不用呢是不是?所以,我们再想一下,是不是可以根据一些特性,来动态得创建Handler呢?既然写道这里,那肯定是有办法的,这里我们以dotnet为例,通过特性+反射+IOC容器来实现动态创建。
首先我们创建一个TopicEndWith特性类,来给不同的消息进行分类:
1 2 3 4 5 6 7 8 9 10 public class TopicEndWithAttribute : Attribute { public TopicEndWithAttribute (string suffix ) { Suffix = suffix; } public string Suffix { get ; set ; } }
这里我们只创建了简单TopicEndWith特性,对于复杂一些的消息分类,我们也可以干脆正则表达式来实现,现在把特性给Handler装上:
1 2 3 4 5 6 7 [TopicEndWith("heartbeat" ) ] public calss HeartbeatHandler: IMessageHandler{ public Task Handle (MqttMessage message ) { } }
接着,我们再Program.cs文件里,把所有的Handler都注入到IOC容器中:
1 2 3 4 5 Assembly.GetAssembly(typeof (Program)) .DefinedTypes .Where(x => x.IsClass && !x.IsAbstract && x.GetInterfaces().Any(i => i == typeof (IMessageHandler))) .ForEach(type => { builder.Services.AddTransient(type); });
最后,我们再改一改工厂类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class MqttMessageHandlerFactory { private readonly IServiceProvider _serviceProvider; private readonly Dictionary<string , Type> _handlerTypes = new (); public MqttMessageHandlerFactory (IServiceProvider serviceProvider ) { _serviceProvider = serviceProvider; var handlerTypeInfos = Assembly.GetAssembly(typeof (Program)) .DefinedTypes .Where(x => x.IsClass && !x.IsAbstract && x.GetInterfaces().Any(i => i == typeof (IMessageHandler))) foreach (var type in handlerTypeInfos) { var attr = type.GetCustomAttribute<TopicEndWithAttribute>(); if (attr != null && !string .IsNullOrEmpty(attr.Suffix)) { _handlerTypes.TryAdd(attr.Suffix.ToLower(), type); } } } public IMessageHandler CreateHandler (string topic ) { var reg = new Regex(".*face/.+/.+" ); if (!reg.IsMatch(topic)) return null ; var command = topic.Split('/' ).Last().ToLower(); if (!_handlerTypes.TryGetValue(command, out var handlerType)) return null ; var handler = _serviceProvider.GetRequiredService(handlerType) as IMessageHandler; return handler; } }
到这里我们发现,我们已经把所有的if控制结构都拿掉了,如果我们后面有新的消息类型,只需要创建一个MessageHandler,并且把TopicEndWith特性装上,就可以专注于业务代码,完全不需要改动其他代码。同时,我们可以把所有的Handler放至同一个文件夹下,不仅代码清晰,也可以防止多个人改动同一个文件造成冲突改出Bug,甚至可以安排多个人员各自开发不同的消息业务,极大提升开发效率。
引入代理模式 然而工作并没有完成,我们上面的Handler里面提到一个AckHandler,它用来处理设备指令的统一回复,也就是说我们对设备下发的所有指令回复,都是通过这一个Handler来处理,这时我们需要解包消息内容,根据消息内容的特征(而非消息Topic的特征)来转交给其他Handler,所以我们又引出了代理模式:
1 2 3 4 public interface IHandlerProxy { public Task DeliverHander (MqttMessage message ) ; }
在实现Handler代理的过程中,我们发现,之前的简单工厂模式又需要演变为抽象工厂模式。由于代码类似,后面就留给大家思考。
总结 通过以上的例子,我们依次使用了策略模式、工厂模式、控制反转、代理模式,实际上真正使用到设计模式的时候,不是单一的用到其中一个两个,而是多种模式互相配合。如果我们去研究dotnet的源码,去研究开源社区一些主流的框架,会发现都是这样的混合模式,因此可见,设计模式并不是完全没有用,当我们设计通用性框架的时候仍然必不可少。但设计模式也不是“屠龙术”,作为架构师,必然是行走在钢索上的,稍有不慎,便会引来争论。最后把看到的一首打油诗送给大家共勉。
我有一把新锤子,问题当成钉子看。 我刚学了屠龙术,猫狗当成龙来宰。