设计模式之美(王争)摘录总结

2023-11-10

经过八个月的学习,极客时间中王争的设计模式之美。最近又重读两遍,读书百遍其义自现,真是遍受益匪浅啊,学习之后才知道代码可以这样设计这样优美的编写。觉得自己有摘录一些在学习中认为比较好的精华,以备自己以后的查用和复习。

设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。

一.术语描述:

  • 面向对象编程:具有丰富的特性(封装、抽象、继承、多态),可以实现很多复杂的设计思路,是很多设计原则、设计模式等编码实现的基础;
  • 面向对象语言:是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言;
  • 设计原则:是指导我们代码设计的一些经验总结,对于某些场景下,是否应该应用某种设计模式,具有指导意义;
  • 设计模式:是针对软件开发中经常遇到的一些设计问题,总结出来的一套解决方案或者设计思路。应用设计模式的主要目的是提高代码的可扩展性;
  • 编程规范:主要解决的是代码的可读性问题。编码规范相对于设计原则、设计模式,更加具体、更加偏重代码细节、更加能落地;
  • 重构:作为保持代码质量不下降的有效手段,利用的就是面向对象、设计原则、设计模式、编码规范这些理论;
  • 面向对象分析/面向对象设计/面向对象编程:简单点讲,面向对象分析就是要搞清楚做什么,面向对象设计就是要搞清楚怎么做,面向对象编程就是将分析和设计的的结果翻译成代码的过程。
  • 封装(Encapsulation):类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据;
  • 抽象(Abstraction):是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的;
  • 继承(Inheritance):继承最大的一个好处就是代码复用;
  • 多态(Polymorphism):子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现;
  • 面向过程编程:也是一种编程范式或编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能;
  • 面向过程编程语言:首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程;
  • 面向过程编程和面向对象编程的区别:对于大规模复杂程序的开发来说,整个程序的处理流程错综复杂,并非只有一条主线。如果把整个程序的处理流程画出来的话,会是一个网状结构。如果我们再用面向过程编程这种流程化、线性的思维方式,去翻译这个网状结构,去思考如何把程序拆解为一组顺序执行的方法,就会比较吃力;面向对象编程是以类为思考对象。在进行面向对象编程的时候,我们并不是一上来就去思考,如何将复杂的流程拆解为一个一个方法,而是采用曲线救国的策略,先去思考如何给业务建模,如何将需求翻译为类,如何给类之间建立交互关系,而完成这些工作完全不需要考虑错综复杂的处理流程。当我们有了类的设计之后,然后再像搭积木一样,按照处理流程,将类组装起来形成整个程序。这种开发模式、思考问题的方式,能让我们在应对复杂程序开发的时候,思路更加清晰;面向对象编程通过类这种组织代码的方式,将数据和方法绑定在一起,通过访问权限控制,只允许外部调用者通过类暴露的有限方法访问数据,而不会像面向过程编程那样,数据可以被任意方法随意修改。因此,面向对象编程提供的封装特性更有利于提高代码的易维护性;
  • 编程语言的异同:跟二进制指令、汇编语言、面向过程编程语言相比,面向对象编程语言的编程套路、思考问题的方式,是完全不一样的。前三者是一种计算机思维方式,而面向对象是一种人类的思维方式。我们在用前面三种语言编程的时候,我们是在思考,如何设计一组指令,告诉机器去执行这组指令,操作某些数据,帮我们完成某个任务。而在进行面向对象编程时候,我们是在思考,如何给业务建模,如何将真实的世界映射为类或者对象,这让我们更加能聚焦到业务本身,而不是思考如何跟机器打交道。可以这么说,越高级的编程语言离机器越“远”,离我们人类越“近”,越“智能”。

  • 怎样避免写出面向过程风格的代码:面向过程编程风格恰恰符合人的这种流程化思维方式。而面向对象编程风格正好相反。它是一种自底向上的思考方式。它不是先去按照执行流程来分解任务,而是将任务翻译成一个一个的小的模块(也就是类),设计类之间的交互,最后按照流程将类组装起来,完成整个任务;
  • 抽象类和接口的异同:抽象类更多的是为了代码复用,而接口就更侧重于解耦。接口是对行为的一种抽象,相当于一组协议或者契约,你可以联想类比一下 API 接口。调用者只需要关注抽象的接口,不需要了解具体的实现,具体的实现代码对调用者透明。接口实现了约定和实现相分离,可以降低代码间的耦合性,提高代码的可扩展性;
  • 基于接口而非实现编程的三个原则:函数的命名不能暴露任何实现细节;封装具体的实现细节;为实现类定义抽象的接口,从设计初衷上来看,如果在我们的业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类就可以了;
  • 继承和组合的优劣势:继承最大的问题就在于继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性,继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。而这三个作用都可以通过其他技术手段来达成。比如 is-a 关系,我们可以通过组合和接口的 has-a 关系来替代;多态特性我们可以利用接口来实现;代码复用我们可以通过组合和委托来实现。所以,从理论上讲,通过组合、接口、委托三个技术手段,我们完全可以替换掉继承,在项目中不用或者少用继承关系,特别是一些复杂的继承关系;   继承改写成组合意味着要做更细粒度的类的拆分。这也就意味着,我们要定义更多的类和接口。类和接口的增多也就或多或少地增加代码的复杂程度和维护成本。所以,在实际的项目开发中,我们还是要根据具体的情况,来具体选择该用继承还是组合;  如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承;

 

