ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

设计原则之【开放封闭原则】

2022-02-27 10:01:25  阅读:187  来源: 互联网

标签:原则 扩展 代码 封闭 书籍 修改 bookList 开放 public


设计原则是指导我们代码设计的一些经验总结,也就是“心法”;面向对象就是我们的“武器”;设计模式就是“招式”。

以心法为基础,以武器运用招式应对复杂的编程问题。

表妹今天上班又忘记打卡了

表妹:哥啊,我真的是一点记性都没有

我:发生什么事啦?

表妹:今天上班又忘记打卡了,又是白打工的一天,做什么事都提不起劲来。


你看,传统的上下班打卡制,这种模式将按时上下班作为考核指标之一,虽然强化了企业的管理,但是却限制了员工的时间自由,每个员工的情况和工作状态都不同,强制的上班时间容易导致员工为了应付打卡而打卡,实则工作效率却不高。

按时上下班其实不是老板希望达到的目的,老板希望的是,所有员工的绩效达标,最终企业能够盈利,而上下班打卡制只不过是为了达到这一目标的其中一个方法而已。

明确了将绩效作为考核指标。那么,绩效至少达标,这个是不可以修改的,在这个基础上,员工的上下班时间是可以自由安排的,这样就可以提高员工的生产效率了。这就是弹性上班制,对业绩成效的修改关闭,而对时间制度扩展的开放。

你看,这不就是我们软件开发中的开放-封闭原则嘛。


是说软件实体(类、模块、函数等)应该可以扩展,但是不可以修改。

这是一条最难理解和掌握,但是又最有用的设计原则。

之所以说难理解,是因为,“怎样的代码改动才被定义为扩展?怎样的代码改动才被定义为修改?怎样才算满足开闭原则?修改代码就一定违反了开闭原则吗?”等问题。

之所以说难掌握,是因为,“如何做到对扩展开放,修改封闭?,如何在项目中灵活地应用开闭原则,在保证扩展性的同时又不影响代码的可读性?”等问题。

之所以说最有用,是因为,扩展性是代码质量最重要的衡量标准之一。在23种经典设计模式之中,大部分设计模式都是为了解决代码的扩展性问题而存在的。

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

比如,书店销售图书。

图书有三个属性:书名、价格和作者。IBook是获取图书三个属性的接口,如下所示:

 1 public interface IBook { 
 2     // 图书的名称 
 3     public String getName(); 
 4 ​
 5     // 图书的售价 
 6     public int getPrice(); 
 7 ​
 8     // 图书的作者 
 9     public String getAuthor(); 
10 } 

 

小说类图书NovelBook是一个具体的实现类,如下所示:

 1 public class NovelBook implements IBook { 
 2     // 图书的名称 
 3     private String name; 
 4 ​
 5     // 图书的价格 
 6     private int price; 
 7 ​
 8     // 图书的作者 
 9     private String author; 
10 ​
11     // 通过构造函数传递书籍数据 
12     public NovelBook(String _name,int _price,String _author){ 
13         this.name = _name; 
14         this.price = _price; 
15         this.author = _author; 
16     } 
17 ​
18     // 获得作者是谁 
19     public String getAuthor() { 
20         return this.author; 
21     } 
22 ​
23     // 获得书名 
24     public String getName() { 
25         return this.name; 
26     } 
27 ​
28     // 获得图书的价格 
29     public int getPrice() {
30         return this.price; 
31     } 
32 } 

 

接下来,我们看一下,书店是如何销售图书的:

 1 public class BookStore { 
 2     private final static ArrayList<IBook> bookList = new ArrayList<IBook>(); 
 3 ​
 4     // 静态模块初始化,项目中一般是从持久层初始化产生 
 5     static{ 
 6         bookList.add(new NovelBook("天龙八部",3200,"金庸")); 
 7         bookList.add(new NovelBook("巴黎圣母院",5600,"雨果")); 
 8         bookList.add(new NovelBook("悲惨世界",3500,"雨果")); 
 9         bookList.add(new NovelBook("平凡的世界",4300,"路遥")); 
10     } 
11 ​
12     //模拟书店买书 
13     public static void main(String[] args) { 
14         NumberFormat formatter = NumberFormat.getCurrencyInstance(); 
15         formatter.setMaximumFractionDigits(2); 
16         System.out.println("------------书店中的小说类图书记录如下:---------------------"); 
17         for(IBook book:bookList){ 
18             System.out.println("书籍名称:" + book.getName()+"\t书籍作者:" + 
19             book.getAuthor()+  "\t书籍价格:"  +  formatter.format(book.getPrice()/100.0)+"元"); 
20         } 
21     } 
22 } 

