「设计模式」六大原则之二:开闭职责小结

Posted 小羊子说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了「设计模式」六大原则之二:开闭职责小结相关的知识,希望对你有一定的参考价值。

文章目录


「设计模式」六大原则系列链接:
「设计模式」六大原则之一:单一职责小结
「设计模式」六大原则之二:开闭职责小结
「设计模式」六大原则之三:里氏替换原则小结
「设计模式」六大原则之四:接口隔离原则小结
「设计模式」六大原则之五:依赖倒置原则小结
「设计模式」六大原则之六:最小知识原则小结

六大原则体现很多编程的底层逻辑:高内聚、低耦合、面向接口编程、面向抽象编程,最终实现可读、可复用、可维护性。

设计模式的六大原则有:

  • Single Responsibility Principle:单一职责原则
  • Open Closed Principle:开闭原则
  • Liskov Substitution Principle:里氏替换原则
  • Law of Demeter:迪米特法则(最少知道原则)
  • Interface Segregation Principle:接口隔离原则
  • Dependence Inversion Principle:依赖倒置原则

把这六个原则的首字母联合起来( L 算做一个)就是 SOLID (solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计。

在 23 种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而存在的,主要遵从的设计原则就是开闭原则。

本文介绍 SOLID 中的第二个原则:开闭原则。

1.开闭原则定义

开闭原则的英文全称是 Open Closed Principle,简写为 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。我们把它翻译成中文就是:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。

添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

代码的扩展性是代码质量评判的最重要的标准之一。 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。

开闭原则的核心思想:面向抽象编程。

2. 如何理解“对扩展开放、对修改关闭”?

添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。

关于定义,我们有两点要注意。

第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。

第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。

3. 如何做到“对扩展开放、修改关闭”?

我们要时刻具备扩展意识、抽象意识、封装意识。在写代码的时候,我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。

在识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。

在众多的设计原则、思想、模式中,最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态等)。

实际上,多态、依赖注入、基于接口而非实现编程,以及前面提到的抽象意识,说的都是同一种设计思路,只是从不同的角度、不同的层面来阐述而已。这也体现了“很多设计原则、思想、模式都是相通的”这一思想。

4. 如何在项目中灵活应用开闭原则?

有一句话说得好,“唯一不变的只有变化本身”。

即便我们对业务、对系统有足够的了解,那也不可能识别出所有的扩展点,即便你能识别出所有的扩展点,为这些地方都预留扩展点,这样做的成本也是不可接受的。我们没必要为一些遥远的、不一定发生的需求去提前买单,做过度设计。

我们需要在可读性和可维护性做一个平衡。

5. 示例:

举例说明如何用继承体现 开闭原则的。

以某个付费课程为例,首先定义一个课程接口:

public interface ICourse 
    Integer getId();
    String getName();
    Double getPrice();

课程有Java、Web、androidios等。这里以 Android 课程为例:

public class AndroidCourse implements ICourse
    private Integer Id;
    private String name;
    private Double price;
    public AndroidCourse(Integer id, String name, Double price) 
        this.Id = id;
        this.name = name;
        this.price = price;
    
   public Double getPrice() 
        return this.price;
    
   // 其他 set和 get 省略

新需求来了,双十一要做活动,Android 课程 要打 7 折促销。

如果直接以之前的类 AndroidCourse ,会引入风险,我们如何在不修改原有代码的前提前下,实现价格优惠这个功能呢?

根据开闭原则的指导思想,我们再写一个处理优惠逻辑的类 AndroidDiscountCourse

public class AndroidDiscountCourse extends AndroidCourse 
    public JavaDiscountCourse(Integer id, String name, Double price) 
        super(id, name, price);
    
    public Double getOriginPrice()
        return super.getPrice();
    
    public Double getPrice()
        return super.getPrice() * 0.7;
    

通过继承父类扩展需要的方法,同时可以保留原有的方法,新增自己的方法。

6. 小结:

对拓展开放是为了应对变化(需求),对修改关闭是为了保证已有代码的稳定性;最终结果是为了让系统更有弹性!

开闭原则:基于接口或抽象实现“封闭”,基于实现接口或继承实现“开放”(拓展)。

参考:

《设计模式之美》

《重学 Java 设计模式》

以上是关于「设计模式」六大原则之二:开闭职责小结的主要内容,如果未能解决你的问题,请参考以下文章

「设计模式」六大原则之二:开闭职责小结

「设计模式」六大原则之二:开闭职责小结

「设计模式」六大原则之四:接口隔离原则小结

「设计模式」六大原则之四:接口隔离原则小结

「设计模式」六大原则之四:接口隔离原则小结

「设计模式」六大原则之四:接口隔离原则小结