Skip to content
0

软件设计模式

设计原则

单一职责原则

一个类应该有且只有一个引起它变化的原因。

简单理解就是,一个类应该只负责一件事。不要把很多功能多塞到一个类中。如果一个方法处理的事情太多,其颗粒度会变得很粗,不利于复用和测试。

修正方法

  • 将大类拆分成多个小类:将类中的处理逻辑以流程图形式画出,然后把相似的逻辑(或者每个逻辑)作为一个类。

开闭原则

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

  • 对扩展开放:当需求变化时,我们可以通过添加新的代码来扩展系统的行为,而不是修改已有的代码。
  • 对修改关闭:现有的、已经测试通过的代码应该尽量不修改,避免引入新的错误。

修正方法

  • 使用类继承来改造代码:找到父类中会变动的部分,将其抽象成新的方法,最终允许新的子类来重写它以改变类的行为。常见如多态。
  • 使用组合与依赖注入来改造代码:在实例化时通过参数将业务逻辑的变化点注入到实例中,比如这个参数可以是一些过滤算法的类(定义抽象类,定义统一的接口),可根据需要传入不同的过滤算法类实例。

里氏替换原则

所有引用基类的地方必须能够透明地使用其子类的对象,而程序的行为不会发生变化。

也就是说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法,如果一定要重写,那这个方法在父类应该是抽象方法或接口。

使用继承时,要时刻考虑:把使用父类的地方换成这个子类,程序是否还能正常运行?

修正方法

  • 如果“对象不能支持某种操作”,本身就是这个类型的核心特征之一,那我们在进行父类设计时,就应该把这个核心特征设计进去。 子类不能只是简单通过抛出异常的方式对某个类方法进行“退化”。
  • 子类方法和父类应该返回同一类型的结果,支持同样的操作。或者更进一步,返回支持更多种操作的子类型结果也是可以接受的。
  • 子类方法的参数签名和父类完全一致,或者添加参数使其更加宽松,而不能减少参数或修改参数类型。

例子:企鹅、鸵鸟和几维鸟从生物学的角度来划分,它们属于鸟类;但从类的继承关系来看,由于它们不能继承“鸟”会飞的功能,所以它们不能定义成“鸟”的子类。同样,由于“气球鱼”不会游泳,所以不能定义成“鱼”的子类;“玩具炮”炸不了敌人,所以不能定义成“炮”的子类等。

解决方法:取消几维鸟等原来的继承关系,定义鸟和几维鸟的更一般的父类,如动物类,然后鸟类和几维鸟继承动物类,会飞的继承鸟类。

依赖倒置原则

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

面向接口编程,不要面向具体实现编程。

修正方法

  • 定义一个抽象类,类中定义高层模块所需要的方法,让高层模块和底层模块都依赖这个抽象类。
  • 定义抽象类解耦了高层模块和低层模块间的依赖关系,让代码变得更灵活,但也增加了额外的代码和理解成本。 只有对代码中那些现在或未来会发生变化的逻辑进行抽象,才能获得最大的收益。

例子:项目(高层模块)中用到了 requests 库(底层模块),那么就应该定义一个抽象类,类中定义接口,确定这个抽象层的职责,比如项目中通过 requests.text 获取文本,那就应该定义一个获取文本的方法 get_text()

接口隔离原则

客户端不应该依赖于它不需要的接口。

要为各个类建立它们需要的专用接口,而不要试图在一个类中去建立一个很庞大的接口或建立很多个接口供所有依赖它的类去调用,即使有些类可能只需要其中一个方法。

修正方法

  • 根据客户(高层模块)调用的方法把抽象层划分为更小的类。

常见违反案例:函数参数是一个很大的类,但函数只需要类中的一个属性。

迪米特法则

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

如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

例子:订单处理类(OrderProcessor)需要获取订单(Order)的用户(User)信息和商品(Goods)信息等,不应该在订单处理类中调用用户类和商品类的方法,而应该通过直接朋友 Order 类来获取这些信息。

合成复用原则

在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。如果要使用继承关系,则必须严格遵循里氏替换原则。

设计模式

分类

根据目的分类

  • 创建型

    用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。

  • 结构型

    用于描述如何将类或对象按某种布局组成更大的结构。

  • 行为型

    用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。

