单一职责
每个类都完成与自己相关的那一部分任务
在SpringBoot项目中,我们往往会涉及到很多的controller
、service
或者mapper
,比如UserController
、GoodsController
,这其实就是单一职责规则的体现——每个类都只负责与自己相关的那一部分工作。
用户控制器不可能控制与商品相关的请求,这显然是不符合常理的。
开闭原则
开闭原则是指:
- 对拓展开放:如果有新的需求,可以对现有代码进行拓展,从而适应新的情况。
- 对修改关闭:类一旦设计完成,就可以独立完成其工作,而不要对已有的代码进行任何修改。
实现开放封闭的核心思想就是面向抽象编程,而不是面向具体编程
比如我们现在需要有一个画笔类,然后需要根据要求画出相应的形状。
public class GraphicEditor {
public void draw(Shape shape) {
if (shape.m_type == 1) {
drawRectangle();
} else if(shape.m_type == 2) {
drawCircle();
}
}
public void drawRectangle() {
System.out.println("画长方形");
}
public void drawCircle() {
System.out.println("画圆形");
}
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type=1;
}
}
class Circle extends Shape {
Circle() {
super.m_type=2;
}
}
}
我们来看看, 这个代码, 初看是符合要求了, 再想想, 要是我增加一种形状呢? 比如增加三角形。
- 首先,要增加一个三角形的类, 继承自
Shape
- 第二,要增加一个画三角形的方法
drawTrriage()
- 第三,在
draw
方法中增加一种类型type=3
的处理方案。
这就违背了开闭原则-对扩展开发,对修改关闭。增加一个类型,修改了三处代码。
比较合适的方法如下
public class GraphicEditor1 {
public void draw(Shape shape) {
shape.draw();
}
interface Shape {
void draw();
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("画矩形");
}
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("画圆形");
}
}
}
使用接口来定义一种抽象行为,而各个形状类自己规范自己的行为
如果现在需要增加一种类型:三角形。那就只需要
- 增加一个三角形类,实现
Shape
接口- 调用
draw
方法即可
整个过程都是基于原有接口进行拓展,没有进行修改,符合开闭原则
里氏替换原则
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
- 子类可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(输入/入参)要比父类方法输入参数更加宽松
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(方法的输出/返回值)要比父类更加严格或与父类一样。
假如现在有Coder
类和JavaCoder
类,并且JavaCoder
继承于Coder
类
但是JavaCoder
类对Coder
类中的coding
方法进行了重写,也就是对coding
方法进行了覆盖,显然违背了里氏替换原则。
为了解决这个问题,我们可以定义一个更高等级的类,并继承此类
abstract class People {
abstract void coding();
}
class Coder extends People {
@Override
void coding() {
System.out.println("程序员会写代码");
}
}
class JavaCoder extends People {
@Override
void coding() {
System.out.println("Java程序员想摆烂");
}
}
依赖倒转原则
依赖倒转原则在Spring框架中就有体现,比如依赖注入
在service
中我们一般会注入对应的dao
,在controller
中注入对应的service
,比如下面的代码
public class UserController {
@Autowired
private UserService userService;
@GetMapping("")
public String hello() {
return userService.hello();
}
}
UserService
应该是一个接口,为什么我们能直接调用接口方法?其实Spring注入的时候帮我们进行了类似下面的操作:UserService userService = new UserServiceImpl()
如果我们不使用注入的方式,那么就需要我们手动进行
new
对象,这就会导致类和类之间的耦合(如果我们不再使用这个实现类,那么就要更改代码中很多的部分)所以通过使用接口,可以弱化这种关联。我们只需要知道接口中定义了什么方法,直接使用即可。而不用关心接口中的方法具体是怎么实现的。
接口隔离原则
客户不应该依赖那些他们用不到的方法,只给每个客户提供所需要的接口。
这其实包含了两层意思:
- 接口的设计规则:接口的设计应该遵循最小接口原则,用户使用的方法不应该塞到同一个接口里。如果一个接口的方法没有被使用到,说明存在冗余,需要进一步进行分割。
- 接口的继承原则:如果一个接口A继承另一个接口B,则接口A相当于继承了接口B的方法,那么继承了接口B后的接口A也应该遵循上述原则:不应该包含用户不使用的方法。反之,则说明接口A被B给污染了,应该重新设计它们的关系。
假如有一个门(Door),有锁门(lock)和开锁(unlock)功能。此外,可以在门上安装一个报警器而使其具有报警(alarm)功能。用户可以选择一般的门,也可以选择具有报警功能的门。分析需求,找出其中的名词,我们不难得到三个候选类:门(Door)、普通门(CommonDoor)、有报警功能的门(AlarmDoor)。我们该如何设计这三个候选类之间的关系呢?
最简单的一种设计方式如下:
但普通门并不需要“报警”,因此这显然是违背接口隔离原则的
有三种解决方式:
- 使用接口的多实现:同时实现
Door
和Alarm
接口
- 继承CommonDoor类并实现
Alarm
接口
- 通过委托分离接口,实现
Alarm
接口并包含了CommonDoor
的对象和方案二有很多相似的地方,但是实现的方式不同。方案三是对方案二应用了组合/聚合复用,将继承关系转换为了聚合关系。
合成复用原则
合成复用原则核心思想在于软件复用的时候,尽量通过组合或聚合的方式实现调用,其次才考虑通过继承的方式来实现。
继承的缺点
- 破坏了类的封装性,父类对于子类来说是透明的。(白箱复用)
增加了子类和父类之间的耦合度,父类的任何修改都会影响子类的实现,不利于类的拓展和维护。
合成复用的优点
维护了类的封装性,成分对象的实现细节对于新对象是不可见的。(黑箱复用)
- 新类与成分对象之间的耦合度较低,只能通过成分对象提供的接口来访问成分对象。
不遵循合成复用的代码
class A {
public void connectDatabase() {
System.out.println("连接数据库");
}
}
class B extends A {
public void test() {
connectDatabase();
}
}
遵循合成复用
class A {
public void connectDatabase() {
System.out.println("我是连接数据库操作");
}
}
class B {
public void test(A a) {
a.connectDatabase();
}
}
迪米特法则
一个类应该尽量少的与其他实体发生相互作用
一个类/模块对其他的类/模块有越少的交互越好。这样,当一个类发生改动的时候,与其相关的类就可以尽可能少的受影响。
比如上述代码虽然能实现要求,但如果Socket
类中的方法发生改变(名称变化、参数变化等等),那么Test
类中与之有关联的部分都要发生修改。但其实我们在Test
类只需要Socket
的IP地址这个字符串参数,并不需要整个对象。