《设计模式》学习笔记1——七大面向对象设计原则

前言

根据这一次的学习计划,系统学习设计模式之前,先系统学习和理解设计原则。面向对象设计原则有如下几类。

原则一:单一职责原则

这是面向对象最简单的原则,对于定义,引用书中所说:

单一职责原则(Single Responsibility Principle, SRP):一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因

这里最重要的地方,我个人觉得应该是一个功能领域这一句。
设计的前提是思考,只有进行了思考才能谈得上设计,所以实际设计过程中最重要的还应该是思考究竟哪些才是一个功能领域,也只有确定了这个,才能真正的遵循单一职责原则。

这个原则让我想起最近重构中一个功能设计的问题,很明显一开始的设计并没有遵循单一职责原则。
这项功能涉及到三个子系统A、B、C,我们需要从A系统向B系统发送http请求,发送过程中带了两个参数,一个是具体对象数据obj,一个是动态获取的B系统向C系统发送请求的请求url等相关信息。
我们当时是在A系统中写了一个httpService来处理向B系统发送请求,在一个方法中同时做了这样三件事:

  1. 封装obj数据对象;
  2. 动态获取B向C发送请求的相关请求信息;
  3. 正式向B发送请求;

很明显的,这个类的这个方法看起来做的都是跟发送请求相关的事,但是仔细分析后会发现其实引起这个方法改变的原因至少会有三个,因此至少应该把这三件事分为三个方法来处理。

原则二:开闭原则

开闭原则引用书中的定义如下:

开闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展

这里最重要的应该是软件实体四个字,只有正确的理解了何为软件实体,才能更好的用好开闭原则,书中对软件实体定义如下:

在开闭原则的定义中,软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类。

对于开闭原则,我个人理解就是说,可以继承或实现某个类,在这个类的基础上增加功能,但是不能直接去改这个类。
然而,在实际实施的过程中显然是不太容易做到的,这通常需要一定的开发功力,可能还需要先遵循其他一些原则,这个原则才会具有实现的可能。
就比如上边说的单一职责原则,如果没有非常好的遵循单一职责原则,各种功能糅合在一起,那么必然在后期拓展的过程中难以做到很好的开闭。这时候可能能对拓展开放,却未必能对修改做到关闭,可能发现很多时候必须修改原本的类才行。

同样是上边举例的那个功能,根据单一职责原则,我们把三件事分到了三个方法中实现,但是依然存在着其他的问题。
就比如封装数据对象这件事,由于很多个业务公用一个请求,在参数个数固定的情况下,不同的业务数据使用的数据对象封装就必然会有所不同。
当我们用一个方法处理这些数据封装的时候,就必须使用若干个if/else这样的判断语句,那么一旦后边有新业务加入或者旧业务需要修改,必然需要改动这个方法和这个类,所以更好的办法应该是针对不同业务的不同数据使用不同的方法或者类来进行处理。

原则三:里氏替换原则

里氏替换原则引用书中的定义如下:

里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类) 的地方必须能透明地使用其子类的对象

在我目前记下的三个原则中,这个恐怕是我觉得最好理解也是最容易做到的了,因为平常开发的过程中几乎时时刻刻都在写着接口和实现类。
而这个原则简单点理解,就是说用接口或者其他类型的父类调用的任何一个方法,都必须要能用这个父类的任何一个子类进行调用;任何一个用父类作为参数的地方,要也都能用任何一个子类作为参数。
里氏替换,其实可以理解为就是用子类代替父类。

原则四:依赖倒转原则

依赖倒转原则引用书中的定义如下:

依赖倒转原则(Dependency Inversion Principle, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程

这个原则和上一个原则乍一看起来我觉得好像是雷同了,但是仔细分析一下后发现其实并没有。
里氏替换原则更具体点理解,说的是父类和子类在各自定义的时候应该遵循的一种原则,重点在于父类和子类的定义。
而依赖倒转原则说的则应该是如何更合理使用父类和子类,重点在于如何使用。
但是这两个原则说的都是父类和子类的问题,因此很显然也有必然的联系,只有遵循里氏替换原则合理的定义了父类和子类,才可能更合理的遵循依赖倒转原则。
因此这也涉及到一个问题,子类虽然从语法上来说可以有自己的对外开放的方法,那么是否应该提供这样的方法呢?
很显然的,如果要完全遵循依赖倒转原则,子类就不应该定义自己的对外开放的方法,否则针对接口编程的时候,那子类的那些对外开放的特有方法就成了摆设。

原则五:接口隔离原则

接口隔离原则引用书中的定义如下:

接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。

对于这个原则,我倒觉得他更像是单一职责原则的补充,或者说是一个更小维度的单一职责原则,因为我觉得接口实际上也可以理解成单一职责原则中说的某个功能领域
而“使用多个专门的接口而不是一个总接口”,这也直接体现了单一职责,只不过这里可能是更小维度的单一职责,目的是为了避免一个接口中过多的功能造成依赖于他的客户端做出一些不必要的工作。
那么如果再结合前两个原则来说,我更觉得他们就是一个整体,首先里氏替换原则概括的说明了父类和子类应该以何种关系定义,然后依赖倒转原则又说了父类和子类应该如何使用,再然后这里的接口隔离原则就更进一步具体说明了父类接口应该如何更好的定义。

原则六:合成复用原则

合成复用原则引用书中的定义如下:

合成复用原则(Composite Reuse Principle, CRP):尽量使用对象组合,而不是继承来达到复用的目的

根据我的理解,复用一般指的是自己本身不具备的方法,但可以拿来使用,而实现方式通常是组合、聚合和继承。
继承指的是,在父类中写的方法被子类继承后,子类不需要再写一遍这个方法,子类的对象就可以调用。
组合指的是,声明一个类的时候,把另一个类以属性的方式声明,然后在这个类的对象中便包含了那个类的对象,然后这个类的对象中就可以调用那个类中的方法,从而实现自己不用定义,当能实现某些功能。
而聚合通常是说把另一个类的对象以参数的方式传进来,然后这个类的对象的方法中也就可以调用参数对象的方法,这样也实现了自己不定义,但能实现某些功能。
以上三种方式都能实现代码和功能的福永,减少了重复代码,但是继承会破坏类的封装性,把父类的实现细节暴露给子类,同时如果父类声明为不可被继承,那么还不能被复用,这些都是非必要,不建议使用继承复用的原因。
相反的,组合和聚合就更加的灵活,具体的实现也不会暴露给其他组合的类,因此建议使用组合和聚合实现代码和功能的复用,也就是合成复用原则。

原则七:迪米特法则

迪米特法则引用书中的定义如下:

迪米特法则(Law of Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用

迪米特法则又称为最少知识原则、最少知道原则,说白了,就是一个类中定义的属性、方法这些应当严格定义访问权限,只暴露必须暴露出来的,而私有不必要暴露出来的。
之所以要这样做,是因为当一个类暴露出过多的内容在外边时,一方面会使引起他变化的因素变多,情况变得复杂,另一方面也会使得使用这个类的时候造成选择的困难。

推荐文章