【策略模式】(Strategy Pattern)

其中的Context是一个所谓的“上下文”,不一定非得是个类,也可以只是一个函数/方法。最关键的是,我们其实根本不需要ConcreteStrategyX类,也不需要它们的对象。我们要的只是一个execute函数而已,我们甚至连execute这个函数名都不需要,只要能执行它就行。
看看函数式编程是怎么玩的(这里以大家都熟悉的JavaScript为例):
function context(func) {
// 做些什么...
var result = func() // 有需要的话可以传参
// 再做些什么...
}
context(function() {
// 做些什么...
return 123 // 是否需要返回值看需求
})
一个匿名函数参数func搞定。
Java能搞出这种类图来,全都是因为Java没有一等公民的函数,所以函数必须依附于类(静态方法)或者对象(非静态方法)。而玩静态方法又没法玩多态,而且类不能当成参数传递给其他函数/方法,所以只能用对象携带方法。而对象的方法必须有个名称,为了统一,就叫execute。由于需要多态,所以我们必须提一个接口出来,在接口里声明execute的方法签名。所有这一切说白了都是为了讨好Java编译器,否则它会给你颜色(red)看。
当然,自从Java有了函数式接口和lambda后一定程度上也能玩函数式编程了。
【观察者模式】(Observer Pattern)

这个在JS里大家已经熟悉到不能再熟悉了:
someButton.onclick = function(event) {
// 处理点击事件
}
又是一个匿名函数搞定!上图里的Subject#attach在这里就是直接赋值。Subject#detach就是赋空值。Subject#notify就是调用一下匿名函数而已。而Observer#update就是匿名函数本身。
【访问者模式】(Visitor Pattern)

能整成这样我也是服了。说白了不就是访问者需要判断一下元素类型嘛。直接套用策略模式,在匿名函数里if-else一下不就行了?(当然还有其他方式,比如引入一个工厂。有模式匹配的函数式编程语言如Haskell、Erlang、Elixir等玩起来更简单)。
【装饰器模式】(Decorator Pattern)

