简介
设计模式用于解决反复出现的问题,是解决特定问题的指导方针。设计模式不是在应用中引用的类、package 或者库,而是在某些特定场景下解决特定问题的指导方针。
设计模式用于解决反复出现的问题,是解决某些特定问题的指导方针。
维基百科中这样描述设计模式:
在软件工程中,设计模式是针对软件设计中普遍存在(反复出现)的各种问题,所提出的可复用型解决方案。设计模式并不直接完成代码的编写,而是描述在不同情况下如何解决问题。
注意
- 设计模式并非解决所有问题的银弹。
- 不要强制使用设计模式,否则结果可能适得其反。谨记:设计模式是用来解决问题的,而不是来寻找问题的,不要过度思考。
- 如果在对的地方对的时机使用设计模式,它会是你的救世主。反之,将会一团糟。
另注:下面的示例代码是用 PHP7 实现的,因为概念是一样的,所以语言并不会阻碍你理解设计模式。其他语言版本的实现正在进行中。
设计模式分类
创建型模式
概述
创建型模式专注于如何初始化对象 。
维基百科
在软件工程中,创建型模式是处理对象创建的设计模式,试图根据实际情况使用合适的方式创建对象。基本的对象创建方式可能会导致设计上的问题,或增加设计的复杂度。创建型模式通过以某种方式控制对象的创建来解决这些问题。
分类
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
- 生成器模式
- 原型模式
- 单例模式
?? 简单工厂模式
现实生活示例
想象一下,你正在建造一座房子而且需要几扇房门,如果每次需要房门的时候,不是用工厂制造的房门,而是穿上木匠服,然后开始自己制造房门,将会搞得一团糟。
概述
简单工厂模式只是为客户端创建实例,而不将任何实例化逻辑暴露给客户端。
维基百科
在面向对象程序设计中,工厂通常是一个用来创建其他对象的对象。通常来讲,工厂是指某个功能或方法,此功能或方法返回不同类型的对象或者类的某个方法调用,返回的东西看起来是「新的」。
程序示例
首先是房门 的接口和实现
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
|
interface Door { public function getWidth(): float; public function getHeight(): float; } class WoodenDoor implements Door { protected $width; protected $height; public function __construct(float $width, float $height) { $this->width = $width; $this->height = $height; } public function getWidth(): float { return $this->width; } public function getHeight(): float { return $this->height; } } |
然后是生产房门的工厂
|
class DoorFactory { public static function makeDoor($width, $height): Door { return new WoodenDoor($width, $height); } } |
这样使用
|
$door = DoorFactory::makeDoor(100, 200); echo 'Width: ' . $door->getWidth(); echo 'Height: ' . $door->getHeight(); |
何时使用?
如果创建对象不仅仅是一些变量的初始化,还涉及某些逻辑,那么将其封装到一个专用工厂中取代随处使用的重复代码是有意义的。
?? 工厂方法模式
现实生活示例
考虑招聘经理的情况。一个人不可能应付所有职位的面试,对于空缺职位,招聘经理必须委派不同的人去面试。
概述
工厂方法模式提供了一种将实例化逻辑委托给子类的方法。
维基百科
在基于类的编程中,工厂方法模式是一种使用了工厂方法的创建型设计模式,在不指定对象具体类型的情况下,处理创建对象的问题。创建对象不是通过调用构造器而是通过调用工厂方法(在接口中指定工厂方法并在子类中实现或者在基类中实现,随意在派生类中重写)来完成。
程序示例
以上述招聘经理为例,首先给出一个面试官接口及实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
interface Interviewer { public function askQuestions(); } class Developer implements Interviewer { public function askQuestions() { echo 'Asking about design patterns!'; } } class CommunityExecutive implements Interviewer { public function askQuestions() { echo 'Asking about community building'; } } |
然后创建 HiringManager
|
abstract class HiringManager { // Factory method abstract public function makeInterviewer(): Interviewer; public function takeInterview() { $interviewer = $this->makeInterviewer(); $interviewer->askQuestions(); } } |
现在,任何子类都可以继承 HiringManager 并委派相应的面试官
|
class DevelopmentManager extends HiringManager { public function makeInterviewer(): Interviewer { return new Developer(); } } class MarketingManager extends HiringManager { public function makeInterviewer(): Interviewer { return new CommunityExecutive(); } } |
这样使用
|
$devManager = new DevelopmentManager(); $devManager->takeInterview(); // Output: Asking about design patterns $marketingManager = new MarketingManager(); $marketingManager->takeInterview(); // Output: Asking about community building. |
何时使用?
类中的一些常见处理需要在运行时动态决定所需的子类,换句话说,当客户端不知道可能需要的确切子类时,使用工厂方法模式。
?? 抽象工厂模式
现实生活示例
扩展一下简单工厂模式 中的房门例子。基于所需,你可能需要从木门店获取木门,从铁门店获取铁门或者从相关的门店获取 PVC 门。进一步讲,你可能需要不同种类的专家来安装房门,比如木匠安装木门,焊接工安装铁门等等。正如你所料,房门有了依赖,木门需要木匠,铁门需要焊接工。
概述
一组工厂的工厂:将相关或者互相依赖的单个工厂聚集在一起,而不指定这些工厂的具体类。
维基百科
抽象工厂模式提供了一种方式,这种方式可以封装一组具有共同主题的个体工厂,而不指定这些工厂的具体类。
编程示例
以上述房门为例,首先给出 Door 接口和一些实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
interface Door { public function getDescription(); } class WoodenDoor implements Door { public function getDescription() { echo 'I am a wooden door'; } } class IronDoor implements Door { public function getDescription() { echo 'I am an iron door'; } } |
然后根据每种房门类型给出对应的安装专家
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
interface DoorFittingExpert { public function getDescription(); } class Welder implements DoorFittingExpert { public function getDescription() { echo 'I can only fit iron doors'; } } class Carpenter implements DoorFittingExpert { public function getDescription() { echo 'I can only fit wooden doors'; } } |
现在抽象工厂可以将相关的对象组建在一起,也就是说,木门工厂会生成木门并提供木门安装专家,铁门工厂会生产铁门并提供铁门安装专家。
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
|
interface DoorFactory { public function makeDoor(): Door; public function makeFittingExpert(): DoorFittingExpert; } // Wooden factory to return carpenter and wooden door class WoodenDoorFactory implements DoorFactory { public function makeDoor(): Door { return new WoodenDoor(); } public function makeFittingExpert(): DoorFittingExpert { return new Carpenter(); } } // Iron door factory to get iron door and the relevant fitting expert class IronDoorFactory implements DoorFactory { public function makeDoor(): Door { return new IronDoor(); } public function makeFittingExpert(): DoorFittingExpert { return new Welder(); } } |
这样使用
|
$woodenFactory = new WoodenDoorFactory(); $door = $woodenFactory->makeDoor(); $expert = $woodenFactory->makeFittingExpert(); $door->getDescription(); // Output: I am a wooden door $expert->getDescription(); // Output: I can only fit wooden doors // Same for Iron Factory $ironFactory = new IronDoorFactory(); $door = $ironFactory->makeDoor(); $expert = $ironFactory->makeFittingExpert(); $door->getDescription(); // Output: I am an iron door $expert->getDescription(); // Output: I can only fit iron doors |
正如你看到的,木门工厂将木匠 和木门 封装在一起,同样地,铁门工厂将铁门 和焊接工 封装在一起。这样就可以帮助我们确保,对于每一扇生产出来的门,都能搭配正确的安装工。
何时使用?
当存在相关的依赖并涉及到稍复杂的创建逻辑时,使用抽象工厂模式。
?? 生成器模式
现实生活示例
想象一下你在 Hardee’s 餐厅点了某个套餐,比如「大 Hardee 套餐」,然后工作人员会正常出餐,这是简单工厂模式。但是在很多情况下,创建逻辑可能涉及到更多步骤。比如,你想要一个定制的 Subway 套餐,对于你的汉堡如何制作有几个选项可供选择,比如你想要什么类型的酱汁?你想要什么奶酪? 在这种情况下,建造者模式便可以派上用场。
概述
允许创建不同风格的对象,同时避免构造器污染。当创建多种风格的对象时或者创建对象时涉及很多步骤,可以使用生成器模式。
维基百科
生成器模式是一种对象创建软件设计模式,其目的是找到重叠构造器反面模式的解决方案。
既然提到了,那我就补充一下什么是重叠构造器反面模式 。 我们时不时地会看到如下构造函数:
|
public function __construct($size, $cheese = true, $pepperoni = true, $tomato = false, $lettuce = true) { } |
正如你看到的,构造器参数的数量可能会迅速失控,并且参数的排列可能让人难以理解。 如果将来要添加更多选项,此参数列表可能会不断增长,这被称为重叠构造器反面模式 。
程序示例
理想之选是使用生成器模式,首先给出汉堡类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
class Burger { protected $size; protected $cheese = false; protected $pepperoni = false; protected $lettuce = false; protected $tomato = false; public function __construct(BurgerBuilder $builder) { $this->size = $builder->size; $this->cheese = $builder->cheese; $this->pepperoni = $builder->pepperoni; $this->lettuce = $builder->lettuce; $this->tomato = $builder->tomato; } } |
然后是 builder
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 35 36 37 38 39 40 41 42 43
|
class BurgerBuilder { public $size; public $cheese = false; public $pepperoni = false; public $lettuce = false; public $tomato = false; public function __construct(int $size) { $this->size = $size; } public function addPepperoni() { $this->pepperoni = true; return $this; } public function addLettuce() { $this->lettuce = true; return $this; } public function addCheese() { $this->cheese = true; return $this; } public function addTomato() { $this->tomato = true; return $this; } public function build(): Burger { return new Burger($this); } } |
这样使用
|
$burger = (new BurgerBuilder(14)) ->addPepperoni() ->addLettuce() ->addTomato() ->build(); |
何时使用?
当需要构建不同风格的对象,同时需要避免构造器重叠时使用生成器模式。与工厂模式的主要区别在于:当创建过程一步到位时,使用工厂模式,而当创建过程需要多个步骤时,使用生成器模式。
?? 原型模式
现实生活示例
还记得多莉吗?那只克隆羊。这里不深入细节,关键点在于克隆。
概述
基于现有对象通过克隆创建对象。
维基百科
在软件开发过程中,原型模式是一种创建型设计模式。当要创建的对象类型由原型实例确定时,将通过克隆原型实例生成新对象。
简言之,原型模式允许你创建现有对象的副本并根据需要进行修改,而不是从头开始创建对象并进行设置。
编程示例
使用 PHP 的 clone 方法可以轻松实现
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
|
class Sheep { protected $name; protected $category; public function __construct(string $name, string $category = 'Mountain Sheep') { $this->name = $name; $this->category = $category; } public function setName(string $name) { $this->name = $name; } public function getName() { return $this->name; } public function setCategory(string $category) { $this->category = $category; } public function getCategory() { return $this->category; } } |
可以像下面这样克隆
|
$original = new Sheep('Jolly'); echo $original->getName(); // Jolly echo $original->getCategory(); // Mountain Sheep // Clone and modify what is required $cloned = clone $original; $cloned->setName('Dolly'); echo $cloned->getName(); // Dolly echo $cloned->getCategory(); // Mountain sheep |
此外,你可以使用魔术方法 **clone 来修改克隆行为。
何时使用
当需要创建一个与已有对象类似的对象,或者当创建对象的成本比克隆更高时,使用原型模式。
?? 单例模式
现实生活示例
一个国家同一时间只能有一位总统。只要使命召唤,这个总统就必须采取行动。 这里的总统就是一个单例。
概述
确保特定类的对象只被创建一次。
维基百科
在软件工程中,单例模式是一种软件设计模式,用来限制类初始化为对象。当恰恰只需要一个对象来协调整个系统的功能时,单例模式非常有用。
实际上,单例模式被认为是反模式,应该避免过度使用。 单例模式并非不好,可能有时候很有用,但应谨慎使用,因为它在你的应用程序中引入了全局状态,一处更改可能会影响其他地方,并且可能会变得很难调试。 另外不好的一点是单例模式会使代码紧耦合,单例也很难mock。
编程示例
要创建一个单例,需要将构造函数设为 private ,禁用克隆,禁用扩展名,并创建静态变量来容纳实例
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
|
final class President { private static $instance; private function __construct() { // Hide the constructor } public static function getInstance(): President { if (!self::$instance) { self::$instance = new self(); } return self::$instance; } private function __clone() { // Disable cloning } private function __wakeup() { // Disable unserialize } } |
这样使用
|
$president1 = President::getInstance(); $president2 = President::getInstance(); var_dump($president1 === $president2); // true |
|