设计模式的基本原则就是设计模式设计的依据所在,是设计模式的基础。开发人员在编码时需要遵守这些基本原则,才能使得编写的代码可维护、可扩展、可重用、灵活性强,主要有六个基本原则:单一职责原则、接口隔离原则、依赖倒转原则、里氏替换原则、开闭原则和迪米特法则。
1. 单一职责原则
定义:一个类只负责一项职责,如类 A 负责两个不同职责:职责1 和职责 2. 当职责1 需求变更而改变类 A 时,可能造成职责2 执行错误,因此需要将类 A 的粒度分解为:A1, A2.
应用案例:
1、有一个交通工具类 Vehicle, 其包含实例方法 run()
,不同的交通工具(摩托车、汽车、飞机等)通过调用 run()
方法“启动”。
public class singleresponsibility { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); vehicle.run("摩托车"); vehicle.run("汽车"); vehicle.run("飞机"); vehicle.run("轮船"); } } class Vehicle { public void run(String vehicle) { System.out.println(vehicle+"在公路上运行..."); } }
在案例1中的 run() 方法,类 Vehicle 负责了多项职责,即摩托车、汽车和飞机的运行,这就违反了单一职责原则。
2、为了遵守单一职责原则,应该让 Vehicle 类只负责一项职责,可以将 Vehicle 类分解为多个不同的类,根据不同的交通工具调用不同类的 run() 方法。
public class singleresponsibility { public static void main(String[] args) { RoadVehicle roadVehicle = new RoadVehicle(); roadVehicle.run("摩托车"); roadVehicle.run("汽车"); AirVehicle airVehicle = new AirVehicle(); airVehicle.run("飞机"); WaterVehicle waterVehicle = new WaterVehicle(); waterVehicle.run("轮船"); } } class RoadVehicle { public void run(String vehicle) { System.out.println(vehicle+"公路运行"); } } class AirVehicle { public void run(String vehicle) { System.out.println(vehicle+"天空运行"); } } class WaterVehicle { public void run(String vehicle) { System.out.println(vehicle+"水中运行"); } }
案例2中将类进行了分解,使得每个类只负责一项职责,遵守了单一职责原则。但是,可以发现将类进行分解之后,不仅需要增加新的类,而且还要改动客户端调用的代码。
3、为了减少代码的改动,并且还要遵守单一职责原则,可以选择在类中增加方法,达到这样的目的。
public class singleresponsibility { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); vehicle.run("摩托车"); vehicle.run("汽车"); vehicle.runAir("飞机"); vehicle.runWater("轮船"); } } class Vehicle { public void run(String vehicle) { System.out.println(vehicle+"在公路上运行..."); } public void runAir(String vehicle) { System.out.println(vehicle+"在天空上运行..."); } public void runWater(String vehicle) { System.out.println(vehicle+"在水中上运行..."); } }
单一职责原则注意事项:
- 降低类的复杂度,一个类只负责一项职责。
- 提高类的可读性和可维护性。
- 降低变更引起的风险。
- 通常情况下,我们应当遵守单一职责原则,但是当逻辑非常简单时,可以在代码级别违反单一职责原则。只有类中方法数量足够少时,才可以在方法级别遵守单一职责原则。
2. 接口隔离原则
定义:一个类对另一个类的依赖应该建立在最小的接口,客户端不应该依赖它不需要的接口。
应用案例:
1、类 A 通过接口 Interface1 依赖类 B,但是只会使用接口的1,2,3方法,类 C 通过接口 Interface1 依赖类 D,但只会使用接口的1,4,5方法,其 UML 图和实现代码如下所示:
interface Interface1 { void operation1(); void operation2(); void operation3(); void operation4(); void operation5(); } class B implements Interface1 { @Override public void operation1() { System.out.println("B 实现了 operation1"); } @Override public void operation2() { System.out.println("B 实现了 operation2"); } @Override public void operation3() { System.out.println("B 实现了 operation3"); }@Override public void operation4() { System.out.println("B 实现了 operation4"); } @Override public void operation5() { System.out.println("B 实现了 operation5"); } } class D implements Interface1 { @Override public void operation1() { System.out.println("D 实现了 operation1"); } @Override public void operation2() { System.out.println("D 实现了 operation2"); } @Override public void operation3() { System.out.println("D 实现了 operation3"); }@Override public void operation4() { System.out.println("D 实现了 operation4"); } @Override public void operation5() { System.out.println("D 实现了 operation5"); } } class A { //A类通过接口Interface1依赖(使用)B类,但是只会使用到operation1,2,3 public void depend1(Interface1 i) { i.operation1(); } public void depend2(Interface1 i) { i.operation2(); } public void depend3(Interface1 i) { i.operation3(); } } class C { //C类通过接口Interface1依赖(使用)D类,但是只会用到1,4,5方法 public void depend1(Interface1 i) { i.operation1(); } public void depend4(Interface1 i) { i.operation4(); } public void depend5(Interface1 i) { i.operation5(); } }
在案例1中,类 A 通过接口 Interface1 依赖类 B,类 C 通过接口 Interface1 依赖类 D,而接口 Interface1 对于类 A 和类 C 来说不是最小接口,那么类 B 和类 D 必须实现它们不需要的方法,这就违反了接口隔离原则。
2、为了让类 B 和类 D 不用实现它们不需要的方法,可以将接口 Interface1 拆分为三个接口,类 A 和类 C 分别与他们需要的接口建立依赖关系,其 UML 图和实现代码如下所示:
public class Segregation1 { public static void main(String[] args) { A a = new A(); a.depend1(new B()); //A类通过接口去依赖(使用)B类 a.depend2(new B()); a.depend3(new B()); C c = new C(); c.depend1(new D()); //C类通过接口去依赖(使用)D类 c.depend4(new D()); c.depend5(new D()); } } //接口 interface Interface1 { void operation1(); } interface Interface2 { void operation2(); void operation3(); } interface Interface3 { void operation4(); void operation5(); } class B implements Interface1, Interface2 { @Override public void operation1() { System.out.println("B 实现了 operation1"); } @Override public void operation2() { System.out.println("B 实现了 operation2"); } @Override public void operation3() { System.out.println("B 实现了 operation3"); } } class D implements Interface1, Interface3 { @Override public void operation1() { System.out.println("D 实现了 operation1"); } @Override public void operation4() { System.out.println("D 实现了 operation4"); } @Override public void operation5() { System.out.println("D 实现了 operation5"); } } class A { //A类通过接口Interface1依赖(使用)B类,但是只会使用到operation1,2,3 public void depend1(Interface1 i) { i.operation1(); } public void depend2(Interface2 i) { i.operation2(); } public void depend3(Interface2 i) { i.operation3(); } } class C { //C类通过接口Interface1依赖(使用)D类,但是只会用到1,4,5方法 public void depend1(Interface1 i) { i.operation1(); } public void depend4(Interface3 i) { i.operation4(); } public void depend5(Interface3 i) { i.operation5(); } }
3. 依赖倒转原则
定义:
高层模块不应该依赖底层模块,两者都应该依赖其抽象。
抽象不应该依赖细节,细节应该依赖抽象。
依赖倒转的核心思想是面向接口编程:相对于细节的多变性,抽象要稳定的多,以抽象为基础搭建的架构比以细节为基础搭建的架构稳定的多。在面向对象编程语言中,这里的抽象就是指接口或者抽象类,细节就是指具体的实现类。
使用接口或抽象类是制定好规范,不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
应用案例:
1、有一个 Person 类,通过调用它的实例方法 receive()
来接收信息。
public class DependencyInversion { public static void main(String[] args) { Person person = new Person(); person.receive(new Email()); } } class Email { public String getInfo() { return "电子邮件信息:hello,world"; } } class Person { public void receive(Email email) { System.out.println(email.getInfo()); } }
在案例1中,如果获取的对象是微信,短信等,则新增类 Wechat 同时 Person 也要增加相应的接收方法,这就使得高层模块依赖了底层模块,而不是依赖其抽象。
2、引入一个抽象的接口 IReceiver 表示接收者,Eemail 和 Wechat 各自实现接口 IReceiver,之后 Person 类与接口 IReceiver 发生依赖,符合依赖倒转原则。
public class DependencyInversion { public static void main(String[] args) { //客户端无需改变 Person person = new Person(); person.receive(new Email()); person.receive(new Wechat()); } } interface IReceiver { String getInfo(); } class Email implements IReceiver{ @Override public String getInfo() { return "电子邮件信息:hello,world"; } } class Wechat implements IReceiver { @Override public String getInfo() { return "微信消息:hello,world"; } } class Person { public void receive(IReceiver receiver) { System.out.println(receiver.getInfo()); } }
依赖关系传递的三种方式:
1、接口传递
public class DependencyPass { public static void main(String[] args) { OpenAndClose openAndClose = new OpenAndClose(); openAndClose.open(new HUAWEI()); } } interface IOpenAndClose { public void open(ITV tv); } interface ITV { public void play(); } class HUAWEI implements ITV { @Override public void play() { System.out.println("打开华为电视机"); } } class OpenAndClose implements IOpenAndClose { @Override public void open(ITV tv) { tv.play(); } }
2、构造器传递
public class DependencyPass { public static void main(String[] args) { OpenAndClose openAndClose = new OpenAndClose(new HUAWEI()); openAndClose.open(); } } interface IOpenAndClose { void open(); } interface ITV { void play(); } class HUAWEI implements ITV { @Override public void play() { System.out.println("打开华为电视机"); } } class OpenAndClose implements IOpenAndClose { public ITV tv; public OpenAndClose(ITV tv) { this.tv = tv; } @Override public void open() { tv.play(); } }
3、通过 setter 方式传递
public class DependencyPass { public static void main(String[] args) { OpenAndClose openAndClose = new OpenAndClose(); openAndClose.setTv(new HUAWEI()); openAndClose.open(); } } interface IOpenAndClose { void open(); void setTv(ITV tv); } interface ITV { void play(); } class HUAWEI implements ITV { @Override public void play() { System.out.println("打开华为电视机"); } } class OpenAndClose implements IOpenAndClose { private ITV tv; @Override public void open() { tv.play(); } @Override public void setTv(ITV tv) { this.tv = tv; } }
依赖倒转原则的注意事项:
- 低层模块尽量都要有抽象类或接口,或两者都有,这样程序的稳定性才更好。
- 变量的声明类型尽量是抽象类或接口,这样变量的引用和实际对象间存在一个缓冲,有利于程序扩展和优化。
4. 里氏替换原则
定义:如果对每个类型为 T1 的对象 o1, 都有类型为 T2 的对象 o2, 使得以 T1 定义的所有程序 P 在所有的对象 o1 都替换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。换而言之,所有引用基类的地方必须能够透明地使用其子类的对象。
应用案例:
1、类 A 作为基类,类 B 继承类 A 并重写了类 A 中的 func()
方法。
public class Liskov { public static void main(String[] args) { A a = new A(); System.out.println("11-3="+a.func(11, 3)); System.out.println("--------------------"); B b = new B(); System.out.println("11-3="+b.func(11, 3));//本意是求出11-3,却变成11+3 } } //A类 class A { //增加新的功能,这里重写了A类的方法,可能是无意识的 public int func(int num1, int num2) { return num1 - num2; } } class B extends A { //增加新的功能,无意识地重写了A类的方法 public int func(int a, int b) { return a + b; } }
在案例1中,类 B 重写了基类 A 的 func1() 方法,即使可能是无意识的重写,原本行为是实现两数相加却变成了两数相减,改变了程序的行为这就违背了里氏替换原则。
在实际场景中,通过重写父类的方法完成新的功能这种方式实现起来确实简单,但是将会导致整个继承体系的复用性会比较差,特别是在多态使用频繁的时候。
2、为了遵守里氏替换原则,可以将原有的继承关系去掉,让原来的父子类都继承一个基类,采用依赖、聚合和组合关系替代继承关系,其 UML 图和实现代码如下所示:
public class Liskov { public static void main(String[] args) { A a = new A(); System.out.println("11-3="+a.func1(11, 3)); System.out.println("--------------------"); B b = new B(); System.out.println("11+3="+b.func1(11, 3));//这里本意是求出11+3 System.out.println("11-3="+b.func2(11, 3)); //11-3 } } //创建一个更加基础的基类 class Base { //把更加基础的方法和成员写到Base类 } class A extends Base{ public int func1(int num1, int num2) { //返回两个数的差 return num1 - num2; } } class B extends Base { //如果B类需要使用A类的方法,使用组合关系 private A a = new A(); public int func1(int a, int b) { return a + b; } //仍然想使用A的方法 public int func2(int a, int b) { return a.func1(a,b); } }
里氏替换原则的注意事项:
- 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法。
- 继承让两个类的耦合性变得更强,为了降低类间的耦合性,适当情况下可以使用聚合、组合和依赖来解决问题。
5. 开闭原则
定义:
- 一个软件实体类,模块和函数应该对扩展对扩展开放(对服务提供方开放),对修改关闭(对服务使用方),用抽象构建框架用实体扩展细节。
- 当软件需求发生变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码实现变化。
应用案例:
1、有一个画图类 Graphical
, 通过调用方法 drawShape(Shape s)
进行画图,根据传入的 s.shape
可以画出不同的图形,其 UML 图和实现代码如下所示:
public class OCP { public static void main(String[] args) { GraphicEditor graphicEditor = new GraphicEditor(); graphicEditor.drawShape(new Rectangle()); graphicEditor.drawShape(new Circle()); } } class GraphicEditor { public void drawShape(Shape s) { 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("绘制圆形"); } } class Shape { int m_type; } class Rectangle extends Shape { Rectangle() { super.m_type = 1; } } class Circle extends Shape { Circle() { super.m_type = 2; } }
在案例1中,如果需要新增一个可以画三角形的功能,不仅需要新增类 Triangle 继承 Shape,与此同时还要修改类 GraphicEditor ,在 drawShape(Shape s) 新加一个分支,并且还要增加一个 drawTriangle(Shape s) 方法。这一系列行为都是对使用方的修改,违背了开闭原则。
2、为了遵守开闭原则,可以考虑将 Shape 作为抽象类并提供抽象方法 draw(),让子类实现抽象类 Shape,每当有新的图形种类增加时,可以直接继承 Shape 并实现 draw() 方法,无须对使用方进行修改。
public class OCP { public static void main(String[] args) { GraphicEditor graphicEditor = new GraphicEditor(); graphicEditor.drawShape(new Rectangle()); graphicEditor.drawShape(new Circle()); graphicEditor.drawShape(new Triangle()); graphicEditor.drawShape(new OtherGraphic()); } } class GraphicEditor { public void drawShape(Shape s) { s.draw(); } } abstract class Shape { public abstract void draw(); } class Rectangle extends Shape { @Override public void draw() { System.out.println("绘制矩形"); } } class Circle extends Shape { @Override public void draw() { System.out.println("绘制圆形"); } } class Triangle extends Shape { @Override public void draw() { System.out.println("绘制三角形"); } } class OtherGraphic extends Shape { @Override public void draw() { System.out.println("绘制其他图形"); } }
6. 迪米特法则
定义:
迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。不管被依赖的类多么复杂,都尽量将逻辑封装在类的内部,对外除了 public 方法不泄露任何信息。
迪米特法则还有个更简单的定义:只与直接的朋友通信。
每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式有很多:依赖、关联、组合和聚合等。
其中,出现在成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类不是朋友关系,陌生的类最好不要以局部变量的形式出现在类的内部。
应用案例:
1、有一个学校,下面有总部和各个学院,要求打印出学校总部员工和各个学院员工的 ID.
public class Demeter { public static void main(String[] args) { //创建了一个SchoolManager对象 SchoolManager schoolManager = new SchoolManager(); //输出学院员工和学校总部员工ID schoolManager.printAllEmployee(new CollegeManager()); } } //学校总部员工 class Employee { private String id; public void setId(String id) { this.id = id; } public String getId() { return id; } } class CollegeEmployee { private String id; public void setId(String id) { this.id = id; } public String getId() { return id; } } class CollegeManager { //返回学院所有员工 public List<CollegeEmployee> getAllEmployee() { ArrayList<CollegeEmployee> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { CollegeEmployee emp = new CollegeEmployee(); emp.setId("学院员工id="+i); list.add(emp); } return list; } } class SchoolManager { //返回学校总部所有员工 public List<Employee> getAllEmployee() { ArrayList<Employee> list = new ArrayList<>(); for (int i = 0; i < 5; i++) { Employee emp = new Employee(); emp.setId("学校总部员工id="+i); list.add(emp); } return list; } //输出学校总部和学院员工信息 public void printAllEmployee(CollegeManager sub) { //CollegeManager作为printAllEmployee局部变量 List<CollegeEmployee> list1 = sub.getAllEmployee(); System.out.println("--------------分公司员工-------------"); for (CollegeEmployee e : list1) { System.out.println(e.getId()); } List<Employee> list2 = this.getAllEmployee(); System.out.println("-------------学校总部员工-------------"); for (Employee e : list2) { System.out.println(e.getId()); } } }
在案例1中,CollegeEmployee 以局部变量的形式出现在 SchoolManager 类中,作为 SchoolManager 的陌生类,并不是它的直接朋友,这就违背了迪米特法则。
2、为了遵守迪米特法则,在类中应该避免这种非直接朋友关系的耦合,将 printEmployee() 方法封装到类 CollegeManager 中,而将类 CollegeManager 作为类 SchoolManager 的直接朋友。
public class Demeter1 { public static void main(String[] args) { System.out.println("使用迪米特法则的改进"); //创建了一个SchoolManager对象 SchoolManager schoolManager = new SchoolManager(); //输出学院员工id 和 学校总部员工信息 schoolManager.printAllEmployee(new CollegeManager()); } } //学校总部员工 class Employee { private String id; public void setId(String id) { this.id = id; } public String getId() { return id; } } class CollegeEmployee { private String id; public void setId(String id) { this.id = id; } public String getId() { return id; } } class CollegeManager { //返回学院所有员工 public List<CollegeEmployee> getAllEmployee() { ArrayList<CollegeEmployee> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { CollegeEmployee emp = new CollegeEmployee(); emp.setId("学院员工id="+i); list.add(emp); } return list; } public void printEmployee() { List<CollegeEmployee> list1 = this.getAllEmployee(); System.out.println("--------------分公司员工-------------"); for (CollegeEmployee e : list1) { System.out.println(e.getId()); } } } class SchoolManager { //返回学校总部所有员工 public List<Employee> getAllEmployee() { ArrayList<Employee> list = new ArrayList<>(); for (int i = 0; i < 5; i++) { Employee emp = new Employee(); emp.setId("学校总部员工id="+i); list.add(emp); } return list; } //输出学校总部和学院员工信息 public void printAllEmployee(CollegeManager sub) { //将获取学院员工的方法封装到CollegeManager类里面 sub.printEmployee(); List<Employee> list2 = this.getAllEmployee(); System.out.println("-------------学校总部员工-------------"); for (Employee e : list2) { System.out.println(e.getId()); } } }
迪米特法则的注意事项:
- 核心思想是降低类之间的耦合。
- 对每个类减少不必要的依赖,降低类之间的耦合关系,并不是严格要求完全没有依赖关系。