设计模式-六大设计原则

150 阅读11分钟

设计模式-六大设计原则

注意:阅读本文时,强烈建议先阅读下 OneMall 项目的概要设计

0. 前言

谈及设计模式,就不能不提六大设计原则,这六大设计原则就像六颗无限宝石一样强大。所有的设计模式都是为了尽量的实现某个或者某些设计原则。

通俗一点,设计原则是接口,而设计模式就是具体的实现类。所以我们先看一下这个接口里到底有哪些方法。

1. 单一职责原则(SRP:Single responsibility principle)

以我们的 OneMall 商城为例。商城首先得有用户吧,用户要有用户名、密码、余额,这些信息吧,而且也要有修改用户名、密码、充值、支付这些操作吧。我们看一下用户这个类的设计。

UserInfo 类图

我相信稍微懂点设计的人都能看出这个类的设计有问题,因为把用户的属性和用户的行为放到了个类里面。应该把用户的属性和用户的行为分开,重新设计一下这个类。

UserInfo 类图2

上面这种把一个类拆分成了两个类就是遵循了 单一职责原则

单一职责原则的定义是:

SRP:There should never be more than one reason for a class to change

引起一个类的变化原因绝对不能超过一个

就像上面的例子一样,修改用户属性会引起类的变化,处理业务逻辑(充值、支付)也会引起类的变化,依据单一职责原则,所以我们把类拆成了两个。

我们在设计类或者是接口的时候尽量做到职责单一,这样才能使每个类的职责更加清晰。

2. 里式替换原则(Liskov Substitution Principle LSP)

因为我们对接了不同的商城,每个商城,都有每个商城自己的下单逻辑,但是我们对外提供的下单逻辑只能有一套,所以需要封装一下,这里就遵守出了 里式替换原则

在一般的 MVC 架构中,我们一般有 Controller、Service层。而 Service 层一般是接口和对应的实现类,类图如下

Order 类图

我们在下单的时候可以使用接口中的 submitOrder,具体的实现逻辑放在不同的子类里,比如我们可以有 JDSubmitOrderServiceImpl、 TMSubmitOrderServiceImpl 等。

里式替换原则:

If for each object o1 of type S there is an object o2 of type T such that for
all programs P defined in terms of T,the behavior of P is unchanged when o1 
is substituted for o2 then S is a subtype of T.

如果对每一个类型为S的对象o1,都有类型为T的对象o2,
使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,
那么类型S是类型T的子类型。

这是一种标准定义。还有一种通俗的定义:

Functions that use pointers or references to base classes must be able to 
useobjects of derived classes without knowing it.

所有引用基类的地方必须能透明地使用其子类的对象。

再换句话说,就是 任何基类可以出现的地方,子类一定可以出现

里式替换原则是在告诉我们,继承关系应该遵循哪些原则? 或者说,如果你要使用继承,那么你就应该遵守里式替换原则。

里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原则是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。

里式替换原则有四层含义,或者说,当你实现继承的时候,应该遵守这四个规则。

  • 子类必须完全实现父类的方法

如果子类不能够完全实现父类的方法,那么建议断绝继承关系,使用组合、依赖等代替继承关系。

比如我们的例子中,如果有一个类 XXXOrderServiceImpl 继承了 SubmitOrderService,但是没有实现 Service 中的接口,那么就应该断绝继承关系。

PS:其实这个原则应该没有说完,还有下句。子类必须完全实现父类的方法。但不得重写(覆盖)父类的非抽象(已实现)方法

就是说子类不能改变父类已经实现好的方法,不能改变父类方法的逻辑。

  • 子类可以有自己的个性

或者说,子类可以有自己独有的方法、属性。但是这里需要注意:如果你在子类中定义了方法,那么你在使用这个类的时候就不能使用多肽了。

子类不仅可以继承父类中的方法和属性,还可以定义自己的方法和属性。但是这样做之后,就不能使用多肽了,比如,当我们在使用下单逻辑的时候,一般会这么定义:

SubmitOrderService orderService = new JDSubmitOrderServiceImpl();

orderService.submitOrder();

但是,如果我们在 JDSubmitOrderServiceImpl 中定义了一个自己的方法 xxxOrder(),如果想要使用该方法,就不能使用上面这种定义方式。

JDSubmitOrderServiceImpl orderService = new JDSubmitOrderServiceImpl();

orderService.xxxOrder();

只能使用 JDSubmitOrderServiceImpl 作为对象的类型。

  • 覆盖或实现父类的方法时输入参数(方法的参数)可以被放大

比如,我们在实现 SubmitOrderService 的时候,重写 submitOrder 方法的时候,可以将方法的参数定义成 Order 或者是 Order 的父类。

比如我们 SubmitOrderService 有个方法xxxOrder(ArrayList list) 参数是一个 ArrayList,在实现这个方法的时候可以传入 ArrayList 的父类,可以这么定义xxxOrder(List list)

PS: 这里说的是参数可以被放大,是当你有不得不放大参数的时候,可以放大,而不是只要实现就去放大参数,一般在实现的时候尽量不要放大,而且,一般父类的方法参数都是某个类的父类,很少使用子类的,所以很少遇到放大参数的情况。

注意: 这里说的是方法定义的时候可以被放大。在实际调用的时候可以传入一个参数的子类。比如:

SubmitOrderService orderService = new JDSubmitOrderServiceImpl();

orderService.submitOrder(new JDOrder());

这里我们传入的参数是 Order 的子类。而不是父类。

  • 覆写或实现父类的方法时输出结果(方法的返回值)可以被缩小

