php 和设计模式
发表于|更新于|设计模式
|总字数:744|阅读时长:2分钟|浏览量:
场面话
工作几年,复杂的业务场景,重复的 CURD 一直在消耗着我作为程序员的激情与精力,在设计模式这方面的积累从来都不够完善,出去面试时还经常会面临面试官的灵魂拷问,总觉得自己是不是就快被淘汰了。
所以,是时候下功夫整理下这方面的知识了。
开始之前,要考虑一个问题,我们为什么要学习设计模式呢?
首先从概念来讲,设计模式作为一种描述问题及其解决方案的方法,是无数的 IT 前辈在工作中总结出的 特定场景 下的 最佳解决方案,那么当我们遇到同样的场景时,就可以通过使用模式,来实现符合自己程序的解决方案,以此降低代码的耦合度,提高代码的质量,同时也方便我们后期对程序进行调整或拓展。
第二,现在大部分 PHP 程序都是依托于框架进行开发,一般情况下,我们对于框架的使用,只是局限于在一个强大的程序基础设施上添加一些小装饰。那么学习并掌握设计模式以后,我们就能够理解框架是如何解决问题,以及框架解决问题的策略,随着开发的深入,我们也能够以设计为导向,开发出自己的可复用的代码库,这对我们来说,也是一种极大的积累和提升。
第三,对于团队来说,人来人往是常态,对于从一开始就已经接手项目的成员来说,理解程序的逻辑会很轻松,但是对于新加入的成员来说,采用标准化设计模式的程序才是更容易的理解和掌握的存在,这可以使新成员更快的参与到项目的开发工作中,发挥出他作为项目成员的作用。
第四,设计模式定义了专业词汇,通过这些词汇,开发人员之间的沟通变得更加容易,可以节省很多沟通成本。
设计模式与面向对象密切相关,因此我应该不会简单的复制一堆模式来加以理解,而是从面向对象入手,逐渐向设计模式演深。
目录
对象 设计原则 设计模式分类 工厂模式 单例模式 生成器模式 原型模式 门面模式 适配器模式 装饰器模式 桥接模式 代理模式 组合模式 享元模式 依赖注入模式 注册模式 流接口模式 策略模式 模板方法模式 观察者模式 迭代器模式 责任链模式 命令行模式 备忘录模式 状态模式 访问者模式 中介者模式文章作者: m-finder
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 M-finder!
相关推荐

2021-03-21
php 和设计模式 - 依赖注入模式
依赖注入是控制反转的一种实现方式。要实现控制反转,需要将创建被调用者实例的工作交由 IOC 容器完成,然后在调用者中注入被调用者,通常使用构造器或方法注入实现。这样我们舅实现了调用者和被调用者的解偶,这个过程就是依赖注入。 那么控制反转是什么呢?其实也就是 A 依赖于 B,常规做法是在 A 中直接实例化 B,那么控制反转就是将 B 在外部实例化,然后传入 A 去使用。看完以后,其实对依赖注入也就有了理解。 12345678910111213141516171819202122232425262728class Computer{ protected HardDisk $hardDisk; public function __construct(HardDisk $disk) { $this->hardDisk = $disk; } public function run() { $this->hardDisk->run(); echo '一台没有感情的电脑开始运行', PHP_EOL; }}class HardDisk{ public function run() { echo '一块没有感情的硬盘开始运行', PHP_EOL; }}$disk = new HardDisk();$computer = new Computer($disk);$computer->run(); 以上代码就是一个简单的依赖注入,你以为这就结束了?并没有,咱们在学一下 IOC 容器: 1234567891011121314151617181920212223242526272829class Container{ public array $bindings = []; public function bind($key, Closure $value) { $this->bindings[$key] = $value; } public function make($key) { $new = $this->bindings[$key]; return $new(); }}$container = new Container();$container->bind('disk', function (){ return new HardDisk();});$container->bind('computer', function () use($container){ return new Computer($container->make('disk'));});$computer = $container->make('computer');$computer->run(); ok,以上就是依赖注入的全部代码了。

