设计模式概述

# 设计模式的来源

  • 来源于 Design Patterns:Elements of Reusable Object-Oriented Software 一书,创立了模式在软件设计中的地位。由 Erich Gamma 、Richard Helm、Ralph Johnson 和 John Vlissides合著,四人组 Gang of Four 简称 GOF 。总结了在面向对象语言开发过程中常见问题的解决方案。GoF23 种设计模式是面向对象设计模式的起点,而非终点。

  • 设计模式是面向对象语言开发过程中,遇到的种种场景和问题,然后提出的思路和解决方案,最后沉淀下来,就成了设计模式

  • 设计模式其实就是解决问题的思路,是前辈总结出来的有效方式方法,就是套路。学习设计模式,就是为了站在前辈的肩膀上,能够快捷优雅的解决面向对象程序开发设计问题。

  • 其他说法:帮助更好理解设计模式与面向对象

    每个设计模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。

    设计模式描述了软件设计过程中某一类常见问题的一般性的解决方案。

    面向对象设计模式描述了面向对象设计过程中,特定场景下,相互通信的对象之间常见的组织关系。

    除了GoF23种设计模式外还有其他更多的面向对象设计模式。

    面向对象设计模式解决的是类与相互通信的对象之间的组织关系,包括它们的角色、职责、协作方式几个方面。

    面向对象设计模式是好的面向对象设计,所谓好的面向对象设计是那些可以满足“应对变化,提高复用的设计”。

    面向对象设计模式描述的是软件设计,因此它是独立于编程语言的,但是面向对象设计模式的最终实现仍然要使用面向对象编程语言来表达。

    面向对象设计模式不像算法技巧,可以照搬照用,它是建立在对面向对象纯熟、深入的理解的基础上的经验性认识。掌握面向对象设计模式的前提是首先掌握“面向对象”。

# 如何学习设计模式

  • 具体场景和问题:设计模式是为了解决具体场景和问题而诞生的,因此任何设计模式的学习,都离不开一个具体的场景和问题。

  • 解决方案和思路:条条大道通罗马,一个问题的解决方案会有很多个,但是通过对比解读,才能找出最合适的解决方案。

  • 解决问题总结沉淀:搞定了问题,还需要总结沉淀,见微知著,举一反三,将来遇到同样的问题才有章可循,而这里的章法,就是设计模式。

  • 推广应用,实践出真知:解决一个特定的场景是没有意义的,更重要的在开发实践中去总结去应用,将设计模式融入自己的开发设计,只有真的解决了问题,才能变成自己的。

# 设计模式相关书籍

  • Design Patterns:Elements of Reusable Object-Oriented Software:世界顶级足球赛射门集锦。
  • 重构:改善既有代码的设计:一场精彩的足球赛。
  • 敏捷软件开发:原则、模式与实践:一场精彩的足球赛。
  • 设计模式解析:一场精彩的足球赛。
  • 面向对象分析与设计
  • Refactoring to Patterns

# 面向对象三大特征

  • 封装

提示

  • 封装是面向对象编程的核心思想。比如电视机,使用者只需要会按按钮就行,至于内部怎么设计的以及工作原理可以一概不知。
  • 封装其实就是对外隐藏复杂细节,提供简单易用接口,便于外界调用,从而提高系统的可扩展性、可维护性。
  • 核心思想把一部分代码封装起来,防止被随意定义和修改,可以被认为是一个保护屏障。

优点

  • 良好的封装能减少耦合。
  • 类内部的结构可以自由修改,可以保护代码安全。
  • 可以对成员变量进行更精确的控制。
  • 隐藏信息,实现细节。
  • 代码复用。
  • 高内聚低耦合,使不同对象,不同模块之间能更好的协同,同时便于修改,增强代码的可维护性。高内聚-类的内部数据操作细节自己完成,不允许外部干涉;低耦合-仅对外暴露少量的方法用于使用。
  • 继承

提示

  • 有时候我们希望基于某一类进行扩展,使一个类直接拥有基类的基本特征,而不需要重复去写,这就是继承的思想。继承原有的功能,增强自己新的功能,实现了拓展和复用。
  • 子类无法继承private修饰的属性和方法。
  • 多态