二.设计原则:

 

  • 单一职责原则(Single Responsibility Principle):一个类或者模块只负责完成一个职责(或者功能),不要设计大而全的类,要设计粒度小、功能单一的类。我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构;
  • 开闭原则(Open Closed Principle):添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”;
  • 里式替换原则(Liskov Substitution Principle):子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
  • 接口隔离原则(Interface Segregation Principle):客户端不应该被强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。换一种理解方式,把接口理解为单个接口或函数。那接口隔离原则就可以理解为:函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。接口隔离原则跟单一职责原则有点类似,不过稍微还是有点区别。单一职责原则针对的是模块、类、接口的设计。而接口隔离原则相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它的思考的角度不同。它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
  • 依赖反转原则:

控制反转(Inversion Of Control):框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。这里的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员“反转”到了框架。

依赖注入(Dependency Injection):不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

依赖注入框架(DI Framework):这个框架就是“依赖注入框架”。我们只需要通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象、类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。

依赖反转原则(Dependency Inversion Principle):高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。

  • KISS 原则(Simple and Stupid):尽量保持简单;不要使用同事可能不懂的技术来实现代码;不要重复造轮子,要善于使用已经有的工具类库;不要过度优化。
  • YAGNI 原则(You Ain’t Gonna Need It):你不会需要它,不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计。
  • DRY 原则(Don’t Repeat Yourself):实现逻辑重复、功能语义重复和代码执行重复;
  • 代码复用性(Code Reusability):代码复用表示一种行为:我们在开发新功能的时候,尽量复用已经存在的代码。代码的可复用性表示一段代码可被复用的特性或能力:我们在编写代码的时候,让代码尽量可复用。DRY 原则是一条原则:不要写重复的代码。从定义描述上,它们好像有点类似,但深究起来,三者的区别还是蛮大的。

提高代码复用性七条手段:

减少代码耦合:对于高度耦合的代码,当我们希望复用其中的一个功能,想把这个功能的代码抽取出来成为一个独立的模块、类或者函数的时候,往往会发现牵一发而动全身。移动一点代码,就要牵连到很多其他相关的代码。所以,高度耦合的代码会影响到代码的复用性,我们要尽量减少代码耦合。

满足单一职责原则:越细粒度的代码,代码的通用性会越好,越容易被复用。

模块化:这里的“模块”,不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统。

业务与非业务逻辑分离:为了复用跟业务无关的代码,我们将业务和非业务逻辑代码分离,抽取成一些通用的框架、类库、组件等。