根据作用范围

  • 类模式

    用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。GoF 中的工厂方法、适配器、模板方法、解释器属于类模式。

  • 对象模式

    用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。GoF 中除了类模式,其他的都是对象模式。

创建型

工厂方法(Factory Method)

定义⼀个创建对象的接口,让其子类决定实例化哪⼀个工厂类,将创建过程延迟到子类进行。

抽象工厂(Abstract Factory)

提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

建造者模式(Builder)

将一个复杂类的构建与其表示相分离,使得相同的构建过程能够得出不同的表示。

单例模式(Singleton)

确保一个类只有一个实例对象,并提供一个访问它的全局访问点。

Brog模式

类似单例模式,通过共享状态的方式,可以有多个实例,但实例的属性访问的都是那个共享中的属性。

记忆

工人抽空,单独建造原型。

原型模式(Prototype)

允许对象在不了解要创建对象的确切类以及如何创建等细节的情况下创建自定义对象,通过拷贝原型对象来创建新对象。

结构型

适配器模式(Adapter)

通过转换接口,将一个类的接口转换成客户希望的另外一个接口,使原本接口不兼容而不能一起工作的类得以协同工作。

桥接模式(Bridge)

将类的抽象部分与实现部分分离,使它们都可以独立地变化。接口与实现分离

典型应用场景就是多种组合出现笛卡尔积的情况。比如:支付平台有微信支付、支付宝、云闪付等,这些平台都支持人脸、密码、指纹三种支付方式,现在要开发一个集成所有支付平台的系统,并支持这三种支付方式。

组合模式(Composite)

将对象组合成树形结构以表示“部分-整体”的层次结构。

组合模式使用户对单个对象和组合对象的使用具有⼀致性。

装饰模式(Decorator)

动态地给对象添加一些额外的职责。

外观模式(Facade)

定义一个统一的接口供客户端能访问所有子系统,将客户端与子系统解耦。

享元模式(Flyweight)

运用共享技术有效地支持大量细粒度的对象。

代理模式(Proxy)

为其他对象提供一种代理以控制对这个对象的访问。

记忆

(代理)(享元)的外观****是(适配器)需要(桥接)(装饰器)并组合的。

行为型

职责链模式(Chain of Responsibility)

避免请求的发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成⼀条链,并且沿着这条链传递请求,直到有对象能处理它为止。

命令模式(Command)

将一个请求封装成一个对象,从而可以用不同的请求对客户进行参数化。

解释器模式(Interpreter)

可以解释定义其语法表示的语言,还提供了用表示来解释语言中的语句的解释器。

迭代器模式(Iterator)

提供一种方法顺序访问一个聚合对象中的各个元素,而又无须暴露该对象的内部表示。

中介者模式(Mediator)

用⼀个中介对象来封装⼀系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

将“网状型”关系结构转为耦合度低的“星型结构”。

备忘录模式(Memento)

在不破坏封装性的前提下,捕获⼀个对象的内部状态(快照),并在该对象之外保存这个状态。

观察者模式(Observer)

定义对象间的⼀种⼀对多的依赖关系,当⼀个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

状态模式(State)

允许对象在内部状态发生改变时改变它的行为。

策略模式(Strategy)

定义一系列算法并把它们都封装起来,且使它们可以相互替换。

模板方法模式(Template Method)

定义⼀个操作中的算法骨架,而将⼀些步骤延迟到子类中,使得子类可以不改变⼀个算法的结构即可重定义该算法的某些特定步骤。

访问者模式(Visitor)

将数据结构与数据操作分离。

记忆

(中介者)于多次(迭代器)命令(职责链)(备忘录),(策略)(模板)(观察者)(状态),(访问者)解释

总结

  • 工厂模式最终返回的是一个具体的产品类,但策略模式注重的使用一种策略后的结果,策略可以任意替换,但用户只关心返回的结果,所以可以提供一个统一的接口给用户。
  • 状态模式策略模式都有上下文管理器,但状态模式需要在状态类中有上下文管理器类,而策略模式的策略类则无需上下文管理类。
  • 代理模式中的代理类和真实主题类的接口是一样的;而在适配器模式中则不同。