提示

同一个实体同时具有多种形式。同字面意思,即一个对象在不同情况下会有不同体现。实现某个功能的多个版本和多种途径。

# 软件开发四大指标(活字印刷的例子)

  • 可维护:要改,只需更改要改的地方,而不必全部推翻前期的工作。
  • 可扩展:需要添加新的东西,加入到既有的功能即可,而既有的东西却不可以发生改变。
  • 灵活性:可以用多种方式实现相关需求。
  • 可复用:在这个项目中用完还可以在其他项目中用,不是用完一次就毫无价值了。

# 类与类的六大关系

  • 依赖关系(Dependency)

    提示

    简单的理解,依赖就是一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是类B的变化会影响到类A。比如某人要过河,需要借用一条船,此时人与船之间的关系就是依赖。表现在代码层面,为类B作为参数被类A在某个方法中使用。在UML类图设计中,依赖关系用由类A指向类B的带箭头虚线表示。

  • 关联关系(Association)

    提示

    关联体现的是两个类之间语义级别的一种 强依赖关系,比如我和我的朋友,这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的。关联可以是单向、双向的。当一个类知道另一个类的时候可以用关联关系。表现在代码层面,为被关联类B以类的属性形式出现在关联类A中,也可能是关联类A引用了一个类型为被关联类B的全局变量。在UML类图设计中,关联关系用由关联类A指向被关联类B的带箭头实线表示。

  • 聚合关系(Aggregation)

    提示

    聚合是关联关系的一种特例,它体现的是整体与部分的关系,即 has-a 的关系。聚合体现的是一种弱拥有关系。此时整体与部分之间是可分离的,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。比如计算机与CPU、公司与员工的关系等。表现在代码层面,和关联关系是一致的,只能从语义级别来区分。在UML类图设计中,聚合关系以空心菱形加实线表示。(菱形指向整体)

  • 组合关系(Composition)

    提示

    组合也是关联关系的一种特例,这种关系比聚合更强,也称为强聚合。体现了一种强拥有关系。它同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束,比如人和人的大脑。表现在代码层面,和关联关系是一致的,只能从语义级别来区分。在UML类图设计中,组合关系以实心菱形加实线表示。

    • 继承关系(Generalization)

    提示

    继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力。在UML类图设计中,继承用一条带空心三角箭头的实线表示,从子类指向父类,或者子接口指向父接口。

  • 实现关系(Realization)

    提示

    实现指的是一个class类实现interface接口(可以是多个)的功能,实现是类与接口之间最常见的关系。在UML类图设计中,实现用一条带空心三角箭头的虚线表示,从类指向实现的接口。

  • 如何区别聚合、关联、组合和依赖关系

    注意

    关联是朋友关系,依赖是临时朋友关系、聚合是弱拥有关系、组合是强拥有关系。

UML类图

# 面向对象程序开发七大原则

  • 单一职责原则(Simple Responsibility Pinciple,SRP):一个类应该有且仅有一个引起变化的因素。
  • 开闭原则(Open-Closed Principle,OCP):对扩展开放,对修改封闭。面对新需求,对程序的改动是通过增加新代码来实现的,而不是修改现有的代码。

提示

  • 开闭原则是面向对象编程的核心原则。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护、可扩展、可复用和灵活性好。开发人员应该仅对程序中呈现出频繁变化的那部分做出抽象,然而,对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。
  • 对于扩展是开放的(Open for extension)。这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。也就是说,我们可以改变模块的功能。
  • 对于修改是关闭的(Closed for modification)。对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。模块的二进制可执行版本,无论是可链接的库、DLL或者.EXE文件,都无需改动。
  • 里氏替换原则(Liskov Substitution Principle,LSP):任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受影响时,基类才能真正的被复用,而衍生类也能够在基类的基础上增加新的行为。里氏替换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏替换原则是对实现抽象化的具体步骤的规范。
  • 依赖倒转原则(Dependence Inversion Principle,DIP):高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。简单的说就是要求对抽象(接口)进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

提示

