设计模式七大原则

设计模式七大原则

  • 单一职责原则(类和方法,接口)
  • 开闭原则 (扩展开放,修改关闭)
  • 里氏替换原则(基类和子类之间的关系)
  • 依赖倒置原则(依赖抽象接口,而不是具体对象)
  • 接口隔离原则(接口按照功能细分)
  • 迪米特法则 (类与类之间的亲疏关系)
  • 合成复用原则

单一职责原则

单一职责原则(SRP:Single responsibility principle)又称为单一功能原则: 它规定一个类应该只负责一项职责。

单一职责原则注意事项和细节:

  • 降低类的复杂度,一个类只负责一项职责。
  • 提高类的可读性,可维护性。
  • 降低变更引起的风险。
  • 通常情况下,我们应当遵守单一职责原则,只有当逻辑足够简单时,才可以在代码级别违反单一职责原则;只有类中方法足够少,可以在方法级别保持单一职责原则。

开闭原则

开闭原则(Open Closed Principle,OCP)的定义是: 一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。模块应尽量在不修改原代码的情况下进行扩展。

开闭原则的基本介绍:

  • 开闭原则是编程中最基础、最重要的设计原则。
  • 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是修改已有的代码来实现变化。
  • 一个软件实体,如类,模块和函数应该对扩展开发(提供方),对修改关闭(使用方)。用抽象构建框架,用实现扩展细节。
  • 编程中遵循其他原则,以及使用设计模式的目的就是遵循开闭原则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//这是一个用于绘图的类 [使用方]
class GraphicEditor {
//接收Shape对象,然后根据type,来绘制不同的图形
public void drawShape(Shape s) {
//**问题所在:此类属于使用方,但当我们需要扩展新的图形时,却要修改使用方,就不符合OCP原则
if (s.m_type == 1) {
drawRectangle(s);
}else if (s.m_type == 2) {
drawCircle(s);
}
}

//绘制矩形
public void drawRectangle(Shape r) {
System.out.println(" 绘制矩形 ");
}
//绘制圆形
public void drawCircle(Shape r) {
System.out.println(" 绘制圆形 ");
}
}

//Shape类,基类
class Shape {
int m_type;
}

class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
}

里氏替换原则

里氏代换原则(Liskov Substitution Principle,LSP)的定义: 所有引用基类的地方必须能透明地使用其子类的对象,子类可以扩展父类的功能,但不能改变父类原有的功能。面向对象(Object Oriented,OO)继承性的思考和说明:

  1. 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有子类都必须遵循这种契约,但是如果子类对这些已经实现的方法任意修改,就会对这个继承体系造成破坏。
  2. 继承在给程序设计带来方便的同时,也带来了弊端。比如使用继承给程序带来侵入性,程序可移植性降低,增加对对象间的耦合性。如果一个类被其他的类所继承,则当此类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障。
  3. 问题提出:在编程中如何正确的使用继承,答案是:遵循里氏替换原则

里氏替换原则基本介绍:

  • 如果对类型为T1的对象o1,对有类型为T2的对象o2,使得以T1定义的所有程序P中对象o1可以代替成o2,程序 P 的行为没有发生变化,那么类型T2是类型T1的子类型。换句话说,所有引用基类的地方能透明地使用其子类的对象。
  • 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法。
  • 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// A类
class A {
// 返回两个数的差
public int func1(int num1, int num2) {
return num1 - num2;
}
}

// B类继承了A
// 增加了一个新功能:完成两个数相加,然后和9求和
class B extends A {
//这里,重写了A类的方法, 可能是无意识
public int func1(int a, int b) {
return a + b;
}

public int func2(int a, int b) {
return func1(a, b) + 9;
}
}

依赖倒置原则

依赖倒转原则(Dependency Inversion Principle,DIP)的定义: 程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

依赖倒置的原则:

  • 高层模块不应该依赖底层模块,二者都应该依赖其抽象。
  • 抽象不应该依赖细节(实现类),细节应该依赖抽象。
  • 依赖倒置的中心思想是面向接口编程。
  • 依赖倒置的的设计理念是:相对于细节的多样性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳的多。在 Java 中,抽象指的是接口和抽象类,细节就是具体的实现类。
  • 使用接口或抽象类的目的是定制好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。

