设计模式之六大设计原则

内容纲要

本文章参考《设计模式之禅》一书,用自己的话进行总结,可以当做笔记,以后回顾起来看这篇就够了(不知道会不会成了多篇)。

概述

需求的变化不可控,如何让代码适应变化、不在一次次迭代中变成一团乱麻?设计模式给了我们指导。
设计模式是前辈们在实践中总结的一套可以反复使用的经验。它可以提高代码的可重用性、增强系统可维护性,以及解决一系列复杂问题。
前辈们首先提出了6大设计原则作为纲领,然后具体23种设计模式中给了具体指导。
设计模式可以理解为是一种哲学,而非工具,所以使用起来没有好坏之分,代码设计成什么样完全取决于你对设计模式和对你的需求的理解。
这篇文章只能讲一下是什么、为什么,想要游刃有余的使用只能通过不断的实践~

六大设计原则

单一职责原则

单一职责(Single Responsibility Principle SRP原则)又称单一功能原则,由罗伯特·C.马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中提出的。这里的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分(There should never be more than one reason for a class to change)。
通俗的说,自己只负责自己的事,不需要理会别人的事。上例子:

用户系统,将上述的IUserInfo,分开成为IUserBO(用户信息)和IUserBiz(用户行为):


这个原则就说完了^_^。

此外强调一下,职责单一,是一个单元内只放会由仅一个原因引起变化的内容,判断有几个原因引起变化,可以看第二个例子:

一个简单的电话接口,三个方法分别负责拨通电话、通话和挂电话。其中通话实现的是数据传送,接通和挂断则是协议相关;因此数据传送和协议的变化都会引起这个接口的变化,按照单一职责原则就可以进行拆分:

在实践中,主要应用在接口和方法设计中,在类中的应用要考虑实际情况,如果为了这个原则带来更高的复杂性(类太多),或影响工期什么的,就得不偿失了,其他原则也是如此,主要在一个trade off^_^。

里氏替换原则

Liskov Substitution Principle(LSP原则)由麻省理工学院计算机科学实验室的里斯科夫(Liskov)女士在 1987 年的“面向对象技术的高峰会议”(OOPSLA)上发表的一篇文章《数据抽象和层次》(Data Abstraction and Hierarchy)里提出来的,她提出:继承必须确保超类所拥有的性质在子类中仍然成立(Inheritance should ensure that any property proved about supertype objects also holds for subtype objects)。
我们知道在继承中,子类拥有父类的所有方法和属性,减少了创建类的工作量,提高了代码的重用性;子类在拥有父类所有功能的基础上,还可以添加自己的功能,提高了代码的扩展性。但也降低了代码的灵活性(子类受父类约束),增强了耦合性(父类的修改影响子类)。LSP原则的引入,将继承的特性扬长避短。

定义:只要父类出现的地方就可以替换为子类,而且不会出现错误,使用者不需要知道使用的是父类还是子类;反过来在使用子类的地方试图用父类替换则可能会出现错误,通俗来讲就是子类可以扩展父类的功能,但不能改变父类原有的功能,LSP原则是实现开闭原则的重要方式之一。

LSP原则为良好的继承制定了规范,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。体现在四个方面:

  1. 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法;
    若子类不完全对父类的方法进行实例化,那么子类就不能被实例化;
    父类中已实现的方法其实是一种已定好的规范和契约,若子类覆写则可能会带来意想不到的错误。eg:初始化父类和子类后,调用同一方法,行为不同。
    有时候父类有多个子类,但在这些子类中有一个特例。要想满足里氏替换原则,又想满足这个子类的功能时,有的伙伴可能会修改父类的方法。但是,修改了父类的方法又会对其他的子类造成影响,产生更多的错误。这时可以为这个特例创建一个新的父类,这个新的父类拥有原父类的部分功能,又有不同的功能。这样既满足了里氏替换原则,又满足了这个特例的需求。

  2. 子类中可以增加自己特有的方法。

  3. 子类在覆写或实现父类方法时,输入参数可以比父类宽松。
    这实际上是重载,因为子类的入参类型和父类不一样。若子类入参宽(Map),父类入参窄(HashMap),则通过父类实例和子类实例调用同一方法名,传入HashMap类型入参时,执行的都是父类方法;
    反之,子类定义入参HashMap,父类定义入参Map后,传入HashMap类型入参,通过父类实例调用的是父类方法,而用子类实例调用的方法是子类方法,父类方法没有被重写的情况下,子类方法被调用了,造成逻辑混乱~

  4. 子类在覆写或实现父类方法时,返回值要比父类严格。
    这样可以实现不同子类实例调用方法能返回各自所需的对象类型。

依赖倒置原则

Dependence Inversion Principle(DIP原则)是 Object Mentor 公司总裁罗伯特·马丁(Robert C.Martin)于 1996 年在 C++ Report 上发表文章提出的。
该原则定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象;本质就是要面向接口编程,不要面向实现编程。这样来实现各类或模块间的彼此独立,实现模块间的松耦合。
“倒置”的解释:“依赖正置”就是类间的依赖使用实现类来依赖,相应的,“倒置”就是指类间通过接口来实现依赖。

