企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
6.4 如何使用开闭原则 开闭原则是一个非常虚的原则,前面5个原则是对开闭原则的具体解释,但是开闭原则并不局限于这么多,它“虚”得没有边界,就像“好好学习,天天向上”的口号一样,告诉我们要好好学习,但是学什么,怎么学并没有告诉我们,需要去体会和掌握,开闭原则也是一个口号,那我们怎么把这个口号应用到实际工作中呢? 1. 抽象约束 抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:第一,通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;第三,抽象层尽量保持稳定,一旦确定即不允许修改。还是以书店为例,目前只是销售小说类书籍,单一经营毕竟是有风险的,于是书店新增加了计算机书籍,它不仅包含书籍名称、作者、价格等信息,还有一个独特的属性:面向的是什么领域,也就是它的范围,比如是和编程语言相关的,还是和数据库相关的,等等,修改后的类图如图6-3所示。 ![](https://box.kancloud.cn/2016-08-14_57b0035fd548e.jpg) 图6-3 增加业务品种后的书店售书类图 增加了一个接口IComputerBook和实现类Computer- Book,而BookStore不用做任何修改就可以完成书店销售计算机书籍的业务。计算机书籍接口如代码清单6-8所示。 代码清单6-8 计算机书籍接口 public interface IComputerBook extends IBook{            //计算机书籍是有一个范围      public String getScope(); } 很简单,计算机书籍增加了一个方法,就是获得该书籍的范围,同时继承IBook接口,毕竟计算机书籍也是书籍,其实现如代码清单6-9所示。 代码清单6-9 计算机书籍类 public class ComputerBook implements IComputerBook {      private String name;      private String scope;      private String author;      private int price;       public ComputerBook(String _name,int _price,String _author,String _scope){              this.name=_name;              this.price = _price;              this.author = _author;              this.scope = _scope;      }      public String getScope() {              return this.scope;      }      public String getAuthor() {              return this.author;      }      public String getName() {              return this.name;      }      public int getPrice() {              return this.price;      } } 这也很简单,实现IComputerBook就可以,而BookStore类没有做任何的修改,只是在static静态模块中增加一条数据,如代码清单6-10所示。 代码清单6-10 书店销售计算机书籍 public class BookStore {      private final static ArrayList<IBook> bookList = new ArrayList<IBook>();      //static静态模块初始化数据,实际项目中一般是由持久层完成      static{              bookList.add(new NovelBook("天龙八部",3200,"金庸"));              bookList.add(new NovelBook("巴黎圣母院",5600,"雨果"));              bookList.add(new NovelBook("悲惨世界",3500,"雨果"));              bookList.add(new NovelBook("金瓶梅",4300,"兰陵笑笑生"));              //增加计算机书籍              bookList.add(new ComputerBook("Think in Java",4300,"Bruce Eckel","编程语言"));      }        //模拟书店卖书      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)+"元");              }      } } 书店开始销售计算机书籍,运行结果如下所示。 --------------------书店卖出去的书籍记录如下:--------------------- 书籍名称:天龙八部    书籍作者:金庸     书籍价格:¥32.00元 书籍名称:巴黎圣母院   书籍作者:雨果     书籍价格:¥56.00元 书籍名称:悲惨世界    书籍作者:雨果     书籍价格:¥35.00元 书籍名称:金瓶梅     书籍作者:兰陵笑笑生  书籍价格:¥43.00元 书籍名称:Think in Java   书籍作者:Bruce Eckel   书籍价格:¥43.00元 如果我是负责维护的,我就非常乐意做这样的事情,简单而且不需要与其他的业务进行耦合。我唯一需要做的事情就是在原有的代码上添砖加瓦,然后就可以实现业务的变化。我们来看看这段代码有哪几层含义。 首先,ComputerBook类必须实现IBook的三个方法,是通过IComputerBook接口传递进来的约束,也就是我们制定的IBook接口对扩展类ComputerBook产生了约束力,正是由于该约束力,BookStore类才不需要进行大量的修改。 其次,如果原有的程序设计采用的不是接口,而是实现类,那会出现什么问题呢?我们把 BookStore类中的私有变量bookList修改一下,如下面的代码所示。 private final static ArrayList<NovelBook> bookList = new ArrayList<NovelBook>(); 把原有IBook的依赖修改为对NovelBook实现类的依赖,想想看,我们这次的扩展是否还能继续下去呢?一旦这样设计,我们就根本没有办法扩展,需要修改原有的业务逻辑(也就是main方法),这样的扩展基本上就是形同虚设。 最后,如果我们在IBook上增加一个方法getScope,是否可以呢?答案是不可以,因为原有的实现类NovelBook已经在投产运行中,它不需要该方法,而且接口是与其他模块交流的契约,修改契约就等于让其他模块修改。因此,接口或抽象类一旦定义,就应该立即执行,不能有修改接口的思想,除非是彻底的大返工。 所以,要实现对扩展开放,首要的前提条件就是抽象约束。 2. 元数据(metadata)控制模块行为 编程是一个很苦很累的活,那怎么才能减轻我们的压力呢?答案是尽量使用元数据来控制程序的行为,减少重复开发。什么是元数据?用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。举个非常简单的例子,login方法中提供了这样的逻辑:先检查IP地址是否在允许访问的列表中,然后再决定是否需要到数据库中验证密码(如果采用SSH架构,则可以通过Struts的拦截器来实现),该行为就是一个典型的元数据控制模块行为的例子,其中达到极致的就是控制反转(Inversion of Control),使用最多的就是Spring容器,在SpringContext配置文件中,基本配置如代码清单6-11所示。 代码清单6-11 SpringContext的基本配置文件 <bean id="father" class="xxx.xxx.xxx.Father" /> <bean id="xx" class="xxx.xxx.xxx.xxx">          <property name="biz" ref="father"></property> </bean> 然后,通过建立一个Father类的子类Son,完成一个新的业务,同时修改SpringContext文件,修改后的文件如代码清单6-12所示。 代码清单6-12 扩展后的SpringContext配置文件 <bean id="son" class="xxx.xxx.xxx.Son" /> <bean id="xx" class="xxx.xxx.xxx.xxx">         <property name="biz" ref="son"></property> </bean> 通过扩展一个子类,修改配置文件,完成了业务变化,这也是采用框架的好处。 3. 制定项目章程 在一个团队中,建立项目章程是非常重要的,因为章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。相信大家都做过项目,会发现一个项目会产生非常多的配置文件。举个简单的例子,以SSH项目开发为例,一个项目中的Bean配置文件就非常多,管理非常麻烦。如果需要扩展,就需要增加子类,并修改SpringContext文件。然而,如果你在项目中指定这样一个章程:所有的Bean都自动注入,使用Annotation进行装配,进行扩展时,甚至只用写一个子类,然后由持久层生成对象,其他的都不需要修改,这就需要项目内约束,每个项目成员都必须遵守,该方法需要一个团队有较高的自觉性,需要一个较长时间的磨合,一旦项目成员都熟悉这样的规则,比通过接口或抽象类进行约束效率更高,而且扩展性一点也没有减少。 4. 封装变化 对变化的封装包含两层含义:第一,将相同的变化封装到一个接口或抽象类中;第二,将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。封装变化,也就是受保护的变化(protected variations),找出预计有变化或不稳定的点,我们为这些变化点创建稳定的接口,准确地讲是封装可能发生的变化,一旦预测到或“第六感”发觉有变化,就可以进行封装,23个设计模式都是从各个不同的角度对变化进行封装的,我们会在各个模式中逐步讲解。