Qicz’s Thoughts HUB

The creative and technical writing. Do more, challenge more, know more, be more.

软件设计原则及常用设计模式

软件设计原则

  • 开闭原则 Open-Closed Principle, OCP

软件实体如类、模块和函数应该对扩展开放,对修改关闭。 所谓的开闭,也正是对扩展和修改两个行为的一个原则。**用抽象构建框架,用实现扩展细节。**可提高软件系统的可复用性和可维护性。

  • 单一职责原则 Simple Responsibility Principle,SRP

不要存在多于一个导致类变更的原因。如有一个 Class 负责两个职责,一旦发生需求变化,修改其中一个职责的逻辑代码,有可能会导致另一个职责的功能发生故障——这个 Class 就存在了两个导致类变更的因素。此时,我们就需要给两个职责分别用两个 Class 来实现,实现解耦职责的清晰拆分。后期的需要变更维护互不影响。如此设计,可降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险。一个 Class/Interface/Method只负责一项职能.

  • 依赖倒置原则 Dependence Inversion Principle,DIP

设计代码结构时,高层模块不应该依赖底层模块,二者都应该是依赖其抽象。**抽象不应依赖细节;细节应该依赖抽象。**通过依赖倒置,可减少类与类的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并可以降低修改程序所造成的风险。【依赖注入

  • 接口隔离原则 Interface Segregation Principle,ISP

使用多个专门的接口,而不是用一个单一的总接口,客户端不应该依赖它不需要的接口。符合 高内聚低耦合,使得类具有很好的可读性、可扩展性和可维护性。 设计时应注意

  • 一个类对一类的依赖应该建立在最小的接口之上;
  • 建立单一接口,不要建立炮打臃肿的接口;
  • 尽量细化接口,接口中的方法尽量少(不是越少越好,适度)
  • 迪米特法则 Law of Demeter ,LoD / 最少知道原则(Least Knowledge Principle,LKP)

一个对象应该对其他对象保持最少的了解。尽量降低类与类之间的耦合。**主要强调只和朋友交流,不和陌生人说话。**出现在成员变量、方法的输入、输出参数中的类都可以称之为成员朋友类,而出现在方法体内部的类不属于朋友类。

  • 里氏替换原则 Liskov Substitution Principle,LSP

