PHP的设计模式及场景应用介绍

有大量的文章解释什么是设计模式,如何实现设计模式,网络上不需要再写一篇这样的文章。相反,在本文中我们更多的讨论什么时候用和为什么要用,而不是用哪一个和如何使用。

我将会为这些设计模式描绘不同的场景和案例,和提供一个简短的定义帮助你们中对这些指定的模式不熟悉的人。让我们开始吧。

这篇文章覆盖了Robert C. Martin书中提到的一些敏捷设计模式。这些设计模式都是最初由四人组在1994年定义和发表的设计模式的现代改写版。Martin的模式更多是对四人组模式的承接,能更好的与现在的编程技术协作。

使用工厂模式

工厂模式用来帮助程序员管理要创建的对象的相关信息。有时对象构造方法的参数有很多,有时这些参数必须用默认的信息填充。这些对象应该在工厂内创建,把它们的生成和初始化信息放在同一个地方。

  • When:当你发现你需要搜集很多信息来创建对象时使用工厂模式
  • Why:工厂帮助我们把对象创建的逻辑放在同一个地方。同样也可以打破依赖,促进松散耦合和依赖注入,让测试更方便。

寻找我们需要的数据

这里有两个常用的模式用于从持久层或外部数据源检索信息。

网关模式

这个模式在持久化解决方案和业务逻辑间定义了一个通信频道。对于简单的应用程序,它可以通过自己检索或重建所有对象,但在复杂的应用程序中对象的创建是工厂的责任。网关只是简单的检索和保存行数据。

  • When:当你需要检索或保存信息时
  • Why:它为复杂的持久化操作提供了简单的公共接口。它同样封装了持久化的信息,解除了业务逻辑和持久化逻辑间的耦合

事实上,网关模式是另一种设计模式的一个特别的实现,我们将在稍后讨论:适配器模式。

代理模式

有时你不能(不想)把持久层的信息暴露给你的业务类。代理模式(proxy pattern)是一个很好的方式:欺骗你的业务类,让它们认为是在使用已经存在的对象。

  • When:当你不得不从持久层或外部源检索信息,但是又不想让业务逻辑知道这些
  • Why:提供了一个非入侵式的方式在幕后创建对象。它同样使得动态的、方便的从不同源检索数据成为可能。

一个代理(proxy)有效实现了一个接口像真正的对象那样,有同样的功能。业务逻辑简单的把它当作真正的对象使用,但实际上,如果对象不存在代理将创建一个。

好吧,好吧。那样是很棒,但是我们如何找到需要创建的对象呢?

请求存储

仓储模式对实现search方法和迷你查询语言是很有用的。它执行查询,使用一个网关封装数据,然后提供给工厂方法创建你需要的对象。

仓储模式同其它的设计模式有点不一样,它是属于领域驱动设计的一部分,也不是Robert C. Martin的书中包含的。

  • When:你需要根据检索条件创建多个对象,或者你需要保存多个对象到持久层。
  • Why:为了让客户端能有特定的对象来处理通用的和单独的查询和持久化的语言。它从业务逻辑中移除了跟创建对象相关的代码。

如果仓库找不到对象呢?一个选择是返回NULL值,但是这样会有两个副作用:

  • 当你尝试调研这样一个对象的一个方法时,会抛出一个Refused Bequest(被拒绝的遗赠)。
  • 它会强迫你在代码中做很多null的检测(if(is_null($param)) return;)。

一个比较好的方法是返回一个空对象。

空对象模式

一个空对象实现了其它对象同样的接口,但是空对象的成员方法返回原始值。比如:一个返回字符串的方法将返回空字符串;另一个返回数字的方法将返回0。这要求你让方法不要返回有意义的值,但是你使用这些对象就不用担心请求被拒绝和去除null的检测代码。

  • When:你频繁的检测null或遇到Refused Bequest(被拒绝的遗赠)
  • Why:它可以增加你代码的透明度和迫使你更多的考虑对象的行为。

It’s not unusual to call many methods on an object before it can do its job. There are situations when you must prepare an object after its creation before you can truly use it. This leads to code duplication when creating those objects in different places.

你需要命令模式

  • When:当你要为初始化对象执行很多操作时。
  • Why:去除客户端代码创建对象时的复杂性。

听起来不错,不是吗?事实上,它在很多情况下是很有用的。命令模式被广泛的用来执行事务。如果你给控制对象添加一个简单的undo()方法, 它可以跟踪所有的要取消的它执行过的事务,如果有必要还可以颠倒顺序。

现在你又有10个(或更多)的命令对象,你希望它们能同时工作。你可以把它们都聚集到一个主动对象。

主动对象

简单而有趣的主动对象只有一个职责:存储一系列的命令对象,和调用它们。

  • When:用同一个命令执行几个相似的对象
  • Why: 它使得客户端代码执行一个任务就能影响多个对象

