1. 设计模式的目的
编写软件过程中,程序员面临着来自耦合性,内聚性,可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式可以使程序在设计以及运行中,具有更好的
- 代码重用性(相同代码不用重复编写)
- 可读性(编程规范,便于自己和其他人阅读和理解)
- 可扩展性(需要增加新的功能时,十分方便)
- 可靠性(增加新的功能后,对原来程序没有影响以及尽可能的不出现bug)
- 使程序呈现低内聚,高耦合的特性
就好比是建造高楼大厦,首先要做的就是去设计地基,设计如何搭建,只有当一份完善的设计图纸出来之后,才能开始搭建,编写代码也一样,只有在写代码之前,设计一份详细的设计和编写方案,才能写出更好的代码!
2. 设计模式的七大原则
设计模式原则,也就是设计模式的基础,即设计模式为什么要如此设计的依据
常见的设计模式原则一共有七种:
- 单一职责原则
- 接口隔离原则
- 依赖倒转原则
- 里氏替换原则
- 开闭原则
- 迪米特原则
- 合成复用原则
2.1 单一职责原则
2.1.1 基本介绍
对类来说,一个类应该只负责一项职责。例如 A 类只负责一个职责1,如果A类需要负责多个原则,这时候最好的方式就是将 A 类拆分成更加细粒度的 A1,A2
2.1.2 应用示例
我们编写一个交通工具类(Transportation),在这个类中定义交通工具运行的方法 run
public class SingleResponsibilityDemo {
public static void main(String[] args) {
Transportation transportation = new Transportation();
transportation.run("汽车");
transportation.run("飞机");
transportation.run("轮船");
}
}
class Transportation {
public void run(String name) {
System.out.println(name + "在地上跑");
}
}
主方法中,不管是汽车,飞机,轮船,它们运行都是 “在地上跑”,这是明显不符合单一职责原则的,解决方案其实很简单,我们只需要根据交通工具的种类不同,编写不同的类即可
class RoadTransportation {
public void run(String name) {
System.out.println(name + "在地上跑");
}
}
class AirTransportation {
public void run() {
System.out.println("在天上跑");
}
}
class WaterTransportation {
public void run(String name) {
System.out.println(name + "在地上跑");
}
}
这样子我们就遵守了单一职责原则,但是对类的改动很大,并且客户端也需要做较大改动
此时我们可以在原先的 Transportation 类的方法上做修改
public class SingleResponsibilityDemo {
public static void main(String[] args) {
Transportation transportation = new Transportation();
transportation.runRoad("汽车");
transportation.runAir("飞机");
transportation.runWater("轮船");
}
}
class Transportation {
public void runRoad(String name) {
System.out.println(name + "在地上跑");
}
public void runAir(String name) {
System.out.println(name + "在地上跑");
}
public void runWater(String name) {
System.out.println(name + "在地上跑");
}
}
这样子修改,不仅改动较小,并且虽然没有在类这个层面上遵守单一职责原则,但是在方法层面上通过新增方法,依然遵守
2.1.3 注意事项和细节
- 降低类的复杂度,一个类只负责一个职责
- 提高类的可读性,可维护性
- 降低变更引起的风险
- 除非类的方法很少,我们才可以在类中通过新增方法的方式遵守单一职责原则
2.2 接口隔离原则
2.2.1 基本介绍
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上
2.2.2 应用示例与代码
如下图:
当 A类通过 Interface1 去依赖(使用) B类时,B类实现了不需要实现的 operation4 和 operation5 方法,C类同理
按照上面的 UML 图,我们的代码如下:
public class InterfaceSegrationDemo {
public static void main(String[] args) {
B b = new B();
A a = new A();
C c = new C();
D d = new D();
a.method1(b);
a.method2(b);
a.method3(b);
c.method1(d);
c.method4(d);
c.method5(d);
}
}
interface Interface1 {
void operation1();
void operation2();
void operation3();
void operation4();
void operation5();
}
class B implements Interface1 {
@Override
public void operation1() {
System.out.println("B1");
}
@Override
public void operation2() {
System.out.println("B2");
}
@Override
public void operation3() {
System.out.println("B3");
}
@Override
public void operation4() {
System.out.println("B4");
}
@Override
public void operation5() {
System.out.println("B5");
}
}
class D implements Interface1 {
@Override
public void operation1() {
System.out.println("D1");
}
@Override
public void operation2() {
System.out.println("D2");
}
@Override
public void operation3() {
System.out.println("D3");
}
@Override
public void operation4() {
System.out.println("D4");
}
@Override
public void operation5() {
System.out.println("D5");
}
}
class A {
public void method1(Interface1 interface1) {
interface1.operation1();
}
public void method2(Interface1 interface1) {
interface1.operation2();
}
public void method3(Interface1 interface1) {
interface1.operation3();
}
}
class C {
public void method1(Interface1 interface1) {
interface1.operation1();
}
public void method4(Interface1 interface1) {
interface1.operation4();
}
public void method5(Interface1 interface1) {
interface1.operation5();
}
}
当我们按照接口隔离原则去改进时,需要将接口 Interface1 拆分成独立的三个接口,类 A 与 类C 分别与它们需要的接口建立依赖关系即可
改进代码如下:
public class InterfaceSegrationDemo2 {
public static void main(String[] args) {
B2 b2 = new B2();
A2 a2 = new A2();
C2 c2 = new C2();
D2 d2 = new D2();
a2.method1(b2);
a2.method2(b2);
a2.method3(b2);
c2.method1(d2);
c2.method4(d2);
c2.method5(d2);
}
}
interface Interface2 {
void operation1();
}
interface Interface3 {
void operation2();
void operation3();
}
interface Interface4 {
void operation4();
void operation5();
}
class B2 implements Interface2, Interface3 {
@Override
public void operation1() {
System.out.println("B1");
}
@Override
public void operation2() {
System.out.println("B2");
}
@Override
public void operation3() {
System.out.println("B3");
}
}
class D2 implements Interface2, Interface4 {
@Override
public void operation1() {
System.out.println("D1");
}
@Override
public void operation4() {
System.out.println("D4");
}
@Override
public void operation5() {
System.out.println("D5");
}
}
class A2 {
public void method1(Interface2 interface2) {
interface2.operation1();
}
public void method2(Interface3 interface3) {
interface3.operation2();
}
public void method3(Interface3 interface3) {
interface3.operation3();
}
}
class C2 {
public void method1(Interface2 interface2) {
interface2.operation1();
}
public void method4(Interface4 interface4) {
interface4.operation4();
}
public void method5(Interface4 interface4) {
interface4.operation5();
}
}
虽然看起来多生成了两个接口,但是逻辑上,更加独立了每个接口的作用,也不需要让类去实现不需要实现的方法
2.3 依赖倒转原则
2.3.1 基本介绍
- 高层模块不应该依赖底层模块,二者都应该依赖抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 依赖倒转原则的核心就是面向接口编程
- 相对于细节的多变性,抽象的东西要稳定的多,以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在Java 中,抽象指的就是抽象类或者接口,细节就是具体的实现类
- 使用接口或者抽象类的目的就是为了制定规范,不涉及具体的操作,从而把展现细节的任务交给它们的实现类去完成
2.3.2 应用示例
我们定义一个 Person 类,有一个方法可以接受邮件的信息,此时我们的代码如下:
public class Demo {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
}
}
class Email {
public void info() {
System.out.println("电子邮件信息");
}
}
class Person {
public void receive(Email email) {
email.info();
}
}
如果我们的 Person 类又有了接受微信或者短息的需求,那么还得重新写接受微信或者短息的方法,如果使用依赖倒转原则,我们可以先定义一个接口,专门用来展示各种邮件,微信或者短息等方式的信息,然后Person 类只需要将 receive 方法的参数改为接口即可
代码改进如下:
public class DemoImprove {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
person.receive(new WeiXin());
person.receive(new Message());
}
}
interface Receiver{
public void info();
}
class Email implements Receiver{
@Override
public void info() {
System.out.println("电子邮件信息");
}
}
class WeiXin implements Receiver{
@Override
public void info() {
System.out.println("微信信息");
}
}
class Message implements Receiver{
@Override
public void info() {
System.out.println("短信信息");
}
}
class Person {
public void receive(Receiver receiver) {
receiver.info();
}
}
2.3.3 依赖传递的三种方式和示例
上面的Person类,我们使用 receive 方法来接收 Reciever 接口类型的参数,这是通过接口传递的方式
第二种方式我们还可以通过构造器的方式传递,例如:
public class DemoImprove {
public static void main(String[] args) {
Person person = new Person(new Email());
person.receive();
}
}
class Person {
private Receiver receiver;
public Person(Receiver receiver) {
this.receiver = receiver;
}
public void receive() {
this.receiver.info();
}
}
第三种则是通过setter 方法来进行传递
public class DemoImprove {
public static void main(String[] args) {
Person person = new Person();
person.setReceiver(new Email());
person.receive();
}
}
class Person {
private Receiver receiver;
public void setReceiver(Receiver receiver) {
this.receiver = receiver;
}
public void receive() {
this.receiver.info();
}
}
2.3.4 注意事项与细节
- 底层模块尽量都要有抽象类或者接口,或者两者都有,稳定性会大大提高
- 变量的声明尽量是接口或者抽象类,这样我们的变量引用和实际对象之间,就存在一个缓冲层,利于程序的拓展和优化
- 继承时需要遵循里氏替换原则
2.4 里氏替换原则
2.4.1 基本介绍
面向对象的继承带来的问题
- 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵守这些规范,但是如果子类对这些已经实现的方法进行任意的修改,就会对整个继承体系造成破坏。
- 继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象之间的耦合性,如果一个类被其他的子类所继承,当这个类需要被修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的方法都可能出现问题
- 编程中,如何正确的使用继承----> 里氏替换原则
里氏替换原则:
- 如果对每个类型为 T1 的对象o1,都有类型为T2的对象o2,使得以T1定义的所有的程序p在所有的对象o1都替换成o2时,程序p的行为没有发生变化,那么类型T2就是类型T1的子类型,换句话说,所有引用父类的地方必须能透明的使用其子类的对象
- 使用继承时,尽量不要重写父类已经实现的方法
- 继承实际上使得两个类的耦合增强了,在适当的情况下,可以考虑使用聚合,组合,依赖来解决问题
2.4.2 示例代码
代码如下
public class Demo {
public static void main(String[] args) {
A a = new A();
System.out.println(a.function1(1,2));
B b = new B();
System.out.println(b.function1(1,2));
}
}
class A {
public int function1(int a,int b){
return a+b;
}
}
class B extends A{
@Override
public int function1(int a, int b) {
return a-b;
}
}
在B中重写了父类A 的function1 方法,本来A的function1 方法是用来计算两个数的和,但是被B改成了计算两个数之间的差,造成原来的功能出错,实际编程中,我们通常会通过重写父类的方法完成新的功能,虽然这样写起来代码简单,但是整个集成体系的复用性会比较差。
通用的做法是:父类A 和 子类B 共同继承一个更加抽象的父类,原有的 A 类和 B类的继承关系去掉,改成聚合,依赖或者组合等关系
改进代码如下:
public class Demo {
public static void main(String[] args) {
A a = new A();
System.out.println(a.function1(1, 2));
B b = new B();
System.out.println(b.function1(1, 2));
}
}
abstract class Base {
public abstract int function1(int a, int b);
}
class A extends Base {
@Override
public int function1(int a, int b) {
return a + b;
}
}
class B extends Base {
private A a = new A();
@Override
public int function1(int a, int b) {
return a - b;
}
public int function2(int a, int b) {
return this.a.function1(a, b) + this.function1(a, b);
}
}
通过在更高层次的父类 Base 中我们定义一个function1,就可以使得 A 类和B类的function1 方法各不干扰,并且如果想要使用A类的function1,我们可以通过在B类中定义A类的对象来调用,整个继承体系就清晰明了许多,子类之间的方法相互独立
2.5 开闭原则
2.5.1 基本介绍
- 开闭原则是编程中最基础的,最重要的设计原则
- 一个软件实体如类,模块和函数应该对提供方扩展开放,对使用方修改关闭,用抽象构建框架,用实现扩展细节
- 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化
- 编程中遵循其他原则,以及使用设计模式的目的就是为了实现开闭原则
2.5.2 应用示例
我们设计一个画图形的类,通过传递的参数的不同,来实现不同的图形的画图功能,代码如下:
public class Demo {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.draw(new Rectangle());
graphicEditor.draw(new Circle());
}
}
// 使用方
class GraphicEditor {
public void draw(Shape shape) {
if (shape.type_int == 1) {
System.out.println("绘制矩形");
} else if (shape.type_int == 2) {
System.out.println("绘制圆形");
}
}
}
// 提供方的父类
class Shape {
int type_int;
}
// 提供方
class Rectangle extends Shape {
public Rectangle() {
super.type_int = 1;
}
}
// 提供方
class Circle extends Shape {
public Circle() {
super.type_int = 2;
}
}
以上代码咋一看似乎没有问题,但是一旦我们新增一个图形类的时候,不仅需要新增一个提供方,还需要修改使用方的代码,在使用方的代码里面进行图形的判断,例如我们新增一个三角形类
// 新的提供方
class Triangle extends Shape {
public Triangle() {
super.type_int = 3;
}
}
// 使用方
class GraphicEditor {
public void draw(Shape shape) {
if (shape.type_int == 1) {
System.out.println("绘制矩形");
} else if (shape.type_int == 2) {
System.out.println("绘制圆形");
} else if (shape.type_int == 3) {
System.out.println("绘制三角形");
}
}
}
这种方式编写的代码,虽然好处是简单易懂,但是违反了设计模式的开闭原则,即对提供方的扩展开放,对使用方的修改关闭
改进思路:把画图的功能集成到 Shape 父类中,并且让它的子类去实现这个方法,使用方只管调用即可
改进代码如下:
public class Demo {
public static void main(String[] args) {
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.draw(new Rectangle());
graphicEditor.draw(new Circle());
graphicEditor.draw(new Triangle());
}
}
class GraphicEditor {
public void draw(Shape shape) {
shape.draw();
}
}
abstract class Shape {
int type_int;
public abstract void draw();
}
class Rectangle extends Shape {
public Rectangle() {
super.type_int = 1;
}
@Override
public void draw() {
System.out.println("绘制矩形");
}
}
class Circle extends Shape {
public Circle() {
super.type_int = 2;
}
@Override
public void draw() {
System.out.println("绘制圆形");
}
}
class Triangle extends Shape {
public Triangle() {
super.type_int = 3;
}
@Override
public void draw() {
System.out.println("绘制三角形");
}
}
对于使用方来说,并不需要关心传递的到底是什么图形,只需要调用图形参数的 draw 方法即可,并且当我们新增一个图形类的时候,只需要让这个类去继承父类并实现对应的draw 方法即可,使用方不用修改代码
2.6 迪米特法则
2.6.1 基本介绍
- 一个对象应该对其他对象保持最少的了解
- 类与类关系越紧密,耦合度越大
- 迪米特法则又叫做最小知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供public 方法,不对外泄露任何信息
- 迪米特法则还有个更简单的解释:只与直接朋友通信
- 直接朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象是朋友关系。耦合的方式有很多,例如依赖,关联,组合,聚合等。其中我们称以成员变量,方法参数,方法返回值表示的类为直接朋友,而以局部变量表示的类不是直接朋友。也就是说,陌生的类最好不要以局部变量的方式出现在类的内部
2.6.2 应用示例
有一个学校,下属有总部和各个学院,要求总部类打印出学院总部员工的信息和学院员工的信息
我们首先定义总部员工和学院员工
// 总部员工
class HeadEmployee{
private int id;
@Override
public String toString() {
return "HeadEmployee{" +
"id=" + id +
'}';
}
}
// 学院员工
class CollegeEmployee{
private int id;
@Override
public String toString() {
return "CollegeEmployee{" +
"id=" + id +
'}';
}
}
然后定义总部类和学院类
class CollegeManager {
private List<CollegeEmployee> collegeEmployeeList = new ArrayList<>();
// 初始化学院员工信息
public CollegeManager() {
for (int i = 0; i < 5; i++) {
collegeEmployeeList.add(new CollegeEmployee(i));
}
}
public List<CollegeEmployee> getCollegeEmployeeList() {
return collegeEmployeeList;
}
}
class HeadQuarterManager {
private List<HeadEmployee> headEmployeeList = new ArrayList<>();
// 初始化总部员工信息
public HeadQuarterManager() {
for (int i = 0; i < 3; i++) {
headEmployeeList.add(new HeadEmployee(i));
}
}
}
接下来完成总部类打印总部员工信息和学院员工信息的方法
public void printAllEmployee(CollegeManager collegeManager) {
List<CollegeEmployee> collegeEmployeeList = collegeManager.getCollegeEmployeeList();
collegeEmployeeList.forEach(System.out::println);
headEmployeeList.forEach(System.out::println);
}
这样子设计的问题在于,在总部类的 printAllEmployee 方法内部,出现了 List<CollegeEmployee> collegeEmployeeList 这个局部变量,其中的 CollegeEmployee 类以局部变量的方式出现在了总部类中,这就是非直接朋友关系的耦合,为了满足迪米特法则,我们需要在 printAllEmployee 方法中只调用collegeManager 这个直接朋友完成学院员工信息的打印,所以将打印学院员工的信息方法放入到collegeManager 类中
// collegeManager 类
public void printCollegeEmployee() {
collegeEmployeeList.forEach(System.out::println);
}
// HeadQuarterManager 类
public void printAllEmployee(CollegeManager collegeManager) {
collegeManager.printCollegeEmployee();
headEmployeeList.forEach(System.out::println);
}
这样子就避免了在 HeadQuarterManager 类中自己去完成学院员工信息的打印,而是交给了 CollegeManager 这个类的对象去完成。从逻辑上也更加合理清晰
2.7 合成复用原则
2.7.1 基本介绍
类与类的关系尽量使用聚合/合成的方式,而不是继承
简单来说,当我们使用继承关系的时候,是在定义类 is a 的关系(子类是一个父类的实现),而如果我们使用聚合等方式,是在定义类 has a的关系(类具有一个其他类的对象),如果不是逻辑上十分紧密的“父子”关系,我们都尽量不要使用继承,因为继承会带来强耦合,而聚合等方式带来的是松耦合的关系
2.8 设计原则核心思想
- 找出应用中可能变化之处,把它们独立出来,不要和那些不需要变化的代码混合在一起
- 针对接口编程,而不是针对实现编程
- 为了交互对象之间的松耦合设计而努力