跟第 3 条对应,这里说的是方法的返回值可以被缩小,比如父类方法的返回值是 List ,那么子类的返回值可以是 List 或者是 List 的子类。比如 ArrayList。

3. 依赖倒置原则(Dependence Inversion Principle DIP)

还是以上面的 Order 类图为例,我们在定义 SubmitOrderService 的方法的时候依赖了 Order 类,在设计的时候我们使用的是 Order 类,而不是 JDOrder 这个子类,这里的设计就遵守了 依赖倒置原则

依赖倒置原则的定义:

High level modules should not depend upon low level modules,Both should depend 
upon abstractions.
高层模块不应该依赖低层模块,两者都应该依赖抽象

Abstractions should not depend upon details.
抽象不应该依赖细节

Details should depend upon abstracts.
细节应该依赖抽象

高层模块一般是抽象层,低层模块是具体的实现层,在上面的例子中 SubmitOrderService 就是高层模块,JDSubmitOrderServiceImpl、 JDOrder 就是低层模块。

在这个例子中,可以将依赖倒置原则翻译成 SubmitOrderService 不应该依赖 JDOrder, 而应该依赖 JDOrder 的抽象层 Order。 JDSubmitOrderServiceImpl 应该依赖 Order。

这么做是为了方便后期扩展,提高程序的可扩展性,想象一下,我们再增加一个 TMSubmitOrderServiceImpl,如果接口中定义的是 JDOrder,那么我们定义的 TMSubmitOrderServiceImpl 就没办法实现了。 而使用 Order 作为参数,就完全可以正常实现。在调用的时候我们可以传入一个 TMOrder 作为参数。

依赖倒置原则是告诉我们如何设计类的依赖关系,目的是降低类与类之间的耦合,提高系统的稳定性和可扩展性。

4. 接口隔离原则(Interface Segregation Principle ISP)

假如我们需要设计一个订单相关的接口, OrderService 这个服务可能会提供一些增删改查的接口,提供给管理模块使用,还有一些订单相关的接口,比如下单接口,订单撤销接口,这些接口不应该放到 OrderService 中。而是写在另外一个 Service 中,比如我们上文中的 SubmitOrderService 中。这里就遵守了 接口隔离原则

Order 类图

OrderManager 不应该依赖下单,相关的接口,所以需要将下单相关的接口”隔离”出来。

接口隔离原则有两种定义:

  • Clients should not be forced to depend upon interfaces that they don’t use.(客户端不应该依赖它不需要的接口。)

  • The dependency of one class to another one should depend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上。)

针对我们上面的例子

  • 客户端(OrderManager) 不应该依赖它不需要的接口(下单接口)。
  • OrderManager 与 OrderService 的依赖关系应该建立在最小的接口上。

接口隔离原则是在告诉我们,应该以什么规则将接口组合到一起,应该以什么规则将接口拆分到不同的类中。

PS: 接口隔离原则中的”接口”,是指的 Java 中 interface 里某个具体的方法。而不是 Java 中使用 interface 声明的某个 class 文件

5. 迪米特法则(Least Knowledge Principle LKP)

以商城为例,假如我们的分销商城 AMall 需要获取一个商品信息,一种做法是,我们提供 2 个接口,JDGoodsService 用于提供 JD 商城的商品信息,一个 TMGoodsService 用于提供 TM 商城的商品信息,AMall 分别调用不同的接口获取商品信息。

上面这种设计就违反了 迪米特法则, 注意,是 违反 了迪米特法则。

迪米特法则又叫最小知识原则,标准的定义是:

Only talk to your immediate friends(只与直接的朋友通信)

根据我们上面的这个例子,AMall 需要获取商品信息,不应该直接与 JD、TM 等接口通信,而是 OneMall 提供统一的接口,用于获取商品信息,至于具体是获取 JD 的商品还是 TM 的商品,那应该是 OneMall 该操心的事情,上面的例子中我们的 AMall 跨过了 OneMall 直接与具体的商城接口通信,就是违反了迪米特法则。

迪米特法则是在告诉我们如何处理接口与接口之间的依赖关系。遵守迪米特法则,可以让代码更加容易扩展,比如,将来我们增加一个 TB 的商城接口,对 AMall 来说是不需要做修改的。

6. 开闭原则(Open Close Principle OCP)

还是以我们的 OneMall 商城为例,现在我对接了 JD 商城、TM 商城、TB 商城、如果我们后期要对接其他的商城,我们的 OneMall 商城应该通过扩展(增加)类接口的方式来实现功能,而不是通过修改接口和类来实现。

如果能够通过扩展来实现,说明我们的项目设计就是遵守了 开闭原则

开闭原则的定义:

Software entities like classes,modules and functions should be open for extension but closed for
modifications.(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。)

代码设计如果遵守了开闭原则,可以使代码更容易扩展。

开闭原则是之前五大设计原则的更高层的抽象,或者说,之前的五大设计原则是开闭原则的具体体现。

可能有点绕,可以这么理解,我们在设计代码的时候,终极目标就是希望代码符合或者遵守开闭原则,而具体应该怎么设计才能够符合开闭原则呢,就是尽量遵守其他五大设计原则!

7. 总结

  • Single Responsibility Principle:单一职责原则
  • Open Closed Principle:开闭原则
  • Liskov Substitution Principle:里氏替换原则
  • Law of Demeter:迪米特法则
  • Interface Segregation Principle:接口隔离原则
  • Dependence Inversion Principle:依赖倒置原则

将这六个原则的首字母组合起来就是 SOLID(solid 稳定的),代表的含义是这六个原则组合使用起来可以建立稳定的、灵活的、可靠地项目。

转载请注明出处
本文链接:zdran.com/20190509.ht…