设计模式概要介绍

大家好,这里是编程Cookbook,关注公众号「编程Cookbook」,获取更多面试资料。本文是对设计模式的概要介绍,包括 23 种设计模式和一些设计原则。


文章目录


优秀代码的特点

关注公众号「编程Cookbook」,获取更多编程学习/面试资料!

我们所说的优秀代码通常具有以下特点:

  1. 可读性:命名规范、结构清晰、注释适当。
  2. 可维护性:低耦合高内聚、遵循设计原则和模式、易于修改扩展。
  3. 可靠性:功能正确、运行稳定、确保安全。
  4. 性能优化:时间复杂度优、空间复杂度优、资源利用高效。
  5. 可测试性:单元测试友好、可模拟隔离。
  6. 兼容性:跨平台兼容、版本兼容。

设计模式基础概念

设计模式及其作用

什么是设计模式?

设计模式是解决软件设计中常见问题的经验总结,它提供了一套经过验证的、可重用的解决方案模板。设计模式并不是具体的代码,而是一种设计思想或方法论,用于指导开发者编写高质量、可维护、可扩展的代码。

设计模式的作用

  1. 提高代码复用性:通过提供通用的解决方案,避免重复造轮子。
  2. 提升代码可维护性:使代码结构清晰,易于理解和修改。
  3. 增强代码扩展性:通过松耦合的设计,方便未来功能的扩展。
  4. 促进团队协作:设计模式是通用的设计语言,便于团队成员之间的沟通和理解。
  5. 解决特定问题:针对常见的设计问题(如对象创建、接口适配、行为管理等),提供标准化的解决方案。

一句话概括

设计模式:

  • 软件设计中常见问题的可重用解决方案模板,用于提高代码的复用性、可维护性和扩展性

为什么要用设计模式?

  • 因为设计模式能帮助我们以优雅的方式解决复杂的软件设计问题,避免重复劳动,同时使代码更易于理解、维护和扩展。

简要概括23种设计模式

关注公众号「编程Cookbook」,获取更多编程学习/面试资料!

1. 创建型模式(Creational Patterns)

目的

用于创建对象的模式,同时隐藏对象创建的逻辑避免代码中出现大量new操作和复杂的创建逻辑,以解耦对象的创建和使用

常用模式

单例模式(Singleton Pattern)
  • 定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
  • 应用场景:适用于资源管理类,如数据库连接池、线程池,确保整个应用程序中只存在一个实例,避免资源浪费和冲突;也常用于配置文件读取类,保证配置信息读取的一致性。
工厂模式(Factory Pattern)
  • 定义:定义一个创建对象的接口,由子类决定实例化哪个类
  • 应用场景:在对象创建过程复杂,或者需要根据不同条件创建不同类型对象时使用,比如游戏开发中创建不同类型角色,或图形绘制系统中创建不同形状图形。
建造者模式(Builder Pattern)
  • 定义:将复杂对象的构建与其表示分离,使同样的构建过程能创建不同表示
  • 应用场景:用于创建复杂对象,且对象有多个可选部件或步骤的场景,例如电脑组装,可根据不同需求组装不同配置电脑;房屋建造,按不同设计要求建造不同房屋。

不常用模式

原型模式(Prototype Pattern)
  • 定义:用原型实例指定创建对象的种类,通过拷贝原型创建新对象
  • 应用场景:适用于创建成本大(如初始化时间长、占用资源多)的对象,且对象状态初始化后变化不大的场景,如在图形绘制系统创建大量相似图形,或频繁创建复杂初始化参数的机器学习模型对象

2. 结构型模式(Structural Patterns)

目的

用于处理对象组合的结构,关注类与对象的组合,通过组合对象或类的方式,形成更大的结构

常用模式

适配器模式(Adapter Pattern)
  • 定义:将一个类的接口转换成客户希望的另一个接口,使原本接口不兼容的类能一起工作
  • 应用场景需要复用现存类,但接口与复用环境不兼容时使用,比如使用不同厂商的数据库接口,通过适配器统一接口;或者在已有系统中集成新的第三方库,该库接口与现有系统不匹配时。