2021-03-21
php 和设计模式 - 设计模式分类
一般来说,设计模式分三个大类,分别是创建型模式、结构型模式、行为型模式。但是随着技术的不断发展,也有一些新型的模式出现。 创建型模式提供创建对象的机制,增加已有代码的灵活性和可复用性。 传统的创建型共有 5 种模式,分别是工厂模式、抽象工厂模式、生成器、原型和单例模式。 结构型模式介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效。 传统的结构型共有 7 种模式,分别是适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。 新型的有依赖注入模式、注册模式和流接口模式。 行为型模式负责对象间的高效沟通和职责委派。 这个最多,共 11 种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式。 其他模式委托模式、服务定位器模式、资源库模式、实体属性值模式。 虽然现在还不知道这些模式都是用来做什么的,但是没关系,一个一个的研究吧。干巴得!(╯‵□′)╯︵┻━┻

2021-03-21
php 和设计模式 - 桥接模式
桥接模式也是一个典型的单一职责模式。 在组件设计过程中,如果职责划分不够清晰,当父类发生变更,子类也需要跟着变动,要么违背开闭原则,要么导致子类数量膨胀。桥接模式,就是为了解决这个问题。 桥接模式的做法是,使抽象和实现完全分离,使其能够独立变化。或者也可以直白一点,通过组合/聚合的方式避免继承滥用。 举个🌰: 123456789101112131415161718192021222324252627282930313233343536abstract class Shape{ protected Color $color; public function setColor(Color $color) { $this->color = $color; } public abstract function draw();}class Circle extends Shape{ public function draw() { $this->color->setColor(); echo 'circle', PHP_EOL; }}interface Color{ public function setColor();}class Blue implements Color{ public function setColor() { echo 'blue', PHP_EOL; }}$shape = new Circle();$shape->setColor(new Blue());$shape->draw(); 抽象部分使用继承,实现部分使用组合。 后续如果我们需要换成另外一个颜色,只需要稍作改动即可实现: 12345678910class Red implements Color{ public function setColor() { echo 'red', PHP_EOL; }}$shape = new Circle();$shape->setColor(new Red());$shape->draw();

2021-03-21
php 和设计模式 - 观察者模式
当对象的状态发生变化时,所有依赖于它的对象都得到通知并被自动更新。它使用的是低耦合的方式。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273class DeleteUserSubject implements \SplSubject{ protected SplObjectStorage $observers; protected $data; public function __construct() { $this->observers = new \SplObjectStorage(); } public function attach(SplObserver $observer) { $this->observers->attach($observer); } public function detach(SplObserver $observer) { $this->observers->detach($observer); } public function notify() { foreach ($this->observers as $observer) { $observer->update($this); } } public function process() { $this->data = new class { public string $name = 'wu'; public function delete() { echo '用户 ', $this->name, ' 被删除', PHP_EOL; } }; $this->data->delete(); echo '开始通知关联处理:', PHP_EOL; $this->notify(); } public function getName() { return $this->data->name; }}class UserLogDeleteObserver implements \SplObserver{ protected SplSubject $subject; public function update(SplSubject $subject) { $this->subject = clone $subject; $this->deleteUserLog(); } public function deleteUserLog() { echo '删除用户', $this->subject->getName(),' 的日志', PHP_EOL; }}$subject = new DeleteUserSubject();$subject->attach(new UserLogDeleteObserver());$subject->process(); 这个模式代码稍微多一点,但是场景很经典,也很容易理解。