依赖倒置解决的问题如下(方法中确定的参数为类,而不是接口):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//完成 Person 接收消息的功能:这里receive方法中直接传入的对象是 类 也是依赖倒置重要强调的问题所在。
/**1. 如果我们获取的对象是 微信,短信等等,则新增类,同时Perons也要增加相应的接收方法getInfo()
* 2. 解决思路:引入一个抽象的接口IReceiver, 表示接收者, 这样Person类与接口IReceiver发生依赖
* 因为Email, WeiXin 等等属于接收的范围,他们各自实现IReceiver 接口就ok, 这样我们就符号依赖倒转原则
*/
class Person {
public void receive(Email email ) {
System.out.println(email.getInfo());
}
}

class Email {
public String getInfo() {
return "电子邮件信息: hello,world";
}
}

循依赖倒置原则后(方法中确定的参数修改为接口):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class DependecyInversion {

public static void main(String[] args) {
Person person = new Person();
//当为电子邮件时,传入邮件对象
person.receive(new Email());
//当为微信时,传入微信对象
person.receive(new WeiXin());
}

}

//定义接口
interface IReceiver {
public String getInfo();
}
//原电子邮件类,实现接口
class Email implements IReceiver {
public String getInfo() {
return "电子邮件信息: hello,world";
}
}

//增加微信
class WeiXin implements IReceiver {
public String getInfo() {
return "微信信息: hello,ok";
}
}

//方法中传入接口
class Person {
//这里我们是对接口的依赖
public void receive(IReceiver receiver ) {
System.out.println(receiver.getInfo());
}
}

接口隔离原则

接口隔离原则(Interface Segregation Principle,ISP)的定义: 客户端不应该依赖它不需要的接口类,类之间的依赖关系应该建立在最小的接口上。一句话,就是实现接口的类中,有多余的方法时,需要将接口进行拆分。

  • 使用接口隔离原则前首先需要满足单一职责原则。
  • 接口需要高内聚,也就是提高接口、类、模块的处理能力,少对外发布public的方法。
  • 定制服务,就是单独为一个个体提供优良的服务,简单来说就是拆分接口,对特定接口进行定制。
  • 接口设计是有限度的,接口的设计粒度越小,系统越灵活,但是值得注意不能过小,否则变成”字节码编程”。

解决以下问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//接口
interface Interface1 {
void operation1();
void operation2();
void operation3();
}

class B implements Interface1 {
public void operation1() {
System.out.println("B 实现了 operation1");
}
public void operation2() {
System.out.println("B 实现了 operation2");
}
public void operation3() {
System.out.println("B 实现了 operation3");
}
}
//问题所在:A类只用到了B类的 1,2 方法,但B类却要实现方法3,造成代码的冗余。
class A { //A 类通过接口Interface1 依赖(使用) B类,但是只会用到1,2方法
public void depend1(Interface1 i) {
i.operation1();
}
public void depend2(Interface1 i) {
i.operation2();
}
}

正确方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 接口1
interface Interface1 {
void operation1();
void operation2();
}

// 接口2
interface Interface2 {
void operation3();
}

class B implements Interface1 {
public void operation1() {
System.out.println("B 实现了 operation1");
}

public void operation2() {
System.out.println("B 实现了 operation2");
}

}

class A { // A 类通过接口Interface1,Interface2 依赖(使用) B类,但是只会用到1,2,3方法
public void depend1(Interface1 i) {
i.operation1();
}

public void depend2(Interface2 i) {
i.operation2();
}
}

迪米特法则

迪米特法则(Law of Demeter,LOD),有时候也叫做最少知识原则(Least Knowledge Principle,LKP)定义是: 一个软件实体应尽可能少地与其他实体发生相互作用。迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块独立,相互之间不存在(或很少有)依赖关系。迪米特法则则不希望类之间建立直接的关系。如果真的有需要建立联系,也希望能通过它的友元类(中间类或者跳转类)来转达。