注:在BookStore中声明了一个静态模块,实现了数据的初始化,这部分应该是从持久层产生的,由持久层工具进行管理。

运行结果如下:

------------书店中的小说类图书记录如下:--------------------- 
书籍名称:天龙八部  书籍作者:金庸  书籍价格:¥32.00元 
书籍名称:巴黎圣母院 书籍作者:雨果  书籍价格:¥56.00元 
书籍名称:悲惨世界  书籍作者:雨果  书籍价格:¥35.00元 
书籍名称:平凡的世界 书籍作者:路遥 书籍价格:¥43.00元 

 

但是,最近书店的小说类图书销量下滑很严重,所以,书店希望通过打折来刺激消费:所有40元及以上的小说类图书8折销售,40元以下的按9折销售。

对于已经投产的项目来说,这就是一个变化,那么,我们应该怎么应对呢?

有三种方法可以解决这个问题:

修改接口

在IBook上新增一个getOffPrice()的方法,专门进行打折处理。

首先,IBook作为接口应该是稳定可靠的,不应该经常发生变化,否则接口作为契约的作用就失去了意义。

其次,修改了接口,NovelBook实现类也要做相应的修改,这样,为了实现这个需求,改动的面积是比较大的。

修改实现类

修改NovelBook实现类中getPrice()的方法,这样,改动的面积相对比较小了,仅仅局限在NovelBook实现类中。但是这样的话,用户就无法获得图书的原价了。

通过扩展实现变化

增加一个子类OffNovelBook,复写getPrice()方法,高层次的模块(也就是static静态模块区)通过OffNovelBook类产生新的对象。如下所示:

public class OffNovelBook extends NovelBook { 
    public OffNovelBook(String _name,int _price,String _author){ 
        super(_name,_price,_author); 
    } 
​
    // 覆写销售价格 
    @Override 
    public int getPrice(){ 
        // 原价 
        int selfPrice = super.getPrice(); 
        int offPrice=0; 
        if(selfPrice < 4000){  // 原价低于40元,则打9折 
            offPrice = selfPrice * 90 /100; 
        }else{ 
            offPrice = selfPrice * 80 /100; 
        } 
        return offPrice; 
    } 
}

你看,仅仅扩展一个子类并复写getPrice()方法,就可以完成新增的业务。接下来看一下BookStore类的修改:

public class BookStore { 
    private final static ArrayList<IBook> bookList = new ArrayList<IBook>(); 
​
    // 静态模块初始化,项目中一般是从持久层初始化产生 
    static{ 
        // 换成打折的小说
        bookList.add(new OffNovelBook("天龙八部",3200,"金庸")); 
        bookList.add(new OffNovelBook("巴黎圣母院",5600,"雨果")); 
        bookList.add(new OffNovelBook("悲惨世界",3500,"雨果")); 
        bookList.add(new OffNovelBook("平凡的世界",4300,"路遥")); 
    } 
​
    // 模拟书店买书 
    public static void main(String[] args) { 
        NumberFormat formatter = NumberFormat.getCurrencyInstance(); 
        formatter.setMaximumFractionDigits(2); 
        System.out.println("------------书店中的小说类图书记录如下:---------------------"); 
        for(IBook book:bookList){ 
            System.out.println("书籍名称:" + book.getName()+"\t书籍作者:" + 
            book.getAuthor()+  "\t书籍价格:"  +  formatter.format(book.getPrice()/100.0)+"元"); 
        } 
    } 
} 

上面只修改了静态模块初始化部分,其他部分没有修改。运行结果如下:

------------书店中的小说类图书记录如下:--------------------- 
书籍名称:天龙八部  书籍作者:金庸  书籍价格:¥25.60元 
书籍名称:巴黎圣母院 书籍作者:雨果  书籍价格:¥50.40元 
书籍名称:悲惨世界  书籍作者:雨果  书籍价格:¥28.00元 
书籍名称:平凡的世界 书籍作者:路遥 书籍价格:¥38.70元 

