声明:
本博客是本人在学习《Java 设计模式精讲》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。
本博客已标明出处,如有侵权请告知,马上删除。
1. 七大原则
- 开闭原则
- 依赖倒置原则
- 单一职责原则
- 接口隔离原则
- 迪米特法则(最少知道原则)
- 里氏替换原则
- 合成/复用原则(组合/复用原则)
2. 开闭原则
- 定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
- 用抽象构建框架,用实现扩展细节
- 优点:提高软件系统的可复用性及可维护性
开闭原则 coding
-
创建一个课程的接口
public interface ICourse { Integer getId(); String getName(); Double getPrice(); }
-
创建 Java 课程的类并且实现课程接口
public class JavaCourse implements ICourse { private Integer id; private String name; private Double price; public JavaCourse(Integer id, String name, Double price) { this.id = id; this.name = name; this.price = price; } @Override public Integer getId() { return this.id; } @Override public String getName() { return this.name; } @Override public Double getPrice() { return this.price; } }
-
创建测试类
public class Test { public static void main(String[] args) { ICourse javaCourse = new JavaCourse(96, "Java 从零开始到企业级开发", 348d); System.out.println("课程Id:" + javaCourse.getId() + " 课程名称:" + javaCourse.getName() + " 课程价格:" + javaCourse.getPrice() + "元"); } }
运行结果:
课程Id:96 课程名称:Java 从零开始到企业级开发 课程价格:348.0元
现在类图如下所示:
假如现在 Java 课程出了一个打折活动,我们需要新增一个打折方法该如何去做呢?
下面是一种不遵守开闭原则的这做法:
-
在课程的接口中新增一个打折方法
public interface ICourse { Integer getId(); String getName(); Double getPrice(); Double getDiscountPrice(); }
-
在 Java 课程的类中实现这个方法
public class JavaCourse implements ICourse { private Integer id; private String name; private Double price; public JavaCourse(Integer id, String name, Double price) { this.id = id; this.name = name; this.price = price; } @Override public Integer getId() { return this.id; } @Override public String getName() { return this.name; } @Override public Double getPrice() { return this.price; } @Override public Double getDiscountPrice() { return this.price * 0.8; } }
-
测试类
public class Test { public static void main(String[] args) { ICourse javaCourse = new JavaCourse(96, "Java 从零开始到企业级开发", 348d); System.out.println("课程Id:" + javaCourse.getId() + " 课程名称:" + javaCourse.getName() + " 课程价格:" + javaCourse.getPrice() + "元" + " 打折后价格:" + javaCourse.getDiscountPrice() + "元"); } }
运行结果:
课程Id:96 课程名称:Java 从零开始到企业级开发 课程价格:348.0元 打折后价格:278.40000000000003元
分析:假如课程很多,那么所有的课程的实现类都要重写一下方法,接口应该是稳定的,不应该是经常修改的。
下面是一种遵守开闭原则的这做法:
-
拓展一个 Java 课程打折类并且继承于 Java 课程类,在这个类中重写 getPrice() 方法
public class JavaDiscountCourse extends JavaCourse { public JavaDiscountCourse(Integer id, String name, Double price) { super(id, name, price); } @Override public Double getPrice() { return super.getPrice() * 0.8; } }
-
测试类
public class Test { public static void main(String[] args) { ICourse javaCourse = new JavaDiscountCourse(96, "Java 从零开始到企业级开发", 348d); System.out.println("课程Id:" + javaCourse.getId() + " 课程名称:" + javaCourse.getName() + " 打折后价格:" + javaCourse.getPrice() + "元"); } }
运行结果:
课程Id:96 课程名称:Java 从零开始到企业级开发 打折后价格:278.40000000000003元
分析:我们通过继承了 Java 课程类这个父类,使对于扩展是开放的,而对于修改这个父类和接口是关闭的,提高软件系统的可复用性及可维护性。
如果我们还想要原价可以采用以下做法:
-
在拓展的打折类中,新增 getOriginPrice() 方法
public class JavaDiscountCourse extends JavaCourse { public JavaDiscountCourse(Integer id, String name, Double price) { super(id, name, price); } public Double getOriginPrice() { return super.getPrice(); } @Override public Double getPrice() { return super.getPrice() * 0.8; } }
-
测试类
public class Test { public static void main(String[] args) { ICourse ICourse = new JavaDiscountCourse(96, "Java 从零开始到企业级开发", 348d); // 如果调用实现类里面的方法,我们就必须要进行强转一下 JavaDiscountCourse javaCourse = (JavaDiscountCourse) ICourse; System.out.println("课程Id:" + javaCourse.getId() + " 课程名称:" + javaCourse.getName() + " 课程价格:" + javaCourse.getOriginPrice() + "元" + " 打折后价格:" + javaCourse.getPrice() + "元"); } }
运行结果:
课程Id:96 课程名称:Java 从零开始到企业级开发 课程价格:348.0元 打折后价格:278.40000000000003元
最终类图如下所示:
3. 依赖倒置原则
- 定义∶高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节;细节应该依赖抽象
- 针对接口编程,不要针对实现编程
- 优点∶可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险
依赖倒置原则 coding
-
创建一个 Gelly 的类,里面有两个方法,一个学习 Java 课程的方法,一个是学习 FE 课程的方法:
public class Gelly { public void studyJavaCourse() { System.out.println("Gelly在学习Java课程"); } public void studyFECourse() { System.out.println("Gelly在学习FE课程"); } }
-
测试类
public class Test { public static void main(String[]args){ Gelly Gelly = new Gelly(); Gelly.studyJavaCourse(); Gelly.studyFECourse(); } }
-
如果这个时候,我还想要添加一个学习 Python 课程的方法,我需要在基类里面进行添加方法,再在测试类中进行添加方法
public class Gelly { public void studyJavaCourse() { System.out.println("Gelly在学习Java课程"); } public void studyFECourse() { System.out.println("Gelly在学习FE课程"); } public void studyPythonCourse() { System.out.println("Gelly在学习Python课程"); } }
public class Test { public static void main(String[]args){ Gelly Gelly = new Gelly(); Gelly.studyJavaCourse(); Gelly.studyFECourse(); Gelly.studyPythonCourse(); } }
运行结果:
Gelly在学习Java课程 Gelly在学习FE课程 Gelly在学习Python课程
分析:以上我们的做法就是在针对实现来进行编程,测试类作为高层模块依赖于 Gelly 这个低层模块,这是不遵循依赖倒置原则的做法。
下面我们针对接口进行编程:
-
创建一个课程接口,里面有一个 studyCourse() 方法
public interface ICourse { public void studyCourse(); }
-
创建两个类 JavaCourse、FECourse 实现 ICourse 接口
public class JavaCourse implements ICourse { @Override public void studyCourse() { System.out.println("Gelly在学习Java课程"); } }
public class FECourse implements ICourse{ @Override public void studyCourse() { System.out.println("Gelly在学习FE课程"); } }
-
创建一个 Gelly 的类,里面有一个学习课程方法,传入了学习课程的接口,由具体的实现类来进行实现
public class Gelly { public void studyCourse(ICourse iCourse) { iCourse.studyCourse(); } }
-
测试类
public class Test { public static void main(String[]args){ Gelly Gelly = new Gelly(); Gelly.studyCourse(new JavaCourse()); Gelly.studyCourse(new FECourse()); } }
-
此时,如果要学习 Python 的课程的话,只需要新增 PythonCourse 实现 ICourse 接口,在测试中调用就可以了。
public class PythonCourse implements ICourse { @Override public void studyCourse() { System.out.println("Gelly在学习Python课程"); } }
public class Test { public static void main(String[]args){ Gelly Gelly = new Gelly(); Gelly.studyCourse(new JavaCourse()); Gelly.studyCourse(new FECourse()); Gelly.studyCourse(new PythonCourse()); } }
运行结果:
Gelly在学习Java课程 Gelly在学习FE课程 Gelly在学习Python课程
分析:测试类作为高层模块不依赖于 Gelly 这个低层模块,Gelly 作为高层模块不依赖于具体的课程实现类,Gelly 只依赖于 ICourse 接口。这样就遵循了依赖倒置原则,降低了类之间的耦合性,提高了可拓展性。
最终类图如下所示:
4. 单一职责原则
- 定义∶不要存在多于一个导致类变更的原因
- 一个类/接口/方法只负责一项职责
- 优点︰降低类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险
单一职责原则 coding
下面将类拆分为单一职责:
-
创建一个 Bird 类
public class Bird { public void mainMoveMode(String birdName) { System.out.println(birdName + "用翅膀飞"); } }
-
测试类
public class Test { public static void main(String[] args) { Bird bird = new Bird(); bird.mainMoveMode("大雁"); bird.mainMoveMode("鸵鸟"); } }
运行结果:
大雁用翅膀飞 鸵鸟用翅膀飞
-
但是鸵鸟不是用翅膀飞的,我们需要在原来 Bird 类里面进行扩展。
public class Bird { public void mainMoveMode(String birdName) { if ("鸵鸟".equals(birdName)) { System.out.println(birdName + "用脚走"); } else { System.out.println(birdName + "用翅膀飞"); } } }
运行结果:
大雁用翅膀飞 鸵鸟用脚走
-
但这样 Bird 类是不遵循单一职责原则的,我们按照职责的不同来进行拆分为 FlyBird 和 WalkBird
public class FlyBird { public void mainMoveMode(String birdName) { System.out.println(birdName + "用脚走"); } }
public class WalkBird { public void mainMoveMode(String birdName) { System.out.println(birdName + "用翅膀飞"); } }
public class Test { public static void main(String[] args) { FlyBird flyBird = new FlyBird(); WalkBird walkBird = new WalkBird(); flyBird.mainMoveMode("大雁"); walkBird.mainMoveMode("鸵鸟"); } }
运行结果:
大雁用脚走 鸵鸟用翅膀飞
下面将接口拆分为单一职责:
-
创建课程接口,里面含有两个大块的功能:一个是获取课程的相关的信息,一个是对课程进行管理
public interface ICourse { /** 获取课程的相关的信息 */ String getCourseName(); byte[] getCourseVideo(); /** 对课程进行管理 */ void studyCourse(); void refundCourse(); }
-
我们对课程接口按照职责的不同来进行拆分为 ICourseContent 和 ICourseManage
public interface ICourseContent { /** * 获取课程的相关的信息 */ String getCourseName(); byte[] getCourseVideo(); }
public interface ICourseManage { /** * 对课程进行管理 */ void studyCourse(); void refundCourse(); }
-
写一个实现类,来实现上面的两个接口
public class CourseImp implements ICourseContent, ICourseManage{ @Override public String getCourseName() { return null; } @Override public byte[] getCourseVideo() { return new byte[0]; } @Override public void studyCourse() { } @Override public void refundCourse() { } }
下面将方法拆分为单一职责:
-
创建一个用户类,里面有一个更新用户信息的方法
public class User { private void updateUserInfo(String userName, String address) { userName = "Tom"; address = "beijing"; } }
-
我们对 updateUserInfo 按照职责的不同来进行拆分为 updateUserName 和 updateUserAddress
public class User { private void updateUserName(String userName) { userName = "Tom"; } private void updateUserAddress(String address) { address = "beijing"; } }
注意:在实际的开发当中,遵循单一职责原则,要看实际的情况。不能过少的运用单一职责原则,也不能过多的使用单一职责原则,如果类过多的话,会引起来类的爆炸的现象。
5. 接口隔离原则
- 定义:用多个专门的接口,而不使用单一的总接口
- 客户端不应该依赖它不需要的接口
- 一个类对一个类的依赖应该建立在最小的接口上建立单一接口,不要建立庞大臃肿的接口
- 尽量细化接口,接口中的方法尽量少
- 注意适度原则,一定要适度
- 优点:符合我们常说的高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性
接口隔离原则 coding
-
创建一个动物接口
public interface IAnimalAction { void eat(); void fly(); void swim(); }
-
创建一个狗类,实现动物接口。狗不会飞,所以 fly 方法是空的实现。
public class Dog implements IAnimalAction { @Override public void eat() { System.out.println("dog eat..."); } @Override public void fly() { } @Override public void swim() { System.out.println("dog swim..."); } }
-
为了让狗类不实现他不需要的 fly 方法,我们可以对上面的接口来进行细化,拆分成 3 个接口
public interface IEatAnimalAction { void eat(); }
public interface ISwimAnimalAction { void swim(); }
public interface IFlyAnimalAction { void fly(); }
-
修改狗类,实现 IEatAnimalAction 和 IFlyAnimalAction
public class Dog implements IEatAnimalAction, ISwimAnimalAction { @Override public void eat() { System.out.println("dog eat..."); } @Override public void swim() { System.out.println("dog swim..."); } }
注意:我们在设计接口的时候,也不能分的太细,让接口过多;接口隔离原则在使用的时候,一定要适度,用的过多,或者过少都是不好的。
6. 迪米特原则(最少知道原则)
- 定义∶一个对象应该对其他对象保持最少的了解,又叫最少知道原则
- 尽量降低类与类之间的耦合
- 优点:降低类之间的耦合
- 强调只和朋友交流,不和陌生人说话
- 朋友∶出现在成员变量、方法的输入、输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。
迪米特原则 coding
-
创建课程类
public class Course { }
-
创建项目经理类,他有个方法可以在线查询课程数量
public class TeamLeader { public void checkNumberOfCourses(List<Course> courseList) { System.out.println("在线课程的数量是:" + courseList.size()); } }
-
创建 Boss 类,他有个方法可以命令项目经理在线查询课程数量
public class Boss { public void commandCheckNumber(TeamLeader teamLeader) { List<Course> courseList = new ArrayList<>(); for (int i = 0; i < 20; i++) { courseList.add(new Course()); } teamLeader.checkNumberOfCourses(courseList); } }
-
测试类
public class Test { public static void main(String[]args){ Boss boss = new Boss(); TeamLeader teamLeader = new TeamLeader(); boss.commandCheckNumber(teamLeader); } }
运行结果:
在线课程的数量是:20
分析:对于 Boss 类中,只有出现在成员变量、方法的输入、输出参数中的类称为成员朋友类,其他的都不能称为朋友。在 commandCheckNumber 这个方法里面不应该和 Course 的这个类有任何的交互,这里就是违背了迪米特法则。
现在类图如下所示:
下面进行修改使其符合迪米特原则
-
修改 Boss 类
public class Boss { public void commandCheckNumber(TeamLeader teamLeader) { teamLeader.checkNumberOfCourses(); } }
-
修改 TeamLeader 类
public class TeamLeader { public void checkNumberOfCourses() { List<Course> courseList = new ArrayList<>(); for (int i = 0; i < 20; i++) { courseList.add(new Course()); } System.out.println("在线课程的数量是:" + courseList.size()); } }
运行结果:
在线课程的数量是:20
最终类图如下所示:
7. 里氏替换原则
-
定义:如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 Р 在所有的对象 o1 都替换成 o2 时,程序 Р 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
-
定义扩展:一个软件实体如果适用一个父类的话,那一定适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。
-
引申意义∶子类可以扩展父类的功能,但不能改变父类原有的功能。
-
含义1∶子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
-
含义2∶子类中可以增加自己特有的方法。
-
含义3:当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
-
含义4∶当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等。
里氏替换原则 coding
-
在之前讲开闭原则的时候,在获取打折价格,我们是这样来写的
public class JavaDiscountCourse extends JavaCourse { public JavaDiscountCourse(Integer id, String name, Double price) { super(id, name, price); } public Double getOriginPrice() { return super.getPrice(); } @Override public Double getPrice() { return super.getPrice() * 0.8; } }
-
这里的 getPrice() 方法已经是重写了父类里面的非抽象方法,我们可以这样来做
public class JavaDiscountCourse extends JavaCourse { public JavaDiscountCourse(Integer id, String name, Double price) { super(id, name, price); } public Double getDiscountPrice() { return super.getPrice() * 0.8; } }
-
测试类
public class Test { public static void main(String[] args) { ICourse ICourse = new JavaDiscountCourse(96, "Java 从零开始到企业级开发", 348d); // 如果调用实现类里面的方法,我们就必须要进行强转一下 JavaDiscountCourse javaCourse = (JavaDiscountCourse) ICourse; System.out.println("课程Id:" + javaCourse.getId() + " 课程名称:" + javaCourse.getName() + " 课程价格:" + javaCourse.getPrice() + "元" + " 打折后价格:" + javaCourse.getDiscountPrice() + "元"); } }
运行结果:
课程Id:96 课程名称:Java 从零开始到企业级开发 课程价格:348.0元 打折后价格:278.40000000000003元
里氏替换原则:
- 优点1∶约束继承泛滥,开闭原则的一种体现。
- 优点2∶加强程序的健壮性,同时变更时也可以做到非常好的兼容性提高程序的维护性、扩展性。降低需求变更时引入的风险。
8. 合成复用原则
- 定义∶尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的
- 聚合 has-A 和组合 contains-A
- 优点:可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少
- 何时使用合成/聚合、继承
- 聚合 has-A、组合 contains-A、继承 is-A
合成复用原则 coding
-
创建连接数据库的类
public class DBConnection { public String getConnection() { return "MySQL的数据库连接"; } }
-
Dao 层
public class ProductDao extends DBConnection { public void addProduct() { String conn = super.getConnection(); System.out.println("使用"+conn+"增加产品"); } }
-
测试类
public class Test { public static void main(String[]args){ ProductDao productDao = new ProductDao(); productDao.addProduct(); } }
运行结果:
使用MySQL的数据库连接增加产品
现在需要再用 Oracle 的数据库来进行连接:
-
我们对之前的连接的类进行修改,改成了抽象类
public abstract class DBConnection { public abstract String getConnection(); }
-
具体获取什么连接交给实现它的子类
public class MySQLConnection extends DBConnection { @Override public String getConnection() { return "MySQL数据库连接"; } }
public class OracleConnection extends DBConnection{ @Override public String getConnection() { return "Oracle数据库连接"; } }
-
Dao 层
public class ProductDao { private DBConnection dbConnection; public void setDbConnection(DBConnection dbConnection) { this.dbConnection = dbConnection; } public void addProduct() { String conn = dbConnection.getConnection(); System.out.println("使用"+conn+"增加产品"); } }
-
测试类
public class Test { public static void main(String[]args){ ProductDao productDao = new ProductDao(); productDao.setDbConnection(new OracleConnection()); productDao.addProduct(); } }
运行结果:
使用Oracle数据库连接增加产品