面向对象六大设计原则和实际应用场景(单一职责、里氏替换、依赖倒置、接口隔离、迪米特、开闭)

2017-02-22 20:45:12

请关注唯心的个人微信公众号:craft6-cn(中划线,也可以搜索:领域驱动业务建模)

一、概述

    关于这6大面向对象设计原则,网上的文章很多,虽然定义是一样的,但是解读的

方式各有不同,我也整理一下我的心得体会,希望对读者有所帮助。

    面向类的六大设计原则:

    • 单一职责 SRP

    • 里氏替换原则 LSP

    • 依赖倒置原则 DIP

    • 接口隔离原则 ISP

    • 迪米特法则 LOD(或称最少知识原则LKP)

    • 开闭原则 OCP

二、单一职责原则(SRP)

    定义:每个类应只有一个引起它变化的原因。


    瑞士军刀是个不错的现实例子,什么工

具都有,瑞士军刀还会区分不同的系列,

对于有些模仿的产品,则是个大杂烩,恨不得什么都往上放,最后导致整个工具非常笨重

和难以使用。


     我在做ECM项目中,订单模块业务最为复杂,从下单、审单、打单、发货、财务、

跟单、售后等等,都是订单的业务逻辑,按照领域建模的开发方式,开发的同事刚开始把

大部分的方法都放入了SoEntity.java这个对象中,导致了这个类方法非常得多,越来越难以

理解和维护。

QQ截图20170222154146.png

    各位可以看到这个类方法非常多,分类不清晰,整个类1200+行。


    而且由于订单的业务逻辑变化非常频繁,经常要进行调整,订单模块又

不能只由一个同事负责(那样会形成瓶颈),所以导致这个类的一些方法谁都不敢动,

怕改了别人的方法导致出现意外的bug,雪球越滚越大,越来越不稳定。


    后来,根据单一职责原则进行重构。

  1. 将不归属业务的代码分离出去,作为工具类、帮助类存在。

  2. 按订单业务来细分领域对象。
    订单领域对象拆分.jpg

  3. 规范方法命名和参数,让类更明确体现出单一职责。


    这样重构后,SoEntity.java的方法分散到了其它的业务领域对象中,但需求发生变更时,只需要修改
相应的对象即可,而且一般某些子对象由单个同事负责,避免了都修改同一个类导致混乱的情况。

    重构后的SoEntity

    QQ截图20170222155725.png


    所以单一职责并不是一种阳春白雪的原则,而关乎着我们的项目管理和项目质量。


三、里氏替换原则(LSP)

    里氏替换定义:所有引用基类的地方必须能透明地使用其子类的对象,
              子类可以扩展父类的功能,但不能改变父类原有的功能。

    

    实际生产中,父类一般作为抽象类形式存在,抽象类有实现方法和抽象方法,

抽象方法类似接口方式的存在,非抽象子类必须实现这些抽象方法,而且每个子类可以

有自己的实现方式,这是继承的约定,在设计模式中:模板模式 就是采用这种方式,

这种情况下是符合里氏替换原则的。

    但是,如果子类覆盖了父类的非抽象方法,改变了该父类方法原先的业务行为,

那么将会导致调用者出现混乱。


    实际项目会出现这种错误的情况会在对框架默认方法的覆盖上。

    比如框架提供了CRUD方法。

    QQ截图20170222161431.png

    比如save()方法,框架默认是进行单表的保存,并提供了onSave()空方法,供保存之前进行处理。

    如果某个子类也需要保存,但是觉得这个方法不合适,然后覆盖了这个方法,但是调用者并不知晓

    这种情况,调用该子类的实现时就会出现问题。


    解决办法:可以写一个新的方法,比如saveCascade,在完成子类自己的逻辑后,调用super.save()

完成保存即可。


四、依赖倒置原则(DIP)  

    随着Spring框架的普及,依赖倒置原则基本上已经融合到大多数的Java系统中。

    但对于依赖倒置DIP我们要知其所以然,才能避免在开发中犯错。    


    依赖倒置定义:依赖于抽象而非具体。

    

    常见的场景有通过Spring的@Resource方式注入依赖的接口实现。

    要满足这个原则注意一下流程:

  • 先定义接口,把接口方法规则定义好。

  • 再写接口的实现,注意该实现类永远不要有new的方式供外部调用。

  • 业务调用者通过调用接口,采用注解依赖注入实现。

  • 但该接口有多个实现时,显式指定具体使用哪个实现类。


    比如在我们项目中要和第三方物流查询平台API集成,但是这些API

平台存在不同的情况:

    1)有时会不稳定;

    2)有时账号会过期;

    3)每天查询次数有限制。

    所以迫使我们要集成多个查询平台,但是实施不同的项目,启用的

查询API又有不一样(客户可能购买了某些平台的查询账号),而且需要增加

轮询等策略。

    所以对于查询,我们需要接口化,并且提供工厂类,这样在查询时,

就可以根据类型来选择不同的实现进行查询。


    整个过程对于调用者而言是透明的。

    QQ截图20170222163322.pngQQ截图20170222163332.png    

五、接口隔离原则(ISP)

    定义:调用者(或实现者)无需被迫依赖它用不到的方法。


     前面依赖倒置要求我们基于接口进行开发,但是当使用不当时,就会导致出现肥接口的问题。
什么方法都往一个接口类里面放,到时混乱不堪,调用复杂。

    而且不同的实现类,由于只能实现其中一部分,导致要在无法实现的方法中加:

    UnsupportedMethodException等。


    调用者就更混乱了,基于接口依赖倒置,但是调用各个方法的实现还会抛出上面那个异常,