依赖倒置是面向对象设计的标志,使用哪种语言编写程序并不重要,编写时如果考虑的都是针对抽象编程,而不是针对细节编程,即程序中的所有依赖关系都是终止于抽象类或者接口,那就是面向对象的设计,反之就是过程化的设计了。

  • 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。
  • 迪米特法则(Least Knowledge Principle,LKP):迪米特法则又叫做最少知识原则,就是说一个对象应当对其它对象有尽可能少的了解,不和陌生人说话。

提示

  • 在类的结构设计上,每一个类都应当降低成员的访问权限。也就是说一个类包装好自己的private状态,不需要让别的类知道的字段和行为就不要公开。需要公开的字段,通常用属性来体现。
  • 迪米特法则的根本思想是强调了类之间的松耦合。
  • 类之间的耦合越弱越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。也就是说信息的隐藏促进了软件的复用。
  • 合成复用原则(Composite/Aggregate Reuse Principle,CARP):合成复用原则要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。优先使用对象的合成复用将有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。
七大设计原则

# 面向对象开发二十三种经典设计模式

设计模式(design pattern)是解决软件开发某些特定问题而提出的一些解决方案,也可以理解成解决问题的一些思路。通过设计模式可以帮助我们增强代码的可重用性可扩充性可维护性灵活性好。我们使用设计模式最终的目的是实现代码的高内聚和低耦合

设计模式三大类分为创建型模式、行为型模式和结构型模式。

创建型模式:关注对象的创建,共6种。对象实例化的模式,创建型模式用于解耦对象的实例化过程。【其实就是如何`new一个对象`的问题】

  • 简单工厂模式(Simple Factory): 静态方法封装创建其他类实例的细节逻辑。
  • 抽象工厂模式(Abstract Factory):创建相关或依赖对象的家族,而无需明确指定具体类。
  • 工厂方法模式(Factory Method):一个工厂类根据传入的参量决定创建出哪一种产品类的实例。
  • 单例模式(Singleton):某个类只能有一个实例,提供一个全局的访问点。
  • 创建者模式(Builder):封装一个复杂对象的创建过程,并可以按步骤构造。创建者模式要求建造的过程必须是稳定的。
  • 原型模式(Prototype):通过复制现有的实例来创建新的实例。

结构型模式:关注类与类之间的关系,共 **7** 种。把类或对象结合在一起形成一个更大的结构。【其实就是折腾对象是如何的`组合和继承`的问题,为程序提供更好的扩展性和灵活性】

  • 适配器模式(Adapter):将一个类的方法接口转换成客户希望的另一个接口。
  • 桥接模式(Bridge):将抽象部分和实现部分分离,使它们都可以独立的变化。
  • 组合模式(Composite):将对象组合成树形结构以表示“部分-整体”的层次结构。
  • 装饰模式(Decorator):动态的给对象添加新的功能。
  • 外观模式(Facade):对外提供一个统一的方法,来访问子系统中的一群接口。
  • 享元模式(Flyweight):通过共享技术来有效的支持大量细粒度的对象。
  • 代理模式(Proxy):为对象提供一个代理以便控制这个对象的访问。

行为模型:关注对象和行为的分离,共 **11** 种。类和对象如何交互,划分职责和算法。【其实就是处理`方法放在哪个类里面更好`的问题,关注的内容更细腻,因此套路也更多】

  • 责任链模式(Chain of Responsibility):将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
  • 命令模式(Command):将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
  • 解释器模式(Interpreter):给定一个语言,定义它的文法的一种表示,并定义一个解释器。
  • 迭代器模式(Iterator):一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
  • 中介者模式(Mediator):用一个中介对象来封装一系列的对象交互。
  • 备忘录模式(Memento):在不破坏封装的前提下,保持对象的内部状态。
  • 状态模式(State):允许一个对象在其对象内部状态改变时改变它的行为。
  • 策略模式(Strategy):定义一系列算法,把他们封装起来,并且使它们可以相互替换。
  • 模板方法模式(TemplateMehond):定义一个算法结构,而将一些步骤延迟到子类实现。
  • 观察者模式(Observer):对象间的一对多的依赖关系。
  • 访问者模式(Visitor):不改变数据结构的前提下,增加作用于一组对象元素的新功能。