通用代码下沉:从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。一般情况下,在代码分层之后,为了避免交叉调用导致调用关系混乱,我们只允许上层代码调用下层代码及同层代码之间的调用,杜绝下层代码调用上层代码。所以,通用的代码我们尽量下沉到更下层。

继承、多态、抽象、封装:利用继承,可以将公共的代码抽取到父类,子类复用父类的属性和方法。利用多态,我们可以动态地替换一段代码的部分逻辑,让这段代码可复用。除此之外,抽象和封装,从更加广义的层面、而非狭义的面向对象特性的层面来理解的话,越抽象、越不依赖具体的实现,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。

应用模板等设计模式:一些设计模式,也能提高代码的复用性。

  • 迪米特法则(Law of Demeter):不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。

高内聚:所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。

松耦合:所谓松耦合是说,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。实际上,我们前面讲的依赖注入、接口隔离、基于接口而非实现编程,以及今天讲的迪米特法则,都是为了实现代码的松耦合。

 

三.规范与重构:

 

1.重构:

重构实际上是对我们学习的经典设计思想、设计原则、设计模式、编程规范的一种应用。重构实际上就是将这些理论知识,应用到实践的一个很好的场景,能够锻炼我们熟练使用这些理论知识的能力。除此之外,平时堆砌业务逻辑,你可能总觉得没啥成长,而将一个比较烂的代码重构成一个比较好的代码,会让你很有成就感。

  • 重构的目的:重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低。
  • 重构的对象:笼统地分为大规模高层次重构和小规模低层次的重构。

小重构:对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名、规范注释、消除超大类或函数、提取重复代码等等。小型重构更多的是利用我们能后面要讲到的编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入 bug 的风险相对来说也会比较小。

大重构:对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等。这类重构的工具就是我们学习过的那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入 bug 的风险也会相对比较大。

  • 重构的时机:技术在更新、需求在变化、人员在流动,代码质量总会在下降,代码总会存在不完美,重构就会持续在进行。时刻具有持续重构意识,才能避免开发初期就过度设计,避免代码维护的过程中质量的下降。
  • 重构的方法:分为大型重构和小型重构。

进行大型重构时:提前做好完善的重构计划,有条不紊地分阶段来进行。每个阶段完成一小部分代码的重构,然后提交、测试、运行,发现没有问题之后,再继续进行下一阶段的重构,保证代码仓库中的代码一直处于可运行、逻辑正确的状态。每个阶段,我们都要控制好重构影响到的代码范围,考虑好如何兼容老的代码逻辑,必要的时候还需要写一些兼容过渡代码。只有这样,我们才能让每一阶段的重构都不至于耗时太长(最好一天就能完成),不至于与新的功能开发相冲突。

小型的重构:影响范围小,改动耗时短,所以,只要你愿意并且有时间,随时都可以去做。

2.单元测试:

单元测试相对于集成测试来说,测试的粒度更小一些。集成测试的测试对象是整个系统或者某个功能模块,而单元测试的测试对象是类或者函数,用来测试一个类和函数是否都按照预期的逻辑执行。这是代码层级的测试。

单元测试主要是测试程序员自己编写的代码逻辑的正确性,并非是端到端的集成测试,它不需要测试所依赖的外部系统(分布式锁、Wallet RPC 服务)的逻辑正确性。所以,如果代码中依赖了外部系统或者不可控组件,比如,需要依赖数据库、网络通信、文件系统等,那我们就需要将被测代码与外部系统解依赖,而这种解依赖的方法就叫作“mock”。所谓的 mock 就是用一个“假”的服务替换真正的服务。mock 的服务完全在我们的控制之下,模拟输出我们想要的数据。

3.代码解耦:

  • 封装与抽象:封装和抽象可以有效地隐藏实现的复杂性,隔离实现的易变性,给依赖的模块提供稳定且易用的抽象接口。
  • 中间件:引入中间层能简化模块或类之间的依赖关系。

第一阶段:引入一个中间层,包裹老的接口,提供新的接口定义;

第二阶段:新开发的代码依赖中间层提供的新接口;