一个主动对象在执行完一个命令后会把它从列表中移除,也就是说,一个命令你只能执行一次。几个主动对象的真实案例:

  • 购物车:执行每个产品的buy()命令,然后再从购物车中删除掉。
  • 金融交易:把交易事项压入一个队列,用一个简单的活动对象作为队列管理者,执行交易并从队列中删除。

主动对象模式在早期的多任务系统中也发挥了作用。active对象内的每个对象都要保存一个该active对象的引用。这样它们可以执行完自己的工作后,把自己压入队列。即时在今天的系统里面,你也可以调用一个对象工作,然后等待另一个系统的响应。

可重用性

我相信你听说过面向对象编程的重大承诺:代码重用。早期的OOP采纳者设想在数百万不同的项目中使用通用的库和类,但是这从未发生过。

用一些方法模版来代替

这个模式可以重用部分代码,它适用于相互差别很小的运算。

  • When:用简单的方式消除重复
  • Why:这样就没有重复和灵活性的问题了.

灵活性很棒,但是我真的需要吗?

该使用策略模式了

  • When:灵活性和重用性比起代码的简明更重要
  • Why:用它来实现大型的、可交换的复杂逻辑块,同时保持相同的逻辑特征

比如你创建了一个通用的Calculator,然后使用不同的ComputationStrategy来执行这个计算。这是一个被适度使用的模式,当你不得不定义很多条件行为时,它非常有用。

Discover-ability

随着项目的发展,外部用户访问我们的应用变得越来越困难。这是一个理由:需要为讨论中的应用和模块提供一个明确定义的入口点。另外一些理由可能有想要隐藏模块的内部运作和结构。

展示一个外观(Present a Facade)

外观(Facade)本质上是一个api——一个面向客户端的友好接口。当客户端调用其中的一个方法时,facade就委托它隐藏的其它类来提供客户端需要的信息或结果。

  • When: 为了简化API或刻意隐藏内部逻辑.
  • Why:你可以把API与具体的逻辑实现独立开来

控制是很棒的,有时事情发生了变化,你需要执行一些任务。用户必须被通知,红灯还是闪烁,警报开始拉响…你可以用这个方法。

流行的Laravel框架把外观模式用得很好

订阅一名观察者

观察者模式提供了一个简单的方法来监视对象,在条件发生变化时采取行动。这里有两种类型的观察者实现:

  • 轮询:对象接收观察者。订阅者观察这个对象,特定事件发生时被通知。订阅者询问被观察者更多的信息以便采取行动。
  • 推送:像轮询一样,对象接收观察者,当特定事件发生时观察者被通知。当一个通知发生时,观察者收到一个暗示:可以行动了。

背景:

  • When:在你的业务逻辑中提供一个通知系统。
  • Why:这个模式提供了一种方式,可以在多个对象间进行事件通信。

这个模式的案例:邮件通知、后台程序日志或信息系统。当然在现实中,有无数的途径来使用它。

正交性

中介者模式是观察者模式的一个扩展。这个模式把两个对象当做参数,中介者把它自己订阅到第一个参数(把自己当做观察者),当被观察对象变化发生时,中介者决定第二个做什么?

  • When:被影响的对象无法知道被观察的对象时
  • Why:提供了一个在系统中一个对象变化时可以影响其他的对象的隐藏机制

单例

有时,你需要一些特别的对象:在系统中只有一个,你希望确保所有客户端都能看见这些对象的变化。因为总总原因,你想要防止创建多个实例,比如:对一些第三方库的初始化和并发操作的问题。

使用单例模式

一个单例对象拥有一个私有的构造函数,和一个公有的getInstance()方法。这样就确保了这个对象只有一个实例存在。

  • when:你需要实现一个单一性和跨平台的惰性计算的解决方案
  • Why:在需要的时候提供了一个单点访问入口

Monostate模式

另一个实现单一性的途径是Monostate模式。这种方式使用了面向对象编程语言提供的一个技巧,他有一个动态的共有方法获取或者设置静态私有变量的值。这样就确保了所有的实例共享相同的值。

  • When:透明、多肽与单一性很好的结合
  • Why:向客户端代码隐藏了提供单一性的事实

要特别注意单一性。它可能会破坏全局命名空间,大多数情况下可以被其它更符合具体情况的方案代替。

控制不同的对象

假设你有一个开关和一个灯,开关可以控制开灯和关灯。但是,现在你购买了一个电风扇,想用旧的开关去控制它。使用开关、连接线,这在物理世界中是很容易实现的。

不幸的是,在编程世界里却并不容易。你有一个Switch 类和一个Light 类.如果你的开关在操作电灯,如何让他操作风扇。

很简单!复制、粘贴Switch类,修改它来操作风扇。但是这样代码重复了,这样相当于是为风扇买了一个新的开关。你可以用FanSwitch继承Switch,用一个对象代替。但是如果你需要一个按钮或远程控制呢?

The Abstract Server Pattern

这是有史以来最简单的一个模式,它仅仅是提供一个接口。但是这里有不同的实现。

  • When:你需要把几个对象联系起来,并且保持灵活性。
  • Why:因为它是实现灵活性最简单的方式,它同时遵守了依赖反转和开闭原则。