# 设计模式之间的关系

  • 从迭代器开始,我们将类中数据结构的遍历和类的功能实现分离出来,本质上使用了工厂模式;
  • 其次我们学习了适配器模式,它将不同的接口进行适配,从而便于版本的兼容性以及其他功能;
  • 然后我们学习了模板方法,使用模板面向抽象编程,便于新的子类的实现和管理;
  • 之后学习了工厂模式,其实借用了模板模式来创建产品,是一种非常重要用处很广的一种方法;
  • 然后我们学习了单例模式,有懒汉式、饿汉式等,生成关于某个类全局唯一的对象,注意多线程的影响;
  • 之后是原型模式,用来复制复杂的对象,使用了clone方法,然后是builder模式,用一个新的类对已有的抽象接口进行整合和编程,从而构建出我们想要的东西;
  • 然后是抽象工厂模式,使用了工厂模式,组合模式等模式,面向抽象编程,将抽象零件组装成抽象产品,便于具体工厂的创建,提高了代码的组件化和复用性;
  • 然后是桥接模式,将类的功能层次和实现层次分割开来,便于对应的扩展和使用;
  • 然后是策略模式,可以整体的替换策略,使用也很广泛;然后是组合模式,保证了同根同源,通过委托添加自己构成递归,树形结构,将具有树形特点的对象组合起来;
  • 然后是装饰器模式,和组合模式的结构类似,同样是递归结构,从而可以不断的装饰,增加新的功能,很好用;
  • 接着是visitor访问者模式,通过在类外访问类中的数据结构从而得到想要的结果,便于程序的可扩展性和组件化;
  • 接着是责任链模式,推卸责任,根据问题的大小来考虑自己释放处理,本质是链表,便于职责分明;
  • 然后是外观模式,通过整合各个类之间的调用关系,组建成了统一的接口(API),便于外部类的调用;
  • 接着是中介者模式,将很多类之间互相关联的关系交给仲裁者处理,省去了各个类之间的嵌套和调动,有利于高内聚和低耦合,思路清晰,便于扩展;
  • 然后是观察者模式,通过互相委托从而能够在被观察的类发生改变的时候得到相应的改变的信息并且处理;
  • 然后是备忘录模式,通过在某一时刻的状态保存下来,便于恢复,在游戏中使用的比较多;
  • 然后是状态模式,将状态当做类,从而职责分明,解除了很多繁琐的if和else这些分支逻辑,便于扩展;
  • 然后是享元模式,轻量级对象,通过共用不变对象来实现;
  • 然后是代理模式,懒加载真正的服务器,加快访问速度,代理是帮助服务器代理的;
  • 然后是命令模式,将命令当做类,通过保存一些列命令,从而能够随时执行这些命令,需要清除命令的本质就是一些操作和数据;
  • 最后是解释器模式,利用编程原理的方法,来更高层次的封装代码,将自己开发的代码当做编译系统,从而不用改变代码只修改更高语言层次的代码就能实现不同的功能。
  • 在设计模式的学习中,有人经常发出这样的疑问:代理模式和装饰者模式,策略模式和状态模式,策略模式和智能命令模式,这些模式的类图看起来几乎一模一样,它们到底有什么区别?实际上这种情况是普遍存在的,许多模式的类图看起来都差不多,模式只有放在具体的环境下才有意义。比如我们的手机,把它当电话的时候,它就是电话;把它当闹钟的时候,它就是闹钟;用它玩游戏的时候,它就是游戏机。我看到有人手中拿着iPhone18,但那实际上可能只是一个吹风机。有很多模式的类图和结构确实很相似,但这不太重要,辨别模式的关键是这个模式出现的场景,以及为我们解决了什么问题。

总之