如果每一个类型为 T1 的对象 o1都有类型为 T2 的对应 o2,使用以 T1 定义的所有程序 P 在所有的对象 o1都替换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类。 一个软件实体如果适用于一个父类的话,那么一定适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而不影响程序原来的逻辑。子类可以扩展父类的功能,但不能改变父类原有的功能。

  1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;
  2. 子类中可以增加自己特有的方法;
  3. 当子类的方法重载Overloading父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松;
  4. 当子类的方法实现父类的方式时(重写 Override/重载 Overloading 或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等。

某种情况下会违背开闭原则。 该原则的优点:

  1. 约束继承泛滥,开闭原则的一种体现;
  2. 加强程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的维护性、扩展性,降低需要变更时引入的风险。
  • 合成复用原则 Composite/Aggregate Reuse Principle,CARP

尽量使用对象组合(Has-a)/聚合(Contain-a),而不是继承关系达到软件复用的目的。可以使用系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。继承:白箱复用,相当于把所有的实现细节暴露给子类。组合/聚合:黑箱复用,对类以外的对象是无法获取到其实现细节的。

设计原则总结

在实际开发中,并不是一定要求所有代码都遵循设计原则,要在适当的场景遵循设计原则,体现一种平衡取舍。

设计模式

工厂模式

  • 简单工厂模式 Simple Factory Pattern
    • 由一个工厂对象决定创建出哪一种产品类的实例;
    • 创建型模式,不属于 GOF(The authors Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides are often referred to as the GoF, or Gang of Four.)23 中设计模式;
    • 适用场景:
      • 工厂类负责创建的对象较少;
      • 客户端只需要传入工厂类的参数,对应如何创建对象的逻辑不需要关系。
    • 优点:
      • 只需传入一个正确的参数,就可以获取你所需要的对应,无须知道其创建的细节。
    • 缺点:
      • 工厂类职责过重,增加新的产品时需要修改工厂类的判断逻辑,违背开闭原则;
      • 不易于扩展过于复杂的产品结构。

eg:

1Logger.getLogger(ClassName)
  • 工厂方法模式 Factory Method Pattern
    • 定义一个创建对象的接口,让实现这个接口的类自己来决定实例化哪个类,工厂方法让类的实例化推迟到到子类中进行;
    • 创建型模式。
    • 使用场景:
      • 创建对象需要大量重复的代码;
      • 客户端(应用层)不依赖产品类实例如何创建、实现等细节;
      • 一个类通过其子类来指定创建哪个对象。
    • 优点:
      • 用户只需要管理所需产品对应的工厂,无须关心创建细节;
      • 加入新的产品符合开闭原则,提高了系统的可扩展性。
    • 缺点:
      • 类的个人易变得过多,增加了代码结构的复杂度;
      • 增加了系统的抽象性和理解难度。

VS 简单工厂模式,遵循开闭原则,增加新的产品时,有对应的产品的子工厂去完成。

  • 抽象工程模式 Abstract Factory Pattern
    • 提供一个创建一系列相关或相互依赖对象的接口,无须指定他们的具体的类;(在设计规则时,具体的业务逻辑都还未确定的情况,具体实现依靠工厂,高度抽象的工厂)
    • 创建型设计模式。
    • 适用场景:
      • 客户端(应用层)不依赖产品类实现如何被创建、实现等细节;(与工厂方法模式有相同处)
      • 强调一系列相关的产品对象(属于同一产品族)一起使用创建对象需要大量重复的代码;
      • 提供一个产品类的库,所有产品以同样的接口出现,从而使客户端不依赖于具体实现。
    • 优点:
      • 具体产品在应用层代码隔离,无须关心创建细节;
      • 将一个系列的产品族统一到一起创建。
    • 缺点:
      • 规定了所有可能被创建的产品集合,产品族中扩展新的产品困难,需要修改抽象工厂的接口;
      • 增加了系统的抽象性和理解难度;(与工厂方法模式有相同处)
      • 不符合开闭原则,顶级工厂在新增产品时,需要修改其实现,而这样修改会影响子工厂的变动。

单例模式 Singleton Pattern

点击查看 » 单例模式 «

原型模式Prototype Pattern

  • 原型实例指定创建对象的种类,通过靠谱这些原型创建新的对象
  • 调用者无须知道任何创建细节,不用调用构造函数
  • 创建型模式
  • 适用场景
    • 类初始化消耗资源较多
    • new产生的一个对象需要繁琐的过程(数据准备,访问权限等)
    • 构造函数较复杂
    • 循环体中生产大量对象

代理模式 Proxy Pattern

  • 造成系统设计中类的数目增加
  • 客户端与目标对象(委托类)之间加了一个代理对象(代理类),造成请求处理速度变慢
  • 增加系统的复杂度

委派模式 Delegate Pattern

  • 精简程序逻辑,提升代码可读性
  • 全权静态代理
  • 不属于 GOF23 之一
  • 行为型模式
  • 命名含:Delegate或Dispatcher

eg:

1HttpServlet

策略模式 Strategy Pattern

  • 定义了算法家族、分别封装起来,让它们之间可以相互替换,此模式让算法的变化不会影响到使用算法的用户;
  • 消除程序中大量的if…else… 和switch语句。
  • 提供给用户选择,关心的是结果,不能干预逻辑和流程,只有选择权和干预权。
  • 优点:
    • 符合开闭原则;
    • 避免使用多重条件转移原件,if…else…语句、switch 语句 ;
    • 提高算法的保密性和安全性。
  • 缺点:
    • 客户端必须知道所有的策略,并且自行决定使用哪一个策略类;
    • 代码中会产生非常多策略类,增加维护难度。

模板模式 Template Pattern/模板方法模式Template Method Pattern

  • 适用场景
    • 一次性实现一个算法的不变部分,将可变的行为留给子类来实现;
    • 各子类中公共的行为被提取出来并集中到一个公共的父类中,避免代码的重复。
  • 钩子方法:控制流程的方法,交给子类去动态控制。
  • 关心的是流程,对流程可进行微调,有干预权,可干预内在逻辑。
  • 优点:
    • 提高代码的复用性;
    • 提高代码的扩展性;
    • 符合开闭原则。
  • 缺点:
    • 类数目增加;
    • 间接的增加了系统实现的复杂度;
    • 继承关系的自身缺点,如父类添加新的抽象方法,所有子类都需要改一遍。

适配器模式 Adapter Pattern

  • 一个类的接口转化成客户期望的另一个接口,使原本的接口不兼容(support)的类可以一起工作。

  • 结构型设计模式。

  • 适用场景:

    • 已经存在的类,它的方法和需求不匹配(方法结果相同或相似)的情况;
    • 适配器模式不是软件设计阶段需要考虑的设计模式,是随着软件维护由于不同产品、不同厂家造成功能类似而接口不相同情况下的解决方案。
  • 有点类似静态代理

  • 优点:

    • 提高类的透明性和复用,现有的类复用但不需要改变;
    • 目标类和适配器类解耦,提供程序的扩展性;
    • 在很多业务场景中符合开闭原则。
  • 缺点:

    • 适配器编写过程需要全面考虑,可能会增加系统的复杂性;
    • 增加代码的阅读难度,降低代码可读性,过度使用适配器会使系统代码变得凌乱。

装饰器模式 Decorator Pattern

  • 对象组合(Has-a)/聚合(Contain-a)??
  • 比继承更具弹性
  • 优点:
    • 继承的有力补充,比继承灵活,不改变原有对象的情况下动态地给一个对象扩展功能,即插即用;
    • 通过使用不同装饰类以及这些装饰类的排列组合,可以实现不同效果;
    • 装饰者完全遵守开闭原则。
  • 缺点:
    • 会出现更多的代码,更多的类,增加程序复杂性;
    • 动态装饰时,多层装饰时会更复杂。

装饰者与适配器对比

装饰者模式Decorator 适配器模式Adapter
形式 有对应的层级关系,一种非常特别的适配器模式 没有层级关系
定义 装饰者和被装饰者都实现同一个接口,主要目的是为了扩展之后依旧保留 OOP 关系 适配器和被适配者没有必然的联系,通常是采用继承或者代理的形式进行包装
关系 满足 is-a 的关系 满足 has-a 的关系
功能 注重覆盖、扩展 注重兼容、装换
设计 前置考虑 后置考虑,到了一定的情况下,再去考虑“亡羊补牢”的感觉

观察者模式 Observer Pattern/发布订阅模式 Pub/Sub

  • 行为型模式;
  • 优点:
    • 观察者 observer 和被观察者 observable之间建立了一个抽象的耦合;
    • 观察者模式支持广播通信。
  • 缺点:
    • 观察者之间有过多的细节依赖、提高时间消耗及程序的复杂度;
    • 使用要得当,要避免循环调用。

各种模式的对比

  • 单例和工厂模式

工厂一般会设计为单例,ApplicationContext

  • 策略和工厂模式
  1. 工厂模式包含工厂方法和抽象工厂,属于创建型模式(new);策略模式属于行为模式(invoke)
  • 工厂模式主要目的是封装好创建逻辑,策略模式接收工厂创建好的对象,从而实现不同的行为。
  • 策略和委派模式
  1. 策略模式是委派模式内部的一种实现形式,其关注的是结果是否能相互替代;比如支付方式:Alipay,WechatPay,JDPay 等。
  • 委派模式更关注份分发和调度的过程。有可能采用 if…else…条件分支语句来分发,内部也可以使用策略模式。
  • 模板方法和工厂方法模式
  1. 工厂方法是模板方法的一种特殊实现;
  • 工厂方法只有一个步骤,是模板模式的特殊实现;
  • 模板模式其执行流程在父类已固化,不可变动。同时,子类实现的是父类的某一个步骤,也就是说子类可能有多个对父类具体步骤的完善。
  • 模板和策略模式
  1. 模板方法和策略都有封装算法;
  • 策略是使不同算法可以相互替换,且不影响客户端应用层的使用;
  • 模板方法是针对定义一个算法的流程,将一些有细微差异的部分交给子类实现,不可被替换;
  • 模板模式不能改变算法流程,策略模式可以改变算法流程且可替换,策略模式通常用来代替 if…else…等条件分支语句。
  • 装饰者和静态代理模式
  1. 装饰者模式关注点在于给对象动态扩展、添加方法,而代理更加注重控制对对象的访问;
  • 代理模式通常会在代理类中创建被代理对象的实例,而装饰者模式通常把被装饰者作为构成参数。
  • 装饰者和适配器模式
  1. 装饰者模式和适配器模式都属于包装器模式(Wrapper);
  • 装饰者模式可以实现被装饰者与相同的接口或者继承被装饰者作为它的子类,而适配器和呗适配者可以实现不同的接口。
  • 适配器和策略模式

在适配业务复杂的情况下,利用策略模式优化动态适配逻辑。

设计模式总结

设计模式Pattern 一句话 举例
工厂模式Factory 只对结果负责,封装创建过程。 BeanFactory、Calendar
单例模式 Singleton 保证独一无二 ApplicationContext、Calendar
原型模式 Prototype 拔一根猴毛,吹出千万个(多例模式) ArrayList、PrototypeBean
代理模式 Proxy 宅人办事,增强职责 ProxyFactoryBean、JdkDynamicAopProxy、CglibAopProxy
委派模式 Delegate 干活算你的(普通员工),功劳算我的(项目经理) DIspatcherServlet、BeanDefinitionPaserDelegate
策略模式 Strategy 用户选择,结果统一 InstantiationStrategy
模板模式 Template 流程标准化、自己实现定制 JdbcTemplate、HttpServlet
适配器 Adapter 兼容转换头 AdvisorAdapter、HandlerAdapter
装饰器模式 Decorator 包装、同宗同源 BufferReader、InputStream、OutputStream、HttpHeadResponseDecorator
观察值模式 Observer 任务完成时通知 ContextLoaderListener