2021-03-21
php 和设计模式 - 设计原则
设计模式是一套被反复使用、多数人知晓、经过分类编目的、代码设计经验的总结。使用设计模式是为了 提高代码复用性 和 灵活性,让代码更容易被他人理解、保证代码 可靠性。 为了实现代码的 可复用性 和 灵活性。设计模式 提出了一些关键的 面向对象设计原则。 单一职责其核心思想为:一个类,最好只做一件事,应该仅有一个引起它变化的原因。 可以理解为,一个类,应该是一组 相关性很高 的方法及数据的封装。 当一个类承担的职责过多时,就相当于把这些职责耦合在了一起,当其中一个职责发生变动,可能会对其他职责造成影响。 类的职责包括两个方面,数据职责和行为职责,数据职责通过类的属性实现,行为职责通过其方法实现。 单一职责是实现高内聚、低耦合的指导方针。它是最简单但又最难实现的原则,需要开发人员发现类的的不同职责并将其分离。 举个🌰:登陆模块显示登录页面,校验登录参数,连接数据库,查找用户,返回结果。 功能太过耦合,拆分成多个模块。 开闭原则开闭原则是面向对象中最重要的原则。 一个软件应当对扩展开放,对修改关闭。也就是说在设计一个模块的时候,应该使这个模块可以在不被修改的前提下进行扩展。 一个类一旦开发完成,后续新增的功能就不应该通过修改这个类来完成,而是应该通过继承增加新的类。为什么要对修改关闭呢?因为一旦修改某个类,就可能破坏了系统原有功能,就需要重新测试。 抽象化 是开闭原则的关键。什么是抽象化,就是把一个或多个类中的公共的、有共性的东西抽取出来。抽象的最大好处在于它是抽象的、稳定的,不容易发生改变的。实现开闭原则的核心思想就是 面向接口编程,而不是具体实现。 开闭原则可以用一个更加具体的原则来描述:可变性封装。也就是找到系统中的可变因素并把它封装起来。 上一篇中提到的灯是个绝佳的🌰。 里氏替换所有引用基类(父类)的地方必须能透明的使用其子类的对象。 这句话怎么理解呢?通俗来讲就是在软件中如果能够使用基类对象,那么一定也可以使用其子类对象。把基类都替换为它的子类对象,程序不会产生任何错误和异常。反过来则不成立。 里氏替换应该是开闭原则的一个扩展,由于使用基类对象的地方都可以使用其子类对象,因此在程序中尽量以基类类型来对对象进行定义,而在运行时再用其子类对象替换基类对象。 其中有一点很关键,里氏替换原则强调子类尽量使用基类中的方法,而不是重写,除非子类有其特殊性。 举个🌰,依然是上一篇提到的灯,但是加了一点改动: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253class Light{ function show() { echo '灯光随机', PHP_EOL; }}class BlueLight extends Light{ function show() { echo '蓝色', PHP_EOL; }}class RedLight extends Light{ private bool $power = false; public function hasPower(): bool { return $this->power; } /** * @throws Exception */ function show() { if(!$this->hasPower()){ throw new Exception('没电了,亮不起来'); } echo '红色', PHP_EOL; }}class User{ function openLight(Light $light) { $light->show(); }}$user = new User();$light = new Light();$blueLight = new BlueLight();$redLight = new RedLight();$user->openLight($light);$user->openLight($blueLight);$user->openLight($redLight); 根据里氏替换原则,子类必须能够替代父类。也就是说,虽然子类重写了父类的方法,但是在能够使用父类的场景里面,也一定要能够使用子类。很显然,BlueLight 类符合原则,RedLight 类虽然也实现了父类方法,但是抛出了父类没有的异常,所以违反了里氏替换原则,在 User 类中,我们无论是传入 Light 基类对象,还是 BlueLight 类对象,都是没有任何错误和异常的。 由以上实例,我们可以总结出,里氏替换原则本质是对继承的约束。 依赖倒置依赖于抽象层,不依赖于具体。即高层次的模块不应该依赖低层次模块。抽象不应该依赖于细节,细节应该依赖于抽象。 要面向接口编程,而不是面向实现编程。 一般情况下,我们认为调用者是高层模块,被调用者是底层模块。 实现开闭原则的关键是抽象化,如果说实现开闭原则是面向对象编程的目标,那么依赖倒置就是面向对象编程的主要手段。常用的手段为在代码中使用抽象类,将具体实现放入元数据。 再强调一遍,抽象是相对稳定的,不容易发生改变。 再举个🌰: 12345678910111213141516171819202122232425class Book{ public function getContent() { echo 'long long ago, 遥远的东方有一个特别英俊帅气的小伙', PHP_EOL; }}class Mother{ public function narrate(Book $book) { $book->getContent(); }}class Paper{ public function getContent() { echo '上周五,中国首次实现经济反超美国称为世界第一经济体', PHP_EOL; }}$mother = new Mother();$mother->narrate(new Book()); 在上边的例子中,麻麻看书讲故事,如果有一天,书看烦了,想看个报纸,但是麻麻做不到,因为要读报纸首先要把麻麻改掉。这就有点荒谬。显示不是一个好的设计,原因就是麻麻和书之间的耦合程度太高了,必须降低他们之间的耦合度。 我们来做下调整: 1234567891011121314151617181920212223242526272829303132interface Reader{ public function getContent();}class Book implements Reader{ public function getContent() { echo 'long long ago, 遥远的东方有一个特别英俊帅气的小伙', PHP_EOL; }}class Paper implements Reader{ public function getContent() { echo '上周五,中国首次实现经济反超美国称为世界第一经济体', PHP_EOL; }}class Mother{ public function narrate(Reader $reader) { $reader->getContent(); }}$mother = new Mother();$mother->narrate(new Book());$mother->narrate(new Paper()); 首先,我们抽取出一个接口类 Reader,然后 Book 和 Paper 分别去实现它。然后将 Mother 调整为依赖于接口。现在,无论是想看书还是想看报纸,又或者想看连环画,我们只要去实现 Reader 即可,再也不用改动 Mother 了。 上边里氏替换中灯的例子其实已经符合依赖倒置原则,但是看到这个例子更生动,更容易理解,所以就赘述了一下。 传递依赖关系的方式有三种,上边的例子中使用的接口传递,还有构造函数传递和 setter 传递。 接口隔离客户端不应该依赖它不需要的接口。 使用多个专门的接口,而不是一个大的单一的接口。 看上去似乎和单一职责很像,但是不是,单一职责针对的是类的职责,接口隔离针对的则是接口。 举个🌰: 123456789101112131415161718192021222324252627282930313233interface WorkerInterface{ public function work(); public function sleep();}class HumanWorker implements WorkerInterface{ public function work() { echo 'I like working', PHP_EOL; } public function sleep() { echo 'I like sleeping', PHP_EOL; }}class RobotWorker implements WorkerInterface{ public function work() { echo 'I like working', PHP_EOL; } public function sleep() { echo 'robot never sleep', PHP_EOL; }} 上边的例子中,有个很明显的缺点,机器人不需要睡觉,但是它却必须实现睡觉的方法,这显然违反了接口隔离。 那么,我们再调整一下: 1234567891011121314151617181920212223242526272829303132interface WorkInterface{ public function work();}interface SleepInterface{ public function sleep();}class HumanWorker implements WorkInterface, SleepInterface{ public function work() { echo 'I like working', PHP_EOL; } public function sleep() { echo 'I like sleeping', PHP_EOL; }}class RobotWorker implements WorkInterface{ public function work() { echo 'I like working', PHP_EOL; }} 调整后,接口一分为二,实际使用时各取所需,不再被迫实现自己不需要的接口。 合成复用面向对象有两种方式实现代码复用,一是继承,二还是继承,哦不,而是组合/聚合,也可以叫做合成。合成复用原则要求在软件复用时,首先考虑使用组合、聚合等关联方式实现,其次才考虑使用继承。也就是在一个新对象里通过关联方式使用已有对象的方法和功能。 那么为什么推荐使用组合呢,首先,继承后,父类的方法暴露给了子类,这等于破坏了类的封装性,所以继承复用也被称为白箱复用。其次,父类的方法发生变动会影响到子类,属于耦合度较高的一种表现,不利于代码的维护。最后,继承自基类的方法是静态的,限制了复用的灵活性。 上个图吧,例子挺清晰的。图片出处 emmm,前边反复提的灯的例子就不太符合合成复用原则了。 有兴趣的可以自己改造一下。 迪米特法则迪米特法则又叫最少知道原则,它强调每一个类应当对其它类有尽可能少的了解,不和陌生人说话。也就是尽可能少的产生依赖。 A 和 B 产生交互,B 和 C 产生交互,A 只和 B 交互,不跟 C 玩。 这个法则就比较搞,分了狭义和广义,怎么来的没搞清楚,后边再补充。 狭义强调如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一类的某一个方法的话,可以通过第三者转发这个调用。 广义则是对封装的强调,也就是对信息隐藏的控制。方法封装在类的内部,调用者只需要调用并获取预期结果,不需要关注具体实现。 举个反面🌰: 1234567891011121314class MySchool{ public function manager() { $myClass = new MyClass(); foreach ($myClass->getClasses() as $class) { foreach ($class->getStudents() as $student) { echo $student->getName(), PHP_EOL; } } }} 在上边的例子中,School 类和 Class 类发生交互,但是也和 Student 类发生了交互,违反了最少知道原则。 再来个优化版: 123456789101112class MySchool{ public function manager() { $myClass = new MyClass(); foreach ($myClass->getClasses() as $class) { $class->manager(); } }} 通过 Class 类中 manager 方法去获取学生信息,School与 Student 类的交互就被清除了,School 也不知道 Class 是怎么获取 Student 信息的,它只管调用。 emmm,还可以继续递进,Class manager 只管理自己的信息, 然后调用 Student manager。 1234567891011121314151617181920212223242526272829303132333435363738394041424344class MySchool{ public function manager() { $myClass = new MyClass(); $myClass->manager(); }}class MyClass{ private function getClasses(): array { return [ ... ]; } public function manager() { foreach ($this->getClasses() as $class) { $student = new MyStudent(); $student->manager(); } }}class MyStudent{ private function getStudents(): array { return [ ... ]; } public function manager() { foreach ($this->getStudents() as $student) { echo $student->name, PHP_EOL; } }}