一共有23种设计模式,可以说都是为了提高代码的可读性、可扩展性、可复用性、类的可替换性、组件化、可移植性等等特性。通过接口、抽象类、继承、实现、委托、抽象、面向接口编程、多态、重载、重写等方式使得代码的这些特性得以彰显,可以说只有深刻的理解了这些概念背后的哲学思想才能更好的理解设计模式。在设计模式中有很多思想,比如可以使用委托的不要使用继承、开闭原则,面向扩展开放,面向修改关闭,里氏替换原则,父类一定能被子类代替并使用,反置则不然,面向接口编程,功能层次和实现层次分离(桥接模式)、高内聚低耦合等思想,这些思想都是宝贵的,正是因为这样的思想的存在才使得代码的更新换代的时候能够尽可能少的甚至不用修改之前的代码,直接加入新的内容。提高软件的开发周期,便于维护和升级,便于查找和纠错,易于扩展和使用。 同样的设计模式主要分为三大类,创建型、行为型、结构型。我们可以简单的这样分类,只不过这样的分类似乎并不准确,不能一语道出所有的本质,设计模式是相互关联的,有的设计模式内部其实是使用了别的设计模式作为支撑的,但是大体上这样的一种划分便于我们去记忆,仅此而已。

设计模式关系图

# 锦囊妙计千千万

  • 聪明人懂得下笨功夫,一定要把基础打牢打扎实。
  • 重复的力量,多次重复践行后总有可以突破的时机。
  • 小型系统,随便开发,大型系统就需要架构设计和模式使用,这样系统才能有较好的生命力。
  • 熟悉设计模式是架构师的养成基本,要求对问题的分析与解法有一定的认知。
  • 抽象化的思考、封装与重用的设计是软件开发的精髓,不是落在纸上的代码。
  • 料敌机先、活学活用、设计模式可以当做软件开发中的独孤九剑。
  • 设计模式无非是在寻找软件中可能存在的变化,并封装这些变化。
  • 重构是使用设计模式最好的办法。
  • 活学活用是设计模式精髓之二。
  • 细粒度的基础模式,粗粒度的架构模式。
  • 模式是特定前提下重复出现问题的一个普遍解答,它是一种思想。
  • 要注意每个设计模式的意图、动机、适用性、结构和预期达到的效果。
  • 精彩的代码是如何想出来的,要比看到精彩的代码更加令人期待。
  • 如果说我比别人看的更远些,那是因为我站在了巨人的肩膀上。
  • 重要的不是设计模式本身,而是通过这些设计模式让你找到封装变化、对象之间松散耦合、针对接口编程的感觉。从而设计出灵活性好、易维护、易扩展、易复用的程序。
  • 如果说数学是思维的体操,那么设计模式就是面向对象编程思维的体操。
  • 使用设计模式境界,中间:不识庐山真面目,只缘身在此山中。最终:会当棱绝顶,一览众山小。
  • 在设计一个类时,除了功能外一定要考虑其灵活性、可扩展、可维护以及可复用因素,主要是为了应对随时可能发生的变化。
  • 球迷(软件使用者)-足球运动员(软件开发者)-球星(软件架构师)。
  • 当你程序中重复的代码多到一定程度,再来维护的时候,这将会是一场灾难。越大的系统,这种方式带来的问题越严重。
  • 一个程序员如果从来没有熬夜写程序的经历,不能算是一个好程序员,因为他们有痴迷过,所以他不会有大成就。
  • 一个方法如果过长,极有可能有坏味道了。
  • 研究历史是为了更好地迎接未来。
  • 深入地理解设计原则,很多设计模式其实就是设计原则的应用而已,或许在不知不觉中就在使用设计模式了。
  • 敏捷开发原则告诉我们,不要为代码添加基于猜测的,实际不需要的功能。如果不清楚一个系统是否需要命令模式,一般就不要着急去实现它,事实上,在需要的时候通过重构实现某一模式并不困难,只有在真正需要某个功能时,重构成相关模式才有意义。
  • 产品以稳定、高效、扩展为指标,项目以交付投产为目标。
  • 变化点要稳定就要做抽象。
  • 任何需求的变更都是需要成本的。
  • 有了新锤子,所有的东西看上去都成钉子了。
  • 使用继承关系时,一定要在是 is-a 的关系时再考虑使用,而不是任何时候都去使用。
  • Peter Norvig曾说,设计模式是对语言不足的补充,如果要使用设计模式,不如去找一门更好的语言。
上次更新: 2025/03/22, 13:47:44
最近更新
01
Git问题集合
01-29
02
安装 Nginx 服务器
01-25
03
安装 Docker 容器
01-25
更多文章>
×
×