第三阶段:将依赖老接口的代码改为调用新接口;

第四阶段:确保所有的代码都调用新接口之后,删除掉老的接口。

  • 模块化:合理地划分模块能有效地解耦代码,提高代码的可读性和可维护性。所以,我们在开发代码的时候,一定要有模块化意识,将每个模块都当作一个独立的 lib 一样来开发,只提供封装了内部实现细节的接口给其他模块使用,这样可以减少不同模块之间的耦合度。
  • 其他设计思想和原则:单一职责原则,基于接口而非实现编程,依赖注入,多用组合少用继承,迪米特法则。

4.编程规范:

命名与注释、代码风格和编程技巧。

  • 命名:命名的好坏,对于代码的可读性来说非常重要,甚至可以说是起决定性作用的;
  • 注释:目的就是让代码更容易看懂。只要符合这个要求的内容,你就可以将它写到注释里;
  • 类、函数多大才合适:在我的电脑上,如果要让一个函数的代码完整地显示在 IDE 中,那最大代码行数不能超过 50;
  • 一行代码多长最合适:一行代码最长不能超过 IDE 显示的宽度。需要滚动鼠标才能查看一行的全部代码,显然不利于代码的阅读;
  • 善用空行分割单元块:在类的成员变量与函数之间、静态成员变量与普通成员变量之间、各函数之间、甚至各成员变量之间,我们都可以通过添加空行的方式,让这些不同模块的代码之间,界限更加明确。写代码就类似写文章,善于应用空行,可以让代码的整体结构看起来更加有清晰、有条理;
  • 类中的成员排列顺序:在类中,成员变量排在函数的前面。成员变量之间或函数之间,都是按照“先静态(静态函数或静态成员变量)、后普通(非静态函数或非静态成员变量)”的方式来排列的。除此之外,成员变量之间或函数之间,还会按照作用域范围从大到小的顺序来排列,先写 public 成员变量或函数,然后是 protected 的,最后是 private 的;
  • 把代码分割成更小的单元块:大部分人阅读代码的习惯都是,先看整体再看细节。所以,我们要有模块化和抽象思维,善于将大块的复杂逻辑提炼成类或者函数,屏蔽掉细节,让阅读代码的人不至于迷失在细节中,这样能极大地提高代码的可读性;
  • 避免函数参数过多
  • 勿用函数参数来控制逻辑:不要在函数中使用布尔类型的标识参数来控制内部逻辑,true 的时候走这块逻辑,false 的时候走另一块逻辑。这明显违背了单一职责原则和接口隔离原则
  • 移除过深的浅套层次:代码嵌套层次过深往往是因为 if-else、switch-case、for 循环过度嵌套导致的。我个人建议,嵌套最好不超过两层,超过两层之后就要思考一下是否可以减少嵌套。过深的嵌套本身理解起来就比较费劲,除此之外,嵌套过深很容易因为代码多次缩进,导致嵌套内部的语句超过一行的长度而折成两行,影响代码的整洁;
  • 学会使用解释性变量:常用的用解释性变量来提高代码的可读性;

 

四.创建型设计模式:

创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。

1.单例模式(Singleton Design Pattern):

一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

  • 饿汉式:饿汉式的实现方式比较简单。在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载(在真正用到 IdGenerator 的时候,再创建实例);
  • 懒汉式:相对于饿汉式的优势是支持延迟加载;
  • 双重检测:再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式;
  • 静态内部类:利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载;
  • 枚举:实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

 

2.工厂模式(Factory):

工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂。

  • 简单工厂(Simple Factory):
  • 工厂方法(Factory Method):
  • 抽象工厂(Abstract Factory):

 

3.建造者模式:

如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,我们可以通过构造函数配合 set() 方法来解决。

  • 我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就又会出现参数列表很长的问题;
  • 如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合 set() 方法的设计思路,那这些依赖关系或约束条件的校验逻辑就无处安放了;
  • 如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露 set() 方法。构造函数配合 set() 方法来设置属性值的方式就不适用了。

 

4.原型模式:

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。