桥接模式(Bridge Pattern)
  • 定义将抽象部分与实现部分分离,使它们能独立变化
  • 应用场景:当有多个维度变化时适用,例如,不同图形(圆形、矩形等)和不同颜色(红色、蓝色等)组合,通过桥接模式可独立扩展图形和颜色,避免在多个维度变化时类数量呈指数级增长。
组合模式(Composite Pattern)
  • 定义将对象组合成树形结构以表示“部分 - 整体”层次结构,使用户对单个对象和组合对象的使用具有一致性。
  • 应用场景:常用于处理树形结构数据,如文件系统(文件和文件夹)、组织机构(部门和员工)等,方便对整体和部分进行统一操作
装饰器模式(Decorator Pattern)
  • 定义:动态给对象添加额外职责,相比继承更灵活
  • 应用场景:在不改变现有对象结构的情况下,给对象动态添加功能,例如给咖啡添加不同配料(糖、牛奶等);在Web开发中,为Servlet添加不同功能的过滤器。
代理模式(Proxy Pattern)
  • 定义:为其他对象提供代理控制对该对象的访问
  • 应用场景:适用于远程代理(代表远程对象)、虚拟代理(在真正对象创建前占位)、保护代理(控制对象访问权限)等场景,如网络图片加载,先使用代理图片占位,真实图片加载后替换;或在系统中对敏感资源访问进行权限控制。

不常用模式

外观模式(Facade Pattern)
  • 定义:为子系统中的一组接口提供一致界面,定义高层接口使子系统更易使用
  • 应用场景:当需要简化复杂系统调用接口时使用,例如操作电脑硬件(CPU、硬盘、内存等),通过操作系统外观类统一操作,然而,在现代软件开发中,依赖注入等技术在一定程度上替代了外观模式的功能,所以使用频率相对较低。
享元模式(Flyweight Pattern)
  • 定义:运用共享技术有效支持大量细粒度对象。
  • 应用场景:适用于系统存在大量相似对象时,如游戏中的树木、棋子等对象,通过共享相同状态减少对象数量,但由于其实现复杂,且在现代硬件资源相对充足的情况下,性能提升不明显,所以使用较少。

关注公众号「编程Cookbook」,获取更多编程学习/面试资料!

3. 行为型模式(Behavioral Patterns)

目的

用于定义对象如何相互协作完成单个对象无法单独实现的任务,目的是定义类和对象间的通信方式

常用模式

观察者模式(Observer Pattern)
  • 定义:定义一对多依赖关系当主题对象状态变化时,通知所有观察者对象自动更新状态。
  • 应用场景:广泛应用于消息推送(发布者 - 订阅者模式)、股票行情监测等场景,当一个对象状态改变需通知其他多个对象时使用。
责任链模式(Chain of Responsibility Pattern)
  • 定义:为解除请求的发送者和接收者之间耦合,使多个对象都有机会处理请求,将这些对象连成一条链,并沿着链传递请求,直到有对象处理它为止
  • 应用场景适用于处理流程不确定,可能由多个对象中的一个或多个来处理请求的场景,例如请假审批流程,可能由组长、经理、总监等不同级别人员审批,请求在职责链上传递,直到被处理。
策略模式(Strategy Pattern)
  • 定义定义一系列算法,将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响到使用算法的客户
  • 应用场景:在一个系统中,某个行为有多种实现方式,且在运行时根据不同条件选择不同实现方式的场景,比如电商系统中不同的促销策略(满减、折扣、赠品等),在订单结算时根据订单条件选择合适的策略。
模板方法模式(Template Method Pattern)
  • 定义:定义操作算法骨架,将部分步骤延迟到子类实现,子类可在不改变算法结构的前提下重定义某些特定步骤
  • 应用场景:多个子类有相同方法,但某些步骤实现有差异时使用,例如冲泡不同类型的茶和咖啡(都有烧水、冲泡、倒入杯子等步骤,但冲泡材料不同)。
状态模式(State Pattern)
  • 定义允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
  • 应用场景:当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变行为时使用,比如游戏角色在不同状态(如正常、中毒、隐身等)下有不同行为。
迭代器模式(Iterator Pattern)
  • 定义:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。
  • 应用场景:在需要遍历聚合对象(如集合、数组等)元素,但不希望暴露聚合对象内部结构时使用,然而,在大多数编程语言中,已经提供了内置的迭代器支持,所以显式使用迭代器模式的场景相对较少