迪米特法则的规则:

  • Only talk to your immediate friends(只与直接的朋友通讯):每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系, 我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友(例如,在一个方法中new了一个类,那么此类就不属于直接朋友)。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。
  • 一个对象应该对其他对象保持最少的了解。
  • 类与类关系越密切,耦合度越大。
  • 迪米特法则指一个类对自己依赖的类知道的 越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public 方法,不对外泄露任何信息。
  • 迪米特法则还有个更简单的定义:只与直接的朋友通信。

迪米特法则解决的问题如下:(方法中出现了局部变量)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//通过查看如下代码会发现,CollegeEmployee 以局部变量的形式出现在方法 printAllEmployee 中,违反了迪米特法则
public class Demeter1 {
//该方法完成输出学校总部和学院员工信息(id)
void printAllEmployee(CollegeManager sub) {

//分析问题
//1. 这里的 CollegeEmployee 不是 SchoolManager的直接朋友
//2. CollegeEmployee 是以局部变量方式出现在 SchoolManager
//3. 违反了 迪米特法则

//获取到学院员工
List<CollegeEmployee> list1 = sub.getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
}

遵循迪米特法则后(将局部变量部分,提取到自己的类中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class DemeterUpdate {
void printAllEmployee(CollegeManager sub) {
//分析问题
//1. 将输出学院的员工方法,封装到CollegeManager
sub.printEmployee();
}
}

//管理学院员工的管理类
class CollegeManager {
//输出学院员工的信息
public void printEmployee() {
//获取到学院员工
List<CollegeEmployee> list1 = getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
}

//返回学院的所有员工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
}

合成复用原则

合成复用原则的定义是: 原则是尽量使用合成/聚合的方法,而不是使用继承。

聚合用来表示“拥有”关系或者整体与部分的关系: 代表部分的对象有可能会被多个代表整体的对象所共享,而且不一定会随着某个代表整体的对象被销毁或破坏而被销毁或破坏,部分的生命周期可以超越整体。例如,班级和学生,当班级删除后,学生还能存在,学生可以被培训机构引用。

1
2
3
4
5
6
7
8
9
10
11
12
class OpenAndClose implements IOpenAndClose {
private ITV tv;
//通过set方法将ITV对象聚合到OpenAndClose对象中
public void setTv(ITV tv) {
this.tv = tv;
}

public void open() {
this.tv.play();
}
}

合成用来表示一种强得多的“拥有”关系: 在一个合成关系里,部分和整体的生命周期是一样的。一个合成的新对象完全拥有对其组成部分的支配权,包括它们的创建和湮灭等。使用程序语言的术语来说,合成的新对象对组成部分的内存分配、内存释放有绝对的责任。例如,一个人由头、四肢和各种器官组成,人与这些具有相同的生命周期,人死了,这些器官也就挂了。房子和房间的关系,当房子没了,房间也不可能独立存在。

1
2
3
4
class OpenAndClose implements IOpenAndClose {
//将 ITV 对象组合到 OpenAndClose 对象中
ITV tv = new ITV();
}

【总结】设计原则的核心思想:

【1】精确应用中可能需要变化的地方,把它们独立出来,不要和那些不需要变化的代码混在一起。
【2】针对编程接口,而不是针对实际编程
【3】为了交互对象之间的松耦合设计和努力。

简单理解就是:开闭原则是总纲,它指导我们要对扩展开放,对修改封闭;单一职责原则指导我们实现类要职责单一;里氏替换原则指导我们不破坏继承体系;依赖倒置原则指导我们针对接口编程;接口隔离原则指导我们在设计接口的时候要专业化;迪米特原则指导我们要降低耦合。

[设计模式:七大原则]: https://it-blog-cn.com/blogs/design_mode/seven_principle.html#%E4%B8%80%E3%80%81%E5%8D%95%E4%B8%80%E8%81%8C%E8%B4%A3%E5%8E%9F%E5%88%99 “设计模式:七大原则”


设计模式七大原则
https://xsinxcos.github.io/2024/04/25/设计模式七大原则/
作者
xsinxcos(涿)
发布于
2024年4月25日
许可协议