PHP是动态类型的。这意味着你可以省略接口,在相同的上下文中使用不同的对象——冒着拒绝的馈赠(refused bequest)的风险。同样,php允许你定义接口,我建议你使用这个功能,让你的源码提供更清晰的意图。

但是,你想说你已经有了很多类。没错。这里有很多库、第三方的api,还有其他的模块,但是这并不意味着我们的业务逻辑知道这些细节。

插上适配器

适配器模式在业务逻辑和其它事物间建立了一个通信。我们已经见过这类模式了:网关模式。

  • When:你需要与已存在的、有潜在变化的模块、库、API建立连接
  • Why:让你的业务逻辑只是依赖适配器提供的公共方法。

如果上面的模式都不适用与你的情况,你可以使用…

桥接模式

这是一个非常复杂的模式。我个人不喜欢它,因为通常可以选择其它简单的方法。但是在某些特殊情况下,其它的模式都失败时,你可以考虑桥接模式。

  • When:当适配器模式不够用,你要修改两边(业务逻辑和外部)的类。
  • Why:在特别复杂的成本下提供了更强大的灵活性。

组合模式

考虑下有个脚本里面有很多相似的命令,你想通过一次调用来运行它们。等等,我们不是之前已经见过类似的情况?主动对象模式?

是的,我们见过了。但是,这次有点不一样。这次是组合模式,它保持一个对象列表。调用组合对象的一个方法,会调用它的所有对象的相同的方法,并不把它们从列表中删除。调用方法的客户端以为他们是在请求一个特定类型的对象,事实上他们的行为被应用到很多相同的类型的对象上。

  • When:你必须应用一个动作到几个类似的对象上
  • Why:减少重复和简化类似的对象

这里有个示例:你有一个应用可以创建和存储订单。假设你有3个订单:$order1, $order2 和 $order3。你可以依次调用他们的place()方法,或者你可以把这些订单放到一个$compositeOrder对象里面,然后调用它的place()方法。这样一来,在包含所有订单的地方调用place().

状态模式

有限状态机(FSM)是一个含有有限数字状态的模型。最简单的实现方式是用可靠的Switch…case模式。每个case语句表示一个当前状态机的状态,它知道如何激活下一个状态。

我们都知道switch…case不是很令人满意,因为它在我们的对象上输出了太多我们不想要的东西。所以忘记switch…case语句吧,我们应该考虑状态模式(state pattern)。状态模式是几个对象的组合:一个对象负责管理调度,一个表示抽象状态的接口,然后做几种不同的实现——每种实现针对一个状态。每个状态知道下一个状态是什么,这个状态可以通知负责调度的对象设置到队列里的下一个状态。

  • When:需要实现像FSM那样的逻辑
  • Why:为了消除switch…case的问题,和更好的概括每种状态的含义。

一个食物自动售货机可能有一个main类来引用state类,state类看起来可能像:WaitingForCoin, InsertedCoin, SelectedProduct, WaitingForConfirmation, DeliveringProduct, ReturningChange.每个状态完成它自己的工作,然后创建下一个对象状态并发送到负责调度的对象。

装饰模式

有时你在一个应用中部署了一个类或模块,你不能修改它,因为会从跟不上影响到系统。但是,同时你需要添加用户需要的新功能。

装饰模式就是针对这种情况的。它很简单:保留现有功能,另外加一个新的。这是通过继承原始类,在运行时提供新功能。旧的用户继续使用旧的对象,新用户同时使用旧的和新的功能。

  • When:你不能修改旧的类,但是你不得不实现新的功能。
  • Why:它提供非侵入性的方式增加新的功能。

一个简单的例子是打印数据。你为你的用户打印文本文件,但是你又想要有能够打印html的能力。装饰模式就是这样一个解决访问,让你同时保持两种功能。

或者,接收一个访问者

如果说你扩展功能时遇到的问题不同,比如:你有一个复杂的像树状结构的对象,你想在一次性的在每个节点上面添加功能。但是一个访问者是一个可行的解决方案。但是不足之处是,访问者模式要求修改旧的类,如果旧的类不能接受访问者的话。

  • When:装饰模式不再适合,增加一些复杂度是可以接受的。
  • Why:To allow and organized approach to defining functionality for several objects but at the price of higher complexity.

总结

设计模式可以帮助我们解决问题。作为一个执行的建议,不要用模式命名你的类。相反,找到一个正确抽象出来的正确的名称。

有些人会说如果在你的命名里面不包含模式名称,其他的开发人员会很难理解你的代码。如果很难识别一个模式,那问题是出在这个模式的实现上。

使用设计模式可以解决你的问题,但是只是在它们适用的情况下,千万不要滥用。对于一些小问题你会找到好的解决方案。反之,只有在你用了其它的一些方案后,你会发现你需要一个设计模式。

如果你是新接触设计模式,我希望这篇文章能够给你带来一些认识:设计模式在应用程序中是很有帮助的。感谢阅读!