这才是函数式编程的魅力所在!
function core1(arg1, arg2) {
// 做些啥
return 123
}
function core2(arg) {
// 做些啥
return 456
}
function decorate(core) {
// 做些准备工作
return function() {
// 做些啥,甚至可以改变参数
var ret = core(...arguments)
// 做些啥,甚至可以改变返回值
return ret
}
}
var decorated1 = decorate(core1)
var decorated2 = decorate(core2)
酷吧?又是匿名函数搞定!只不过这次的匿名函数是返回值。
小结
很多设计模式,尤其是行为模式,都是为了弥补语言本身没有一等公民的函数而存在的。如果一个语言有一等公民的函数(比如JavaScript),可以砍掉大概一半的设计模式。
题外话
设计模式,尤其是行为模式的核心思想其实是“依赖注入”。
一说到依赖注入,很多人的第一反应是Spring,但其实不然。“依赖注入”用一句简单的话说,就是**“我需要但我不知道怎么造的东西,我找你要”**。而这个“东西”不一定是对象(当然,在Java这种语言里只能是对象),也可以是函数。至于“你怎么把这个东西给我”,就有很多方式了。比如在Spring里,可以通过暴露一个set方法给你调用,也可以通过构造方法的参数接收,甚至可以公开一个成员变量等你把东西塞过来(顺带说一句,这里就可以看出来为什么很多DI框架提倡使用构造方法参数注入,因为只有这种方式才能保证依赖别人的对象在正式提供服务之前,依赖已经注入了,而且不会导致cyclic dependency的问题),而在函数式编程里,还有办法就是直接通过函数的参数接收。要对依赖定一个规范也不一定非要用接口,可以只是用文档或示例代码来展示依赖的特征(有些规范甚至没法用接口来限制,比如“我要一个Class类的实例,它代表的类必须有一个public的无参构造函数”,这个规范在Java里就无法用接口来表达)。更高层次的依赖注入就是框架(比如你在Servlet框架里写的每个Servlet实现类都是框架的一个非必须的依赖),再高层次的就是架构级别的了(比如服务发现需要一个能给我服务访问方式列表的机制,这个机制就是我的依赖,但我不知道怎么造,所以你要给我配一个(例如etcd,consul之类的))。
只要理解了依赖注入,你完全就可以不看设计模式了,因为你凭直觉写出来的代码就是符合设计模式的,甚至比设计模式更简洁,且一样的松耦合。
设计模式本质上是对于开发中的常见问题的封装。请注意:设计模式不是代码 而是类似于如何解读一个问题,然后如何解决一个问题的解释方法。
在工作中我经常碰到的场景是这样的,很多在大学中即便是计算机科学专业,或者其他与IT领域相关的专业毕业的人们。即便他们在学校期间学习过设计模式,但从未受到足够的训练来使用它们。在开始工作之后,大多数人都会忘记设计模式,或者感觉在实际的开发中并无用处。
然而,大多数初级开发者无法认识到的是,理解设计模式的确是开发者必须具备的核心能力。对于开发人员来说,如果希望开发水平不断提升,那么一定会在更高级的开发任务中与设计模式不期而遇。如果在构建完整的架构 / 系统 / 解决方案时,无法识别或者应用设计模式来解决问题,且不说失去了提升自己的机会,项目本身在投入生产之后也可能会迅速变得岌岌可危。
设计模式为什么会于你有益的几点原因
长话短说:设计模式会让任务变得简单。
举个例子
有一个需求是要求一个类的所有属性都必须是私有的,并且保证不能被其他类获取。面对这样的需求时,是否要为每一个属性都加上private关键字?能不能使用创建者模式来保证实例变量无法被外界获取?
另一个原因
设计模式本身其实是一种对于解决方案的共识,通过所有人都认可的词汇来表示。这样你和其他同事之间进行沟通就会更加有效率。你可以说:“这种情况下实现一个单例模式就行了”。然后其他人就都会明白你建议背后的含义。如果没有这种共识,你将需要向大家解释所谓单例模式背后的含义,以及好处和使用场景。
在本文中,会通过应用我个人认为_最常用的三种设计模式_,向读者们展示设计模式是如何让具体代码变的更好的。
1. 策略模式
这是我最喜爱的一种模式。
使用设计模式允许你在运行时切换具体实现,而且不会影响客户端。对于需要处理很多条件的处理方式的场景来说这句有两个“处理”,大部分人的第一反应是if-elif-elif-else的处理方式。但是相比于直接实现一个单一方法,使用策略模式可以在代码运行时选择具体的处理逻辑。也就是说对于不同的处理逻辑,我们可以建立不同的类,从而将分发逻辑和处理逻辑解耦。
从开放-关闭原则中我们知道,代码应该对扩展开放,而对修改关闭。这一原则是我在刚刚成为编程新手时就学到的一个原则,我相信大多数人都至少听说过它。
但在具体的开发工作中应用开闭原则,最常见的场景就是在需要应用策略模式的时候。