用一个例子来看面向实现编程和面向接口编程:
(原始开奔驰图)

上图司机类通过奔驰类来实现driver方法,使司机可以开奔驰车。但想让司机也能开宝马,就需要修改司机类,导致系统可维护性降低。


改为司机通过ICar接口来控制开什么车,体现了抽象不依赖细节。这样一来司机想换个车开,只需要修改client,不侵入原有的司机逻辑。

依赖的三种写法:

  1. 构造函数传递依赖对象
    public interface IDriver {
    //是司机就应该会驾驶汽车
    public void drive();
    }
    public class Driver implements IDriver{
    private ICar car;
    //构造函数注入
    public Driver(ICar _car){
        this.car = _car;
    }
    //司机的主要职责就是驾驶汽车
    public void drive(){
        this.car.run();
    }
    }
  2. setter方法传递依赖对象
    public interface IDriver {
    //车辆型号
    public void setCar(ICar car);
    //是司机就应该会驾驶汽车
    public void drive();
    }
    public class Driver implements IDriver{
    private ICar car;
    public void setCar(ICar car){
        this.car = car;
    }
    //司机的主要职责就是驾驶汽车
    public void drive(){
        this.car.run();
    }
    }
  3. 接口声明依赖对象
    public interface IDriver {
    //在接口的方法中注入
    public void drive(ICar car);
    }

想要使用这个原则,遵循以下四点就够了:

  1. 每个类尽量提供接口或抽象类,或者两者都具备。
  2. 变量的声明类型尽量是接口或者是抽象类。
  3. 任何类都不应该从具体类派生。
  4. 使用继承时尽量遵循里氏替换原则。

接口隔离原则

Interface Segregation Principle(ISP原则)是2002 年罗伯特·C.马丁提出的,“客户端不应该被迫依赖于它不使用的方法”,或者说“一个类对另一个类的依赖应该建立在最小的接口上”。这要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法
接口隔离原则和单一职责都是为了提高类的内聚性、降低耦合性,体现了封装的思想,但两者是不同的:
单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
例子可以参考单一职责原则的例子(这两个原则真的很像)
遵循接口隔离原则时需要注意接口拆分粒度,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
实现时遵循以下规则:

  1. 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
  2. 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
  3. 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
  4. 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

迪米特法则

迪米特法则(Law of Demeter),又称最少知识原则(Least Knowledge Principle,LKP),产生于 1987 年美国东北大学的一个名为迪米特(Demeter)的研究项目,由伊恩·荷兰(Ian Holland)提出,被 UML 创始者之一的布奇(Booch)普及,后来又因为在经典著作《程序员修炼之道》(The Pragmatic Programmer)提及而广为人知。
定义:一个对象应该对其他对象有最少的了解;如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。
迪米特法则要求限制软件实体之间通信的宽度和深度,降低类之间的耦合,提高类的复用率和系统的扩展性。
使用时也需要注意使用程度,确保高内聚和低耦合的同时,保证系统的结构清晰。过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。

实现时遵循以下规则:

  1. 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
  2. 在类的结构设计上,尽量降低类成员的访问权限。
  3. 在类的设计上,优先考虑将一个类设置成不变类。
  4. 在对其他类的引用上,将引用其他对象的次数降到最低。
  5. 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
  6. 谨慎使用序列化(Serializable)功能。

举两个例子:
eg: 明星、代理人、媒体公司的关系:

体现定义:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。

eg: 软件类和安装类

优化前,Wizard把太多方法暴露给InstallSoftware类,一旦需要修改Wizard的first方法返回值,就需要修改InstallSoftware类,耦合较深。

优化后在Wizard类中增加一个installWizard方法,对安装过程进行封装,同时把原有的三个
public方法修改为private方法。通过这样的重构后,Wizard类就只对外公布了一个public方法,即使要修改具体步骤方法的返回值,影响的也仅仅只是Wizard本身,其他类不受影响,这显示了类的高内聚
特性。

开闭原则

Open Closed Principle,由勃兰特·梅耶(Bertrand Meyer)提出,他在 1988 年的著作《面向对象软件构造》(Object Oriented Software Construction)中提出:软件实体应当对扩展开放,对修改关闭(Software entities should be open for extension,but closed for modification),其含义是软件实体应该通过扩展来实现变化,而不是通过修改已有代码来实现。
开闭原则作用入下:

  1. 软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
  2. 可以提高代码的可复用性
  3. 可以提高软件的可维护性
    实现方法:
    可以通过抽象约束、封装变化来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

参考

除了《设计模式之禅》外,这个网站给了极大帮助,讲解全面又简洁,看完这个感觉前书将的内容很细抓不住重点。
http://c.biancheng.net/view/1324.html

One thought on “设计模式之六大设计原则”

发表评论

邮箱地址不会被公开。 必填项已用*标注