不常用模式

备忘录模式(Memento Pattern)
  • 定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态以便以后恢复到这个状态。
  • 应用场景:适用于需要保存和恢复对象状态的场景,如游戏存档、文本编辑器的撤销操作等,但由于保存对象状态可能占用大量资源,且实现过程相对复杂,所以在实际应用中使用频率不高。
命令模式(Command Pattern)
  • 定义:将请求封装为对象,可用不同请求对客户进行参数化,支持请求排队、记录日志及撤销操作。
  • 应用场景:适用于需要将请求发送者和接收者解耦,或实现命令的撤销、重做等操作的场景,例如菜单操作(每个菜单项对应一个命令)、遥控器按键操作,但在实际应用中,对于简单操作使用命令模式会增加系统复杂度,所以使用频率不高。
中介者模式(Mediator Pattern)
  • 定义:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
  • 应用场景:适用于多个对象之间存在复杂交互关系,导致对象间耦合度高的场景,例如,在机场调度系统中,飞机、跑道、塔台等对象之间的交互通过中介者进行管理,但在实际应用中,如果系统中对象交互关系简单,使用中介者模式会增加系统复杂性,所以使用不多。
访问者模式(Visitor Pattern)
  • 定义:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
  • 应用场景:当一个对象结构包含多种类型对象,且需要对这些对象执行不同操作,但又不想在这些对象类中添加过多方法时使用,例如,在编译器中,对不同语法树节点(如变量声明节点、表达式节点等)进行语义检查、代码生成等操作,但由于访问者模式增加了代码复杂性和维护难度,所以使用频率较低。
解释器模式(Interpreter Pattern)
  • 定义:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
  • 应用场景:当有一个简单语言需要解释执行,且可以将该语言中的语句表示为一个抽象语法树时使用。例如在小型特定领域编程语言(DSL)的实现中,对用该DSL编写的语句进行解析和执行操作可采用解释器模式;在一些简单的规则引擎设计中,如根据特定规则判断某些条件是否满足,也可应用此模式。但由于解释器模式在处理复杂语言和大量语句时,构建语法树和递归解释可能导致性能问题,并且实现复杂的文法会使系统变得庞大和难以维护,所以在实际项目中使用频率不是特别高。

关注公众号「编程Cookbook」,获取更多编程学习/面试资料!

设计原则

软件设计原则是指导软件设计的基本准则,旨在提高代码的可维护性、可扩展性和复用性。以下是面向对象设计的 五大原则(SOLID 即 单一职责、开闭原则、里氏替换、接口隔离、依赖倒置) 以及其他重要设计原则的详细介绍。

1. 单一职责原则(Single Responsibility Principle, SRP)

定义
一个类应该只有一个引起它变化的原因,即一个类只负责一项职责

特点

  • 优点
    • 提高类的内聚性,使类更容易理解和维护。
    • 降低类的复杂度,减少类之间的耦合。
  • 缺点
    • 可能会导致类的数量增加,增加系统的复杂度。

使用场景

  • 当一个类承担了多个职责时,可以将这些职责分离到不同的类中。

示例

type Report struct {
    content string
}

func (r *Report) Generate() string {
    return r.content
}

type ReportSaver struct{}

func (rs *ReportSaver) Save(report *Report) {
    // Save report to file
}

2. 开闭原则(Open/Closed Principle, OCP)

定义

软件实体(类、模块、函数等)应该对扩展开放,对修改关闭

特点

  • 优点
    • 提高系统的可扩展性,可以在不修改现有代码的情况下扩展功能。
    • 减少引入新功能时的风险
  • 缺点
    • 需要更多的抽象和设计,增加了系统的复杂度。

使用场景

  • 当需要扩展系统功能时,可以通过添加新类或模块来实现,而不是修改现有代码。这样可以避免因为修改现有代码而引入新的错误

示例

type Shape interface {
    Area() float64
}

type Rectangle struct {
    width, height float64
}

func (r *Rectangle) Area() float64 {
    return r.width * r.height
}

type Circle struct {
    radius float64
}