上面这个例子,通过一处扩展,一处修改,实现了打折的新需求。可能有同学就会问:“这不还是修改了代码吗?”

修改代码就意味着违反了开闭原则吗?

BookStore类确实修改了,这部分属于高层次的模块。在业务规则改变的情况下,高层模块必须有部分改变以适应新业务。添加一个新功能,不可能任何模块、类、方法的代码都不“修改”,这个是做不到的。类需要创建、组装、并且做一些初始化操作,才能构建成可运行的程序,这部分代码的修改是在所难免的。

我们要做的是,尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。

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

实际上,开闭原则讲的就是代码的扩展性问题,是判断一段代码是否易扩展的“金标准”。

在讲具体的方法论之前,我们先来看一些更加偏向顶层的指导思想。为了尽量写出扩展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识。这些“潜意识”可能比任何开发技巧都重要。

扩展意识:在写代码的时候,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够灵活地插入到扩展点上,做到“对扩展开放、对修改关闭”。

抽象意识:提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换老的实现即可,上游系统的代码几乎不需要修改。

封装意识:在识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化。

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

设计模式这一块,我们另外再分享。今天重点学习一下,如何利用多态、依赖注入、基于接口而非实现编程,来实现“对扩展开放、对修改关闭”。

假如,我们现在要开发一个通过Kafka来发送异步消息。对于这样一个功能的开发,我们要学会将其抽象成一组跟具体消息队列(Kafka)无关的异步消息接口。所有上层系统都依赖这组抽象的接口编程,并且通过依赖注入的方式来调用。当我们要替换新的消息队列的时候,比如将Kafka替换成RocketMQ,可以很方便地拔掉老的消息队列实现,插入新的消息队列实现。

// 这一部分体现了抽象意识
public interface MessageQueue { //...}
public class KafkaMessageQueue implements MessageQueue { //...}
public class RocketMQMessageQueue implements MessageQueue { //...}
​
public interface MessageFromatter { //...}
public class JsonMessageFromatter implements MessageFromatter { //...}
public class ProtoBufMessageFromatter implements MessageFromatter { //...}
​
public class Demo {
    private MessageQueue msgQueue;        // 基于接口而非实现编程
    public Demo(MessageQueue msgQueue) {  // 依赖注入
        this.msgQueue = msgQueue;
    }
    
    // msgFormatter:多态、依赖注入
    public void sendNotification(Notification notification, MessageFormatter msg) {
        //..
    }
}

当然,开闭原则也不是免费的,有时候,代码的扩展性会跟可读性冲突。这个时候,我们就需要在两者之间做一个权衡。总之,没有一个放之四海而皆准的参考标准,全凭实际的应用场景来决定。

如何预留扩展点?

前面我们提到,写出支持“对扩展开放、对修改关闭”的代码的关键是预留扩展点,那么问题是,应该如何才能识别出所有可能的扩展点呢?

如果开发业务导向的系统,比如电商系统、物流系统、金融系统等,要想识别尽可能多的扩展点,就需要对业务本身有足够多的了解。

如果开发通用、偏底层的框架、类库、组件等,就需要了解它们会被如何使用,日后可能会添加什么功能。

“唯一不变的就是变化本身”,尽管我们对业务系统、框架功能有足够多的了解,也不能识别出所有的扩展点。即便我们能够识别出所有的扩展点,为这些地方做预留扩展点的设计,成本都是很大的,这就叫做“过度设计”

合理的做法,应该是对于一些比较确定的,短期内可能就会扩展,或者需要改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候,我们就可以事先做预留扩展点设计。但是对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,可以通过重构代码的方式来支持扩展的需求。

好啦,设计原则是否应用得当,应该根据具体的业务场景,具体分析。

总结

对扩展开放,是为了应付变化(需求);

对修改封闭,是为了保证已有代码的稳定性;

最终结果是为了让系统更有弹性

参考

《大话设计模式》

极客时间专栏《设计模式之美》

https://blog.csdn.net/sinat_20645961/article/details/48239347

标签:原则,扩展,代码,封闭,书籍,修改,bookList,开放,public
来源: https://www.cnblogs.com/Gopher-Wei/p/15941534.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有