单一职责

每个类都完成与自己相关的那一部分任务

在SpringBoot项目中,我们往往会涉及到很多的controllerservice或者mapper,比如UserControllerGoodsController,这其实就是单一职责规则的体现——每个类都只负责与自己相关的那一部分工作。

用户控制器不可能控制与商品相关的请求,这显然是不符合常理的。

开闭原则

开闭原则是指:

  • 对拓展开放:如果有新的需求,可以对现有代码进行拓展,从而适应新的情况。
  • 对修改关闭:类一旦设计完成,就可以独立完成其工作,而不要对已有的代码进行任何修改。

    实现开放封闭的核心思想就是面向抽象编程,而不是面向具体编程

比如我们现在需要有一个画笔类,然后需要根据要求画出相应的形状。

  1. public class GraphicEditor {
  2. public void draw(Shape shape) {
  3. if (shape.m_type == 1) {
  4. drawRectangle();
  5. } else if(shape.m_type == 2) {
  6. drawCircle();
  7. }
  8. }
  9. public void drawRectangle() {
  10. System.out.println("画长方形");
  11. }
  12. public void drawCircle() {
  13. System.out.println("画圆形");
  14. }
  15. class Shape {
  16. int m_type;
  17. }
  18. class Rectangle extends Shape {
  19. Rectangle() {
  20. super.m_type=1;
  21. }
  22. }
  23. class Circle extends Shape {
  24. Circle() {
  25. super.m_type=2;
  26. }
  27. }
  28. }

我们来看看, 这个代码, 初看是符合要求了, 再想想, 要是我增加一种形状呢? 比如增加三角形。

  1. 首先,要增加一个三角形的类, 继承自Shape
  2. 第二,要增加一个画三角形的方法drawTrriage()
  3. 第三,在draw方法中增加一种类型type=3的处理方案。

这就违背了开闭原则-对扩展开发,对修改关闭。增加一个类型,修改了三处代码。

比较合适的方法如下

  1. public class GraphicEditor1 {
  2. public void draw(Shape shape) {
  3. shape.draw();
  4. }
  5. interface Shape {
  6. void draw();
  7. }
  8. class Rectangle implements Shape {
  9. @Override
  10. public void draw() {
  11. System.out.println("画矩形");
  12. }
  13. }
  14. class Circle implements Shape {
  15. @Override
  16. public void draw() {
  17. System.out.println("画圆形");
  18. }
  19. }
  20. }

使用接口来定义一种抽象行为,而各个形状类自己规范自己的行为

如果现在需要增加一种类型:三角形。那就只需要

  1. 增加一个三角形类,实现Shape接口
  2. 调用draw方法即可

整个过程都是基于原有接口进行拓展,没有进行修改,符合开闭原则

里氏替换原则

  1. 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
  2. 子类可以增加自己特有的方法。
  3. 当子类的方法重载父类的方法时,方法的前置条件(输入/入参)要比父类方法输入参数更加宽松
  4. 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(方法的输出/返回值)要比父类更加严格或与父类一样。

假如现在有Coder类和JavaCoder类,并且JavaCoder继承于Coder
image.png
但是JavaCoder类对Coder类中的coding方法进行了重写,也就是对coding方法进行了覆盖,显然违背了里氏替换原则。
为了解决这个问题,我们可以定义一个更高等级的类,并继承此类

  1. abstract class People {
  2. abstract void coding();
  3. }
  4. class Coder extends People {
  5. @Override
  6. void coding() {
  7. System.out.println("程序员会写代码");
  8. }
  9. }
  10. class JavaCoder extends People {
  11. @Override
  12. void coding() {
  13. System.out.println("Java程序员想摆烂");
  14. }
  15. }

依赖倒转原则

依赖倒转原则在Spring框架中就有体现,比如依赖注入

