设计模式之七大原则(三)——接口隔离原则、迪米特法则、组合/聚合复用原则
接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。根绝接口隔离原则,当一个接口太大时,我们需要将它分割成一些细小的接口,使用该接口的客户端只需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事情,干该干的事请
1.接口隔离原则
接口隔离原则定义如下:
接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
一个类对另一个类的依赖应该建立在最小的接口上。
根绝接口隔离原则,当一个接口太大时,我们需要将它分割成一些细小的接口,使用该接口的客户端只需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事情,干该干的事请。这里的"接口"往往有两种不同的定义:一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象;另外一种是某种语言上具体的"接口"定义,比如Java语言的interface。
接口隔离原则优点
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
- 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
- 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
接口隔离原则的实现方法
- 在具体应用接口隔离原则时,应该根据以下几个规则来衡量。
- 接口尽量小,但是要有限度。一个接口只服务一个子模块或业务逻辑。
- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
以下假设一个场景并用代码演示(项目代码名称为ISP):
创建类Client.java,代码如下:
public class Client {
public static void main(String[] args) {
A a = new A();
a.b1(new B()); // A 类通过接口去依赖 B 类
a.b2(new B());
a.b3(new B());
System.out.println("-----------------------");
C c = new C();
c.d1(new D()); // C 类通过接口去依赖(使用)D 类
c.d2(new D());
c.d3(new D());
}
}
//A 类通过接口 Interface1 依赖(使用) B 类,但是只会用到 1,2,3 方法
class A {
public void b1(Interface1 i) {
i.run1();
}
public void b2(Interface1 i) {
i.run2();
}
public void b3(Interface1 i) {
i.run3();
}
}
//C 类通过接口 Interface1 依赖(使用) D 类,但是只会用到 1,4,5 方法
class C {
public void d1(Interface1 i) {
i.run1();
}
public void d2(Interface1 i) {
i.run4();
}
public void d3(Interface1 i) {
i.run5();
}
}
//接口
interface Interface1 {
void run1();
void run2();
void run3();
void run4();
void run5();
}
class B implements Interface1 {
public void run1() {
System.out.println("B 实现了 run1");
}
public void run2() {
System.out.println("B 实现了 run2");
}
public void run3() {
System.out.println("B 实现了 run3");
}
public void run4() {
System.out.println("B 实现了 run4");
}
public void run5() {
System.out.println("B 实现了 run5");
}
}
class D implements Interface1 {
public void run1() {
System.out.println("D 实现了 run1");
}
public void run2() {
System.out.println("D 实现了 run2");
}
public void run3() {
System.out.println("D 实现了 run3");
}
public void run4() {
System.out.println("D 实现了 run4");
}
public void run5() {
System.out.println("D 实现了 run5");
}
}
运行结果如下所示:
B 实现了 run1
B 实现了 run2
B 实现了 run3
-----------------------
D 实现了 run1
D 实现了 run4
D 实现了 run5
修改代码,改为用ISP原则:
创建代码ClientISP.java,代码如下:
public class ClientISP {
public static void main(String[] args) {
A a = new A();
a.b1(new B()); // A 类通过接口去依赖 B 类
a.b2(new B());
a.b3(new B());
System.out.println("-----------------------");
C c = new C();
c.d1(new D()); // C 类通过接口去依赖(使用)D 类
c.d2(new D());
c.d3(new D());
}
}
//A 类通过接口 Interface1 依赖(使用) B 类,但是只会用到 1,2,3 方法
class A {
public void b1(Interface1 i) {
i.run1();
}
public void b2(Interface2 i) {
i.run2();
}
public void b3(Interface2 i) {
i.run3();
}
}
//C 类通过接口 Interface1 依赖(使用) D 类,但是只会用到 1,4,5 方法
class C {
public void d1(Interface1 i) {
i.run1();
}
public void d2(Interface3 i) {
i.run4();
}
public void d3(Interface3 i) {
i.run5();
}
}
//接口
interface Interface1 {
void run1();
}
interface Interface2 {
void run2();
void run3();
}
interface Interface3 {
void run4();
void run5();
}
class B implements Interface1, Interface2 {
public void run1() {
System.out.println("B 实现了 run1");
}
public void run2() {
System.out.println("B 实现了 run2");
}
public void run3() {
System.out.println("B 实现了 run3");
}
public void run4() {
System.out.println("B 实现了 run4");
}
public void run5() {
System.out.println("B 实现了 run5");
}
}
class D implements Interface1, Interface3 {
public void run1() {
System.out.println("D 实现了 run1");
}
public void run2() {
System.out.println("D 实现了 run2");
}
public void run3() {
System.out.println("D 实现了 run3");
}
public void run4() {
System.out.println("D 实现了 run4");
}
public void run5() {
System.out.println("D 实现了 run5");
}
}
运行结果如下所示:
B 实现了 run1
B 实现了 run2
B 实现了 run3
-----------------------
D 实现了 run1
D 实现了 run4
D 实现了 run5
重构之后每个接口都承担自己的职责,灵活性很高,使用起来很方便,每个接口中都只包含和自己业务相关的方法,不会存在和自己无关的方法,达到了高内聚、松耦合的效果。
2.迪米特法则
迪米特法则定义如下:
迪米特法则(Law of Demeter, LOD)也称为最少知识原则,它们的意思为:一个对象应该对其他对象有最少的了解。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的 public 方法,不对外泄露任何信息
迪米特法则的定义是:只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
迪米特法则的优点
迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点。
- 降低了类之间的耦合度,提高了模块的相对独立性。
- 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
迪米特法则的实现方法
从迪米特法则的定义和特点可知,它强调以下两点:
- 从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
所以,在运用迪米特法则时要注意以下:
- 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
- 在类的结构设计上,尽量降低类成员的访问权限。
- 在类的设计上,优先考虑将一个类设置成不变类。
- 在对其他类的引用上,将引用其他对象的次数降到最低。
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
- 谨慎使用序列化(Serializable)功能。
如果一个系统满足迪米特法则,那么当其中一个软件实体发生变化时,就会尽量少的影响其他软件实体,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可以降低系统的耦合度,使类与类之间保持松耦合状态。
以下假设一个场景并用代码演示(项目代码名称为LOD):
创建类School.java,代码如下:
import java.util.ArrayList;
import java.util.List;
public class School {
public static void main(String[] args) {
Teacher teacher = new Teacher();
//输出学院的教师id和学生id
teacher.printPeople(new Student());
}
}
//学校教师类
class EmployeeDTO {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
//学院的学生类
class StudentDTO {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
//学院学生管理类
class Student {
//返回学院的所有学生
public List<StudentDTO> getAllEmployee() {
List<StudentDTO> list = new ArrayList<>();
//增加 10 个员工到 list
for (int i = 0; i < 10; i++) {
StudentDTO stu = new StudentDTO();
stu.setId("学院学生id= " + i);
list.add(stu);
}
return list;
}
}
//教师管理类
class Teacher {
//返回学校教师
public List<EmployeeDTO> getAllEmployee() {
List<EmployeeDTO> list = new ArrayList<EmployeeDTO>();
//增加 5 个员工到 list
for (int i = 0; i < 5; i++) {
EmployeeDTO emp = new EmployeeDTO();
emp.setId("学校教师id= " + i);
list.add(emp);
}
return list;
}
//该方法完成输出学校总部和学院员工信息(id)
void printPeople(Student sub) {
/*
问题分析:
1. 这里的 StudentDTO 不是 EmployeeDTO 的直接朋友
2. StudentDTO 是以局部变量方式出现在 EmployeeDTO
3. 违反了 迪米特法则
*/
//获取到学院学生数据
List<StudentDTO> list1 = sub.getAllEmployee();
System.out.println("------------学院学生------------");
for (StudentDTO e : list1) {
System.out.println(e.getId());
}
//获取到学校总部员工
List<EmployeeDTO> list2 = this.getAllEmployee();
System.out.println("------------学校教师------------");
for (EmployeeDTO e : list2) {
System.out.println(e.getId());
}
}
}
运行结果如下所示:
------------学院学生------------
学院学生id= 0
学院学生id= 1
学院学生id= 2
学院学生id= 3
学院学生id= 4
学院学生id= 5
学院学生id= 6
学院学生id= 7
学院学生id= 8
学院学生id= 9
------------学校教师------------
学校教师id= 0
学校教师id= 1
学校教师id= 2
学校教师id= 3
学校教师id= 4
修改代码,改为用LOD原则:
创建代码SchoolLOD.java,代码如下:
import java.util.ArrayList;
import java.util.List;
public class SchoolLOD {
public static void main(String[] args) {
Teacher teacher = new Teacher();
//输出学院的教师id和学生id
teacher.printPeople(new Student());
}
}
//学校教师类
class EmployeeDTO {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
//学院的学生类
class StudentDTO {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
//学院学生管理类
class Student {
//返回学院的所有学生
public List<StudentDTO> getAllEmployee() {
List<StudentDTO> list = new ArrayList<>();
//增加 10 个员工到 list
for (int i = 0; i < 10; i++) {
StudentDTO stu = new StudentDTO();
stu.setId("学院学生id= " + i);
list.add(stu);
}
return list;
}
public void printStudent(){
//获取到学院员工
List<StudentDTO> list1 = getAllEmployee();
System.out.println("------------学院学生------------");
for (StudentDTO e : list1) {
System.out.println(e.getId());
}
}
}
//教师管理类
class Teacher {
//返回学校教师
public List<EmployeeDTO> getAllEmployee() {
List<EmployeeDTO> list = new ArrayList<EmployeeDTO>();
//增加 5 个员工到 list
for (int i = 0; i < 5; i++) {
EmployeeDTO emp = new EmployeeDTO();
emp.setId("学校教师id= " + i);
list.add(emp);
}
return list;
}
//该方法完成输出学校总部和学院员工信息(id)
void printPeople(Student sub) {
//将输出学院学生方法,封装到 Student
sub.printStudent();
//获取到学校总部员工
List<EmployeeDTO> list2 = this.getAllEmployee();
System.out.println("------------学校教师------------");
for (EmployeeDTO e : list2) {
System.out.println(e.getId());
}
}
}
运行结果如下所示:
------------学院学生------------
学院学生id= 0
学院学生id= 1
学院学生id= 2
学院学生id= 3
学院学生id= 4
学院学生id= 5
学院学生id= 6
学院学生id= 7
学院学生id= 8
学院学生id= 9
------------学校教师------------
学校教师id= 0
学校教师id= 1
学校教师id= 2
学校教师id= 3
学校教师id= 4
迪米特法则注意事项和细节
迪米特法则的核心是降低类之间的耦合
注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类之间或对象之间的耦合关系,并不是要求完全没有依赖关系
3.组合/聚合复用原则
组合/聚合复用原则定义如下:
合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
合成/聚合复用原则是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。
组合/聚合复用原则的优点
由于合成或聚合可以将已有对象纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能。这样做的好处有
- 新对象存取成分对象的唯一方法是通过成分对象的接口。
- 这种复用是黑箱复用,因为成分对象的内部细节是新对象看不见的。
- 这种复用支持包装。
- 这种复用所需的依赖较少。
- 每一个新的类可以将焦点集中到一个任务上。
- 这种复用可以再运行时间内动态进行,新对象可以动态地引用与成分对象类型相同的对象。
一般而言,如果一个角色得到了更多的责任,那么可以使用合成/聚合关系将新的责任委派到合适的对象。当然,这种复用也有缺点。最主要的缺点就是通过这种复用建造的系统会有较多的对象需要管理。
组合/聚合复用原则的实现方法
由于组合或聚合关系可以将已有的对象(也可称为成员对象)纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使得成员对象的内部实现细节对于新对象不可见,所以这种复用又称为“黑箱”复用,相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作;合成复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。
以下假设一个场景并用代码演示(项目代码名称为CRP):
创建类Shop.java,代码如下:
public class Shop {
public String getShopping() {
return "线上商店销售";
}
}
class Fruit extends Shop {
public void addFruit() {
String fruit = super.getShopping();
System.out.println(String.format("%s苹果", fruit));
}
}
class test {
public static void main(String[] args) {
Fruit fruit = new Fruit();
fruit.addFruit();
}
}
运行结果如下所示:
线上商店销售苹果
假设现在需要添加线下销售苹果的方式,
而目前Shop 中只会返回线上销售方法。
其实也简单,我们在 Shop中再新增一个方法用于获取 线下销售方法。功能实现上其实是没什么问题的,但是这样会违反开闭原则。
那如何利用合成复用原则对示例代码进行重构?代码如下:
创建类Shop.java,代码如下:
public abstract class ShopCRP {
public abstract String getShopping();
}
class Online extends ShopCRP {
@Override
public String getShopping() {
return "线上商店销售";
}
}
class Offline extends ShopCRP {
@Override
public String getShopping() {
return "线下商店销售";
}
}
class Fruit {
private ShopCRP shopCRP;
public void setShopCRP(ShopCRP shopCRP) {
this.shopCRP = shopCRP;
}
public void addFruit() {
System.out.println(String.format("%s苹果", shopCRP.getShopping()));
}
}
class test {
public static void main(String[] args) {
Fruit fruit = new Fruit();
fruit.setShopCRP(new Online());
fruit.addFruit();
fruit.setShopCRP(new Offline());
fruit.addFruit();
}
}
运行结果如下所示:
线上商店销售苹果
线下商店销售苹果
如果还有扩展,则只要写与 Online 平级的类继承 ShopCRP ,而具体的选择则交由应用层。
优点
- 提高系统的灵活性
- 降低类与类之间的耦合
- 一个类的变化对其他类造成的影响相对较少
以上代码下载请点击该链接:https://github.com/Yarrow052/Java-package.git
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)