2021-03-21
php 和设计模式 - 享元模式
享元模式会尽量使相似的对象共享内存,能让你在有限的内存中载入更多对象。 当一个应用程序需要创建大量对象,并且这些对象的大多数状都可变为外部状态时,就很适合享元模式。 一如既往的举个🌰: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889interface Message{ public function send(User $user);}class AliMessage implements Message{ // 内部状态 protected Template $template; public function __construct(Template $template) { $this->template = $template; } // user 属于外部状态 public function send(User $user) { echo 'use ', $this->template->getTemplate(), ' send msg ', 'to user ', $user->getName(), ' by ali', PHP_EOL;; }}class MessageFactory{ protected array $messages = []; public function getMessage(Template $template) { $key = md5($template->getTemplate()); if (!array_key_exists($key, $this->messages)) { echo 'message create', PHP_EOL; $this->messages[$key] = new AliMessage($template); } return $this->messages[$key]; }}class Template{ protected string $template; public function setTemplate(string $template) { $this->template = $template; } public function getTemplate(): string { return $this->template; }}class User{ protected string $name; public function setName(string $name) { $this->name = $name; } public function getName(): string { return $this->name; }}$templateA = new Template();$templateA->setTemplate('template a');$templateB = new Template();$templateB->setTemplate('template b');$userA = new User();$userA->setName('wu');$userB = new User();$userB->setName('yf');$factory = new MessageFactory();$flyweightA = $factory->getMessage($templateA);$flyweightA->send($userA);$flyweightA->send($userB);$flyweightB = $factory->getMessage($templateB);$flyweightB->send($userA); 这次来点不一样的,贴张截图帮助理解: 可以看到,在享元工厂中,一共创建了两次 message,当我们重复用一个模板发送消息时,模板作为内部状态已经被缓存了,调用的时候直接取出即可,避免了重复创建造成的资源浪费。 例子虽然不太贴切,但是看完应该也能总结出,享元模式需要依赖于一个享元工厂以及一个享元角色,也就是咱们代码中的 AliMessage 类。