原型模式的实现方式:深拷贝和浅拷贝。

在 Java 语言中,Object 类的 clone() 方法执行的就是我们刚刚说的浅拷贝。它只会拷贝对象中的基本数据类型的数据(比如,int、long),以及引用对象(SearchWord)的内存地址,不会递归地拷贝引用对象本身。

 

单例模式用来创建全局唯一的对象。工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。原型模式针对创建成本比较大的对象,利用对已有对象进行复制的方式进行创建,以达到节省创建时间的目的。

 

五.结构型设计模式:

 

1.代理模式(Proxy Design Pattern):

在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

  • 静态代理:需要针对每个类都创建一个代理类,并且每个代理类中的代码都有点像模板式的“重复”代码,增加了维护成本和开发成本;
  • 动态代理(Dynamic Proxy):不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。动态代理底层依赖的就是 Java 的反射语法。

 

2.桥接模式(Bridge Design Pattern):

一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。

 

3.装饰器模式(Decorator Pattern):

装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。

 

4.适配器模式(Adapter Design Pattern):

这个模式就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。

 

5.代理/桥接/装饰器/适配器模式的区别:

  • 代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同;

  • 桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变;

  • 装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用;

  • 适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

 

6.门面模式(Facade Design Pattern):

门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。

 

7.组合模式(Composite Design Pattern):

将一组对象组织成树形结构,以表示一种“部分 - 整体”的层次结构。组合让客户端(在很多设计模式书籍中,“客户端”代指代码的使用者。)可以统一单个对象和组合对象的处理逻辑。

将一组对象组织成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。使用组合模式的前提在于,你的业务场景必须能够表示成树形结构。

 

8.享元模式(Flyweight Design Pattern):

享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。

  • 享元模式的实现:享元模式的代码实现,主要是通过工厂模式,在工厂类中,通过一个 Map 或者 List 来缓存已经创建好的享元对象,以达到复用的目的
  • 享元模式 VS 单例、缓存、对象池:应用单例模式是为了保证对象全局唯一。应用享元模式是为了实现对象复用,节省内存。缓存是为了提高访问效率,而非复用。池化技术中的“复用”理解为“重复使用”,主要是为了节省时间。

 

六.行为型设计模式:

1.观察者模式(Observer Design Pattern):

 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

 

2.模版模式(Template Method Design Pattern):

模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

 

3.策略模式(Strategy Design Pattern):

定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。

一个完整的策略模式就是由这三个部分组成的:

  • 策略类的定义比较简单,包含一个策略接口和一组实现这个接口的策略类;
  • 策略的创建由工厂类来完成,封装策略创建的细节;
  • 策略模式包含一组策略可选,客户端代码如何选择使用哪个策略,有两种确定方法:编译时静态确定和运行时动态确定。其中,“运行时动态确定”才是策略模式最典型的应用场景。

 

4.责任链模式(Chain Of Responsibility Design Pattern):

将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。

在职责链模式中,多个处理器(也就是刚刚定义中说的“接收对象”)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。

 

5.状态模式(Finite State Machine):

当状态改变时,这个对象的行为也会变,而看起来就像是这个类改变了一样。

 

6.迭代器模式(Iterator Design Pattern):

迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。

迭代器是用来遍历容器的,所以,一个完整的迭代器模式一般会涉及容器和容器迭代器两部分内容。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。

迭代器设计上的用途:

  • 首先,对于类似数组和链表这样的数据结构,遍历方式比较简单,直接使用 for 循环来遍历就足够了。但是,对于复杂的数据结构(比如树、图)来说,有各种复杂的遍历方式。比如,树有前中后序、按层遍历,图有深度优先、广度优先遍历等等。如果由客户端代码来实现这些遍历算法,势必增加开发成本,而且容易写错。如果将这部分遍历的逻辑写到容器类中,也会导致容器类代码的复杂性;
  • 其次,将游标指向的当前位置等信息,存储在迭代器类中,每个迭代器独享游标信息。这样,我们就可以创建多个不同的迭代器,同时对同一个容器进行遍历而互不影响;
  • 最后,容器和迭代器都提供了抽象的接口,方便我们在开发的时候,基于接口而非具体的实现编程。

 