service中我们一般会注入对应的dao,在controller中注入对应的service,比如下面的代码

  1. public class UserController {
  2. @Autowired
  3. private UserService userService;
  4. @GetMapping("")
  5. public String hello() {
  6. return userService.hello();
  7. }
  8. }

UserService应该是一个接口,为什么我们能直接调用接口方法?其实Spring注入的时候帮我们进行了类似下面的操作:UserService userService = new UserServiceImpl()

如果我们不使用注入的方式,那么就需要我们手动进行new对象,这就会导致类和类之间的耦合(如果我们不再使用这个实现类,那么就要更改代码中很多的部分)所以通过使用接口,可以弱化这种关联。我们只需要知道接口中定义了什么方法,直接使用即可。而不用关心接口中的方法具体是怎么实现的。

接口隔离原则

客户不应该依赖那些他们用不到的方法,只给每个客户提供所需要的接口。

这其实包含了两层意思:

  • 接口的设计规则:接口的设计应该遵循最小接口原则,用户使用的方法不应该塞到同一个接口里。如果一个接口的方法没有被使用到,说明存在冗余,需要进一步进行分割。
  • 接口的继承原则:如果一个接口A继承另一个接口B,则接口A相当于继承了接口B的方法,那么继承了接口B后的接口A也应该遵循上述原则:不应该包含用户不使用的方法。反之,则说明接口A被B给污染了,应该重新设计它们的关系。

    假如有一个门(Door),有锁门(lock)和开锁(unlock)功能。此外,可以在门上安装一个报警器而使其具有报警(alarm)功能。用户可以选择一般的门,也可以选择具有报警功能的门。分析需求,找出其中的名词,我们不难得到三个候选类:门(Door)、普通门(CommonDoor)、有报警功能的门(AlarmDoor)。我们该如何设计这三个候选类之间的关系呢?

最简单的一种设计方式如下:
七大原则 - 图2
但普通门并不需要“报警”,因此这显然是违背接口隔离原则的
有三种解决方式:

  1. 使用接口的多实现:同时实现DoorAlarm接口

七大原则 - 图3

  1. 继承CommonDoor类并实现Alarm接口

七大原则 - 图4

  1. 通过委托分离接口,实现Alarm接口并包含了CommonDoor的对象

    和方案二有很多相似的地方,但是实现的方式不同。方案三是对方案二应用了组合/聚合复用,将继承关系转换为了聚合关系。

七大原则 - 图5

合成复用原则

合成复用原则核心思想在于软件复用的时候,尽量通过组合或聚合的方式实现调用,其次才考虑通过继承的方式来实现。

继承的缺点

  • 破坏了类的封装性,父类对于子类来说是透明的。(白箱复用)
  • 增加了子类和父类之间的耦合度,父类的任何修改都会影响子类的实现,不利于类的拓展和维护。

    合成复用的优点

  • 维护了类的封装性,成分对象的实现细节对于新对象是不可见的。(黑箱复用)

  • 新类与成分对象之间的耦合度较低,只能通过成分对象提供的接口来访问成分对象。

不遵循合成复用的代码

  1. class A {
  2. public void connectDatabase() {
  3. System.out.println("连接数据库");
  4. }
  5. }
  6. class B extends A {
  7. public void test() {
  8. connectDatabase();
  9. }
  10. }

遵循合成复用

  1. class A {
  2. public void connectDatabase() {
  3. System.out.println("我是连接数据库操作");
  4. }
  5. }
  6. class B {
  7. public void test(A a) {
  8. a.connectDatabase();
  9. }
  10. }

迪米特法则

一个类应该尽量少的与其他实体发生相互作用

一个类/模块对其他的类/模块有越少的交互越好。这样,当一个类发生改动的时候,与其相关的类就可以尽可能少的受影响。
image.png
比如上述代码虽然能实现要求,但如果Socket类中的方法发生改变(名称变化、参数变化等等),那么Test类中与之有关联的部分都要发生修改。但其实我们在Test类只需要Socket的IP地址这个字符串参数,并不需要整个对象。