具体来说,当多种处理逻辑需要统一对外接口的场景时,这种模式就非常趁手。实现策略模式大致涉及4个元素,包括客户端:
— 客户端 -> 发起上下文的地方
— 上下文 -> 客户端希望处理的场景
— 接口 -> 分发策略的模块
— 算法 -> 真实的处理逻辑
一起来看个例子
想象你有一个神级创业产品,最开始的MVP版本先从附近的邻居开始。因为客户都在周边,所以应用的第一版就只有使用自行车送货这一种选项,发布当日即获得100万的日活,很明显你的业务要起飞了。但不久后你的业务拓展到周边的城市,这时候还仅有自行车一种选项,听起来很难雇到合适的快递小哥。
于是,事情发生了变化:业务要求至少来个通过汽车配送的选项吧。作为首席架构师你应该知道:这只是一个开始。
在这之后各种交通工具你方唱罢我登场,铁路、飞机轮流排期。PackageDelivery这个类随着运输逻辑的增加变得越来越臃肿,越来越难以维护。任何一个小的缺陷都可能威胁整个类,于是即便只是修改了关于自行车运输的处理逻辑,也不得不引入全量回归,来测试整个运输流程。
作为一个有追求的架构师,你终于引入策略模式来解救这场灾难。于是你对所有开发者声明:每一个具体运输逻辑应该有自己的类,这些类我们称之为“策略”。这样一来,下次再需要引入“通过海运传输”这一新选项时,就不用再去修改主流程的代码了。
下面的伪代码展示了主类是如何应用这些策略,以及根据包裹类型分发给不同的策略
interface process PackageStrategy has
method processPackage(package);
//These strategies implement the algorithm by implementing the interface above.
class SendByRail implements process PackageStrategy has
method processPackage(package) {
//process the package that will be sent by Rail and return something
}
class SendByBike implements process PackageStrategy has
method processPackage(package) {
// process and return something
}
//strategies used by the context class
class Context {
private strategy: processPackageStrategy
method setStrategy(processPackageStrategy Strategy) does
this.strategy= strategy;
method executeStrategy(Package package) does
return strategy.processPackage();
}
// read that strategy from user (UI or Api)
class App {
create a new context instance;
get package info;
read the desired user choice
if (choice is rail) then
context.setStrategy(new SendByRail())
if (choice is bike) then
context.setStrategy(new SendByBike())
response = context.executeStrategy(package)
//do something with response.
2. 单例模式
一个类只有一个实例
单例模式适用于对于一个类仅仅需要其唯一实例的场景。限制类的实例化的主要动机在于希望能够统一维护和控制共享资源,比如数据库、存储或者文件。凭借单例模式的技巧,我们可以构建唯一的类实例,然后让其可在全局访问。
比如现代前端框架中对于统一状态管理的工具,通常都会使用单例模式生成全局唯一的实例来管理当前应用的状态。这句好像和上面的英文原文不一致
下面的JavaScript片段可以当作一个小小的实例,展示如何实现一个单例
class Store {
static instance;
constructor() {
if (!Store.instance) {
this._state = [];
Store.instance = this;
}
return Store.instance;
}
add(stuff) {
this._state.push(stuff);
}
}
const instance = new Store();
Object.freeze(instance);
export default instance;
// In other files
const a = new Store();
a.add("phone");
const b = new Store();
console.log(b._state) // outputs ["phone"] - shared state
很多人应该在工作中多多少少都会碰到类似场景:我们只想要全局唯一的一个实例。也许甚至直到看到这篇文章是才知道这是一种设计模式。
这再次回应了前文所说的,设计模式是开发人员在实践中总结出来的对于特定类型的问题专用解决方案、最佳实践。抛开那些看起来很高大上的名字,设计模式本身解决的问题都是很接地气的。
3. 观察者模式
观察者模式存在的土壤就是需要解决大量对象之间一对多关系的问题。观察者模式可以用于设置订阅机制,以便在一个实体上发生某种你感兴趣的行为时,将每次这种行为发生的事件通知给你。
Kafka, RabbitMQ都是在真实世界中通过实现pub/sub(一种观察者模式的变体)模式创造出的发布订阅系统。
下面是一些示例
— 在web开发领域中,尤其是React生态中,你一定听说过使用Redux来管理应用的状态。Redux就是一种观察者模式的实现。当你通过action更新了store中的状态,监听这一改变的组件会根据变更的状态调整自身的显示
— 一旦代码被推送到远程Git仓库,CI环境会监视到这一事件,并执行构建
— 事件驱动的过程式编程用于模拟观察者模式。与其他模式类似,这一模式的应用让整个系统的各个组件通过松散的关系各自链接。由此特性我们便可以开发并维护模块化的系统,以及在事件驱动系统中实现不同actor之间的明显分割
设计模式总是好的吗?
虽然当我们谈到设计模式的时候,总是讨论它广为人知的好的一面。然而当设计模式被滥用时,也会产生不好的副作用。
因此,开发者应当思考在项目中使用设计模式是否是有效并且恰当的,以及查找其他替代方案,并有能力比较替代方案和设计模式。
I’m not sure what’s wrong with people: they don’t learn by comprehending; they learn by rote or something else.” Their understanding is so weak! — Feynman, Richard P.
我在工作中以及技术交流中见过很多工程师,我经常会谈起关于设计模式的话题。我发现有一些工程师很擅长记住这些设计模式,然后就很有信心的认为自己已经掌握了设计模式的咒语可以随便施展。理解设计模式可不是仅仅记住那些炫酷的名字。
这就是为什么在理解每个设计模式之前,要了解它的固有限制以及应用它的时候需要进行权衡,是至关重要的。