7.访问者模式(Visitor Design Pattern):

允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。

 

8.备忘录模式(Memento Design Pattern):

在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。

这个模式的定义主要表达了两部分内容。一部分是,存储副本以便后期恢复。这一部分很好理解。另一部分是,要在不违背封装原则的前提下,进行对象的备份和恢复。

 

9.命令模式(Command Design Pattern):

命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。

落实到编码实现,命令模式用的最核心的实现手段,是将函数封装成对象。我们知道,C 语言支持函数指针,我们可以把函数当作变量传递来传递去。但是,在大部分编程语言中,函数没法儿作为参数传递给其他函数,也没法儿赋值给变量。借助命令模式,我们可以将函数封装成对象。具体来说就是,设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。从实现的角度来说,它类似我们之前讲过的回调。

当我们把函数封装成对象之后,对象就可以存储下来,方便控制执行。所以,命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,这才是命令模式能发挥独一无二作用的地方。

 

10.解释器模式(Interpreter Design Pattern):

解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。

解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。实际上,这里的“语言”不仅仅指我们平时说的中、英、日、法等各种语言。从广义上来讲,只要是能承载信息的载体,我们都可以称之为“语言”,比如,古代的结绳记事、盲文、哑语、摩斯密码等。

 

11.中介模式(Mediator Design Pattern):

中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。

实际上,中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。原来一个对象要跟 n 个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低了代码的复杂度,提高了代码的可读性和可维护性。

 

 

创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合或组装”问题,那行为型设计模式主要解决的就是“类或对象之间的交互”问题。

 

每个设计模式都应该由两部分组成:第一部分是应用场景,即这个模式可以解决哪类问题;第二部分是解决方案,即这个模式的设计思路和具体的代码实现。不过,代码实现并不是模式必须包含的。如果你单纯地只关注解决方案这一部分,甚至只关注代码实现,就会产生大部分模式看起来都很相似的错觉。

​​​​​​​

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

设计模式之美(王争)摘录总结 的相关文章