甚至需要去看这个实现类的代码,才知道它究竟只实现了什么方法。


    所以基于接口隔离原则:

    • 不该实现者实现的方法不要让它实现

    • 不该调用者调用的方法不要让它看见


    比如JDK本身存在Comparable、Runable、Serializable、Cloneable等接口,分别表示不同的

的功能含义,那如果我们将其合并到一起,比如 ICloneRobot,那么除非这个接口确实具备业务含义,

否则它的实现类如果只想提供比较功能,那也会被迫要实现其它的接口方法。


    在框架设计中,主要问题会出现在底层的IDao、IManager、IRepository、IDomain等等方法

中,在设计,把业务层面,或者具体技术选型层面的方法也引入到了底层,这样就增加了实现类的

开发实现难度。

    如,这是默认的仓库接口,提供领域对象的实例化和查询方法。

    QQ截图20170222164807.png

    但是对于某个简单的领域对象,它其实只需要几个实例化的方法,不需要查询,

    为此可以增加一个

    QQ截图20170222164816.png

    然后IRepository继承它即可,这样实现类就可以根据需要,实现IRepository或者

ISimpleRepository了。



六、迪米特法则(LoD)(或称最少知识原则)

    定义:一个对象应当对其他对象有尽可能少的了解。


    从笔者项目经历来看,项目开发过程中程序员违反这个原则的情况最多,主要集中在

随意设计接口方法的参数类型上面。


    下面有三个方法:

     方法一:

    public boolean startFlow(FlowDef flowDef,

            FlowForm flowForm,Employee employee,String startType)

    这个方法表示我们调用这个方法需要传入三个对象,这三个对象都是复杂对象,可以想象

调用难度有多大,而且程序里面对各个对象里面的变量的校验、依赖我们都是不知道,如果数据

不合乎要求,那么调用将会出错。


    方法二、

    public boolean startFlow(long defId,long flowFormId,long employeeId,String startType)

    这个方法明显比刚才的要简单,当然,对于流程定义(FlowDef)、FlowForm(流程表单)

是从持久化的数据库(或xml)里面读取,但是好处就是校验的工作其它地方帮我们做了,我们只需要查询

出来即可。


    方法三、

    public boolean startFlow(long defId,long flowFormId,long employeeId,StartType startType)

    基于前面的接口方法再进一步对startType进行约束,创建一个枚举类型。

    这样好处就是该参数的传入值不会有错(用字符串很容易出错,比如大小写就是个严重的问题了)


    从三个方法来看,第三种方法最符合迪米特法则,因为它对调用者的知识要求最低,

准备最少的内容就可以调用了,而且不用担心会出现意外错误。


    我的建议,在设计接口方法的参数时:

  • 尽量减少用复杂对象(我指的是非单纯的PO、VO对象,而是有主从关系的复杂业务对象)

  • 尽量避免超过3个参数,如果超过三个参数,最好封装成一个Vo对象(只有get、set方法)

  • 参数之间最好避免同类型相邻,比如参数一、参数二均是String,这样容易导致调用顺序写错。

  • 能用枚举尽量用枚举,这样可以进行可选值范围限定。


七、开闭原则(OCP)

    定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。


    6大原则中最抽象、最复杂的原则,而且也是最难把握其中尺度的原则。


    ===============================================================

     开闭原则分为类级别的开闭和方法级别的开闭:

    类开闭例子一:支持多数据库类型的查询扩展。

    开闭原则(一).jpg

    接口DaoHelper提供两个方法:

    1)查询单条记录并映射到单个对象

    2)查询记录集映射到一个列表集合中。


    ===============================================================

     类开闭例子二:支持多形式的数据源配置存储。

    开闭原则(二).jpg


    ===============================================================

    方法开闭例子一:传入参数预计会发生修改时,封装为对象(Vo)

    QQ截图20170222194025.png

    传入参数封装为RuleContext,这样当需要增加参数时,修改RuleContext即可,

方法本身不需要改变,这样就不会影响调用者了。

    【注】用这种方式要避免参数VO过于复杂,否则就反而违背了迪米特法则(最少知识原则)了。

    



可通过扫描左侧二维码阅读本文。本站文章均为颜超敏原创,欢迎转载,请注明出处即可,转载可通过下面的社会化工具快速完成。

分享到:


为您推荐这些文章,如果感兴趣,请继续阅读吧:

面向对象六大设计原则和实际应用场景(单一职责、里氏替换、依赖倒置、接口隔离、迪米特、开闭)

六大设计原则、单一职责、里氏替换、依赖倒置、接口隔离、迪米特、开闭

关于这6大面向对象设计原则,网上的文章很多,

虽然定义是一样的,但是解读的 方式各有不同,

我也整理一下我的心得体会,希望对读者有所帮助。

面向类的六大设计原则: 

单一职责 SRP 

里氏替换原则 LSP

依赖倒置原则 DIP 

接口隔离原则 ISP 

迪米特法则 LOD(或称最少知识原则LKP) 

开闭原则 OCP

颜超敏,唯心六艺,Craft6.cn,电子商务博客,电子商务研发,电商研发,电子商务研究,电商研究,电子商务专家,电商专家,电子商务知识,电商知识,电子商务教程,电商教程,电子商务模式,电子商务平台,电子商务商业模式,电子商务数据库设计,电商数据库设计,电子商务系统分析,Java架构设计,Java软件架构,B2C,O2O,o2o模式,o2o电子商务,o2o电子商务平台,中国电子商务,电子商务平台建设方案
粤ICP备14060523号 Copyright @2014 -唯心六艺软件