软件设计模式白话文系列(一)设计模式介绍
前言
说说我为什么写软件设计模式白话文这个系列?作者初学设计模式时,有看过视频,有看过博客,敲过 demo 代码,这些资料基本都是通过介绍设计模式的实现方式、使用场景来学习,对这些模式的思想侧重点不高,这样时间是花费了不少,但是一直没有觉得自己把设计模式学习透彻,可以说只是了解和认识设计模式,并不能真正在项目开发中灵活运用。所以作者打算提供一系列从设计模式思维介绍设计模式的文章,同时为了利用费曼学习法梳理自己的知识体系,同时希望得到各位同行和前辈的指点。这个系列作者虽不能保证写的多么高深,但我会认真、用心的写下去,同时后续会一直更新、补充内容,在这里期望读者若有发现任何不妥之处,敬请批正,作者会在第一时间改正。当然,该系列文章若对您有些许帮助,作者将感觉莫大荣幸。
1、软件设计模式的概念
在软件工程中,设计模式
(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。
设计模式并不是直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。设计模式通常以类
或对象
来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类或对象。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。
并非所有的软件模式都是设计模式,设计模式特指软件设计
层次上的问题。还有其他非设计模式的模式,如架构模式
。同时,算法
不能算是一种设计模式,因为算法主要是用来解决计算上的问题,而非设计上的问题。
2、学习设计模式的好处
学习设计模式,可以让我们站在前人的肩膀上思考问题,可以提高我们的思维能力、编程能力、设计能力。
优秀项目的源码一般都会采用大量的设计模式思想,掌握设计模式,在阅读源码时,你就能够更快速地领会框架作者意图,可以大大提高我们源码阅读能力。
学习设计模式,让我们在分析业务需求时能以一种结构化的思维去考虑问题,这样写出来的代码会更简洁,更易于扩展。使程序设计更加标准化、代码编制更加工程化,同时开发效率会大大提高。
3、设计模式的表述格式
表述一个软件设计模式的格式根据作者的不同,划分和名称等都会有所不同。常用的 GoF 描述模式的格式大致分为以下这些部分:
- 模式名:每一个模式都有自己的名字,模式的名字使得我们可以讨论我们的设计。
- 问题:在面向对象的系统设计过程中反复出现的特定场合,它导致我们采用某个模式。
- 解决方案:上述问题的解决方案,其内容给出了设计的各个组成部分,它们之间的关系、职责划分和协作方式。
- 别名:一个模式可以有超过一个以上的名称。这些名称应该要在这一节注明。
- 动机:在哪种情况使用该模式,是本节提供的方案(包括问题与来龙去脉)的责任。
- 适用性:模式适用于哪些情况、模式的背景等等。
- 结构:这部分常用类图与交互图阐述此模式。
- 参与者:这部分提供一份本模式用到的类与对象清单,与它们在设计下扮演的角色。
- 合作:描述在此模式下,类与对象间的交互。
- 影响:采用该模式对软件系统其他部分的影响,比如对系统的扩充性、可移植性的影响。影响也包括负面的影响。这部分应描述使用本模式后的结果、副作用、与权衡(trade-off)。
- 实现:这部分应描述实现该模式、该模式的部分方案、实现该模式的可能技术、或者建议实现模式的方法。
- 示例:简略描绘出如何以编程语言来使用模式。
- 已知应用:业界已知的实现示例。
- 相关模式:这部分包括其他相关模式,以及与其他类似模式的不同。
4、设计模式在 Java 中的运用
设计模式是对面向对象
的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解后的⼀种开发设计指导思想
。是系统服务设计中针对常⻅场景的解决⽅案,可以解决功能逻辑开发中遇到的共性问题。
每⼀种设计模式都是解决某⼀类问题的概念模型,所以在实际的使⽤过程中,不要拘泥于某种已经存在的固定代码格式,⽽要根据实际的业务场景做出改变。 正因为设计模式的这种特点,所以即使是同⼀种设计模式,在不同的场景中也有不同的代码实现⽅式。另外,即便是相同的场 景,选择相同的设计模式,不同的研发⼈员也可能给出不⼀样的实现⽅案。所以,设计模式并不局限于最终的实现⽅案,⽽是在这种概念模型下,解决系统设计中的代码逻辑问题,重要的是学会和使用这种设计的思想。
5、设计模式的分类
我们普遍把设计模式分为创建型模式
、结构型模式
、行为型模式
三大类,共 23 种
5.1 创建型模式
用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式
- 单例
确保一个类只有一个实例,并提供对该实例的全局访问。 - 原型
用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象。 - 工厂方法
定义一个接口用于创建对象,但是让子类决定初始化哪个类。工厂方法把一个类的初始化下放到子类。 - 抽象工厂
为一个产品族提供了统一的创建接口。当需要这个产品族的某一系列的时候,可以从抽象工厂中选出相应的系列创建一个具体的工厂类。 - 建造者
也叫生成器模式,将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
5.2 结构型模式
用于描述如何将类或对象按某种布局组成更大的结构,GoF(四人组)书中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
-
代理
为其他对象提供一个代理以控制对这个对象的访问。 -
适配器
将某个类的接口转换成客户端期望的另一个接口表示。适配器模式可以消除由于接口不匹配所造成的类兼容性问题。
-
桥接
将一个抽象与实现解耦,以便两者可以独立的变化。 -
装饰
也叫修饰模式,向某个对象动态地添加更多的功能。修饰模式是除类继承外另一种扩展功能的方法。 -
外观
为子系统中的一组接口提供一个一致的界面, 外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 -
享元
通过共享以便有效的支持大量小颗粒对象。 -
组合
把多个对象组成树状结构来表示局部与整体,这样用户可以一样的对待单个对象和对象的组合。
5.3 行为型模式
用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。GoF(四人组)书中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。
-
模板方法
模板方法模式准备一个抽象类,将部分逻辑以具体方法及具体构造子类的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先构建一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现。
-
策略
定义一个算法的系列,将其各个分装,并且使他们有交互性。策略模式使得算法在用户使用的时候能独立的改变。 -
命令
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。 -
职责链
也叫责任链模式,为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。 -
状态
让一个对象在其内部状态改变的时候,其行为也随之改变。状态模式需要对每一个系统可能获取的状态创立一个状态类的子类。当系统的状态变化时,系统便改变所选的子类 -
观察者
在对象间定义一个一对多的联系性,由此当一个对象改变了状态,所有其他相关的对象会被通知并且自动刷新。 -
中介者
包装了一系列对象相互作用的方式,使得这些对象不必相互明显作用,从而使它们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用,保证这些作用可以彼此独立的变化。 -
迭代器
提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。 -
访问者
封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改,接受这个操作的数据结构可以保持不变。访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由的演化。 -
备忘录
备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捉住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。 -
解释器
给定一个语言, 定义它的文法的一种表示,并定义一个解释器, 该解释器使用该表示来解释语言中的句子。
6、软件设计原则
设计原则是软件设计模式必须尽量
遵循的原则,各种原则要求的侧重点不同。在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据软件设计原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。
6.1 开闭原则
软件实体应当对扩展开放,对修改关闭。即当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。
Java 中我们需要使用接口和抽象类。即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。这样提高代码的可复用性,粒度越小,被复用的可能性就越大;
好处:
- 提高测试效率:软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
- 可以提高代码的可复用性:在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
- 可以提高软件的可维护性:遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。
6.2 里氏替换原则
里氏代换原则是面向对象设计的基本原则之一。
任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
好处:
- 类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
6.3 依赖倒转原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。核心思想是:要面向接口编程,不要面向实现编程。
依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。
在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。这里的抽象指的是接口或者抽象类,而细节是指具体的实现类。
好处:
- 降低类间的耦合性。
- 提高系统的稳定性。
- 减少并行开发引起的风险。
- 提高代码的可读性和可维护性。
实现逻辑:
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
6.4 单一职责原则
又称单一功能原则,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。
该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:
-
一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
-
当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。
好处:
- 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
- 提高类的可读性。复杂性降低,自然其可读性会提高。
- 提高系统的可维护性。可读性提高,那自然更容易维护了。
- 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。
单一职责同样也适用于方法
。一个方法应该尽可能做好一件事情。如果一个方法处理的事情太多,其颗粒度会变得很粗,不利于重用。
6.5 接口隔离原则
要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
-
单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
-
单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
好处:
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
- 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
- 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
- 能减少项目工程中的代码冗余。过大的接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
实现逻辑:
- 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
6.6 迪米特法则
又叫作最少知道原则,果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
好处:
- 降低了类之间的耦合度,提高了模块的相对独立性。
- 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
实现逻辑:
- 从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
- 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
- 在类的结构设计上,尽量降低类成员的访问权限。
- 在类的设计上,优先考虑将一个类设置成不变类。
- 在对其他类的引用上,将引用其他对象的次数降到最低。
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
- 谨慎使用序列化(Serializable)功能。
6.6 合成复用原则
又叫组合/聚合复用原则,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。 如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
通常类的复用分为继承复用和合成复用。
继承复用虽然有简单和易实现的优点,但它也存在以下缺点。
-
继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
-
子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
-
它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。
-
它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
-
新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
-
复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
实现逻辑:
- 通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。
作者:eajur
出处:https://www.cnblogs.com/eajur/p/16824948.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】