随机推荐

  • DDlog语言特征

    原文地址 这里写链接内容 DDlog是一种语法为datalog式 用来编写DeepDive应用的语言 一个ddlog程序会被编译成deepdive格式下的配置文件 我们通常用这个文件来运行我们的应用 一个ddlog程序由一组声明组成 这些声
  • 【Doxygen】Vscode 插件 DoxyGen Documentation Generator C语言详细设置

    DoxyGen Documentation Generatorc 这个VsCode插件可以设置C C 的doxygen格式自动填充 本文只介绍C语言的json文件配置 触发方式 doxdocgen c triggerSequence 输入
  • Eclipse查看JDK源码

    1 找到jdk 2 配置rt的源码
  • 关于Connection aborted等问题的解决

    错误类型 在爬虫中报如下的错误 requests exceptions ConnectionError Connection aborted RemoteDisconnected Remote end closed connection w
  • Win10远程桌面连接怎么用?(远程桌面连接教程)

    怎么开启远程桌面连接 电脑远程桌面连接怎么用 要使用远程桌面控制电脑 您需先在电脑上开启远程桌面连接并且两台电脑还需在同一个局域网中 否则您的设置端口转发才能使用远程桌面连 那么怎么开启远程桌面连接呢 具体操作步骤如下 步骤1 在Win10
  • Redis的String常用命令

    Redis 基础知识 不想key被更改 再key的后面加上nx eg 127 0 0 1 6379 gt set s 11 OK 127 0 0 1 6379 gt set ss 111 OK 127 0 0 1 6379 gt renam
  • Jest 单元测试术语解析:describe、it、expect、test

    jest测试代码片段 describe number test gt it 1 is true gt expect 1 toBeTruthy test 2 is true gt expect 2 toBeTruthy describe 描述
  • Vue研习录(08)——Vue引入Swiper知识详解及示例分析

    Vue研习录 08 Vue引入Swiper知识详解及示例分析 版权声明 一 Swiper 版权声明 本文原创作者 清风不渡 博客地址 https blog csdn net WXKKang 重拾前端记忆 记录学习笔记 现在进入Vue引入Sw
  • uniapp的webview实现左滑返回上一个页面

    uniapp默认左滑是关闭整个webview 而不是关闭当前页 实现思路 拦截webview的url跳转操作 将新url用webview组件重新打开 当左滑的时候 默认关闭的就是当前webview 继而跳转到上一次的页面中
  • 100 道 Linux 常见面试题,慢慢读~

    1 Linux 概述 1 1 什么是 Linux Linux 是一套免费使用和自由传播的类 Unix 操作系统 是一个基于 POSIX 和 Unix 的多用户 多任务 支持多线程和多 CPU 的操作系统 它能运行主要的 Unix 工具软件
  • 关于对话机器人,你需要了解这些技术

    对话系统 对话机器人 本质上是通过机器学习和人工智能等技术让机器理解人的语言 它包含了诸多学科方法的融合使用 是人工智能领域的一个技术集中演练营 图1给出了对话系统开发中涉及到的主要技术 对话系统技能进阶之路 图1给出的诸多对话系统相关技术
  • Linux SELinux

    Security Enhanced Linux SELinux 是一种Linux系统的安全体系结构 允许管理员对系统访问权限进行更精细的控制 它最初由美国国家安全局 NSA 开发 使用Linux安全模块 LSM 对Linux内核打了一系列补
  • emd分解MATLAB自带函数

    matlab 2018开始自带了emd和hht函数 现将emd函数用法总结如下 imf residual info emd x Interpolation pchip 或者 emd x Interpolation pchip 输入参数 x
  • DataWhale Linux——Task01 Linux简介

    目录 什么是Linux Linux的优点 常用发行版 什么是Linux Linux 全称为GNU Linux 是一种免费使用和自由传播的类UNIX操作系统 我们常说的Linux 是指Linux内核 Linux的优点 开源免费 多用户 多任务
  • 预备1-window常用快捷键

    window常用快捷键 快捷键有很多很多 开发程序 刚入门只需要掌握简单的 菜单键 和书本一样的按键 鼠标的右键 1 查看 菜单 v gt gt gt gt 常用于 隐藏桌面图标 gt gt gt 菜单 v d 2 刷新 菜单键 E 3 新
  • 多功能透明屏,在智能家居领域中,有哪些功能特点?显示、连接

    多功能透明屏是一种新型的显示技术 它能够在透明的表面上显示图像和视频 并且具有多种功能 这种屏幕可以应用于各种领域 如商业广告 智能家居 教育等 为用户提供更加便捷和多样化的体验 首先 多功能透明屏可以用于商业广告 传统的广告牌通常是单一的
  • 一文教你如何用C代码解析一段网络数据包?【含代码】

    本文的目的是通过随机截取的一段网络数据包 然后根据协议类型来解析出这段内存 学习本文需要掌握的基础知识 网络协议 C语言 Linux操作 抓包工具的使用 其中抓包工具的安装和使用见下文 一文包你学会网络数据抓包 视频教学链接如下 教你如何抓
  • shell编写监控httpd服务80端口脚本.间隔3秒监控端口如果服务关闭自动重启

    bin bash 实时每隔3秒监控HTTP服务状态 服务异常输出报警信息 并尝试启动 服务正常时提示正常运行 web netstat ant grep 80 awk print 4 awk F print 4 apachelog var l
  • Redis在项目中的应用

    在aop模式下使用redis 的方式 1 添加依赖
  • 设计模式之美(王争)摘录总结

    经过八个月的学习 极客时间中王争的设计模式之美 最近又重读两遍 读书百遍其义自现 真是遍受益匪浅啊 学习之后才知道代码可以这样设计这样优美的编写 觉得自己有摘录一些在学习中认为比较好的精华 以备自己以后的查用和复习 设计模式要干的事情就是解