func (c *Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

关注公众号「编程Cookbook」,获取更多编程学习/面试资料!

3. 里氏替换原则(Liskov Substitution Principle, LSP)

定义

子类应该能够替换其父类,并且替换后不会影响程序的正确性

  • 这要求子类必须实现父类的所有抽象方法和属性,并且不能改变父类方法的前置条件和后置条件。

特点

  • 优点
    • 提高代码的可复用性和可维护性。
    • 确保继承关系的正确性。
  • 缺点
    • 需要仔细设计类的继承关系,避免违反原则。

使用场景

  • 当使用继承时,确保子类可以替换父类而不影响程序的正确性。

示例

type Bird interface {
    Fly() string
}

type Sparrow struct{}

func (s *Sparrow) Fly() string {
    return "Sparrow is flying"
}

type Ostrich struct{}

func (o *Ostrich) Fly() string {
    return "Ostrich cannot fly"
}

关注公众号「编程Cookbook」,获取更多编程学习/面试资料!

4. 接口隔离原则(Interface Segregation Principle, ISP)

定义

客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。这样可以避免客户端依赖一些不必要的方法。

特点

  • 优点
    • 提高接口的内聚性,减少接口的复杂度。
    • 减少类之间的耦合。
  • 缺点
    • 可能会导致接口数量增加,增加系统的复杂度。

使用场景

  • 当一个接口包含多个方法,而某些类只需要其中一部分方法时,可以将接口拆分为多个小接口。

示例

type Printer interface {
    Print(document string)
}

type Scanner interface {
    Scan() string
}

type MultiFunctionDevice interface {
    Printer
    Scanner
}

5. 依赖倒置原则(Dependency Inversion Principle, DIP)

定义

  • 高层模块不应该依赖低层模块,二者都应该依赖抽象。
  • 抽象不应该依赖细节,细节应该依赖抽象

特点

  • 优点
    • 提高代码的灵活性和可维护性。
    • 减少模块之间的耦合。
  • 缺点
    • 需要更多的抽象和设计,增加了系统的复杂度。

使用场景

  • 当需要解耦高层模块和低层模块时,可以通过依赖抽象来实现。

示例

type Database interface {
    Save(data string)
}

type MySQL struct{}

func (m *MySQL) Save(data string) {
    // Save data to MySQL
}

type Service struct {
    db Database
}

func NewService(db Database) *Service {
    return &Service{db: db}
}

func (s *Service) SaveData(data string) {
    s.db.Save(data)
}

额外解释:
假设我们有一个高层模块 “订单处理系统”,它需要依赖一个低层模块 “数据库存储” 来保存订单信息。如果直接依赖具体的数据库存储实现,比如 MySQL 数据库,那么当我们需要将数据库换成 Oracle 时,订单处理系统的代码就可能需要大量修改。


关注公众号「编程Cookbook」,获取更多编程学习/面试资料!

6. 迪米特法则 / 最少知识原则(Law of Demeter, LoD)

定义

一个对象应该对其他对象有最少的了解,即只与直接的朋友通信

特点

  • 优点
    • 减少类之间的耦合,提高系统的可维护性。
    • 提高代码的可读性和可理解性。
  • 缺点
    • 可能会导致类的数量增加,增加系统的复杂度。

使用场景

  • 当需要减少类之间的耦合时,可以通过限制类之间的直接交互来实现。

示例

type Person struct {
    name string
}

func (p *Person) GetName() string {
    return p.name
}

type Group struct {
    people []*Person
}

func (g *Group) GetPersonNames() []string {
    names := make([]string, 0)
    for _, person := range g.people {
        names = append(names, person.GetName())
    }
    return names
}

额外解释:
假设有一个学校系统,包含学生、班级和学校三个类。学生属于某个班级,班级属于某个学校。如果学生类需要获取学校的信息,按照迪米特法则,学生不应该直接与学校类交互,而是通过班级类来获取学校信息。因为班级是学生的直接朋友,而学校对于学生来说是陌生人。

总结

  • 单一职责原则:一个类只负责一项职责。
  • 开闭原则:对扩展开放,对修改关闭。
  • 里氏替换原则:子类可以替换父类而不影响程序的正确性。
  • 接口隔离原则:客户端不应该依赖它不需要的接口。
  • 依赖倒置原则:高层模块和低层模块都应该依赖抽象。
  • 迪米特法则:一个对象应该对其他对象有最少的了解。

关注公众号「编程Cookbook」,获取更多编程学习/面试资料!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值