设计模式01-七大设计原则
文章目录
开闭原则-Open Close
一个软件实体如类、模块、函数应该对扩展开放,对修改关闭。用抽象构建框架,用实现扩展细节。提高程序的复用性和可维护性。
案例:一个课程类,具有:课程类型、课程名称、售价3个属性,代码实现:
课程接口:
public interface ICourse {
// 获取类型
String getCategory();
// 获取名称
String getName();
// 获取价格
Double getPrice();
}
Java课程实现类:
public class JavaCourse implements ICourse {
private String name;
private Double price;
private String category;
public JavaCourse(String name, Double price, String category) {
this.name = name;
this.price = price;
this.category = category;
}
@Override
public String getCategory() {
return category;
}
@Override
public String getName() {
return name;
}
@Override
public Double getPrice() {
return price;
}
}
测试类:
public static void main(String[] args) {
JavaCourse course = new JavaCourse("架构师", 998.00, "Java");
System.out.println("课程:" + course.getName()
+ " 分类:" + course.getCategory()
+ " 价格:" + course.getPrice());
}
问题:此时要根据情况修改价格,如双11打8折,春节打五折,那我们直接在getPrice()方法中修改显然违背了开闭原则中对修改关闭的定义,比如下面这样:
@Override
public Double getPrice() {
return price * 0.8;// 双11打8折
}
这样会带来的问题:
- 频繁修改已有代码逻辑,如春节、双11、双12各有不同的折扣力度,每种会员可能又有不同的折扣计算方式,这时候都要来修改我们已有的程序,降低了程序的可维护性
- 违背了开闭原则
这时候该怎么办呢?当然是通过对扩展开放的定义来扩展我们的程序:
比如我们可以定义一个JavaCourse的折扣类来实现这个逻辑:
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(String name, Double price, String category) {
super(name, price, category);
}
public Double getDiscountPrice() { // 获取折扣价格
return super.getPrice() * 0.8;
}
}
然后修改我们的主程序来进行测试:
public static void main(String[] args) {
JavaCourse course = new JavaDiscountCourse("架构师", 998.00, "Java");
System.out.println("课程:" + course.getName() +
" 分类:" + course.getCategory() +
" 折扣价格:" + course.getPrice() +
"原价:" + ((JavaDiscountCourse) course).getDiscountPrice()
);
}
这样即符合了开闭原则的理念,又提高了我们程序的复用性和可维护性。
依赖倒置原则-Dependence Inversion
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象;针对接口编程,不针对实现编程
优点:可以减少类之间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险
高层低层的概念: 离调用者越近,层次越高。离被调用者越近,层次越低。如我去调用别人的接口的某个方法,那我的代码就是高层模块,被调用的就是底层。
高层依赖低层模块可能造成的问题:低层方法参数修饰符、返回值类型等发生了变化,那么高层调用者也要做出相应的变化。
案例:Tom类需要学习Java、Python两门课程
Tom类(被调用方:低层):
public class Tom {
public void studyJava() {
System.out.println("学习Java课程");
}
public void studyPython() {
System.out.println("学习Python课程");
}
}
测试类(也就是我们的高层调用):
public class DIDemo {
public static void main(String[] args) {
Tom tom = new Tom();
tom.studyJava();
tom.studyPython();
}
}
这时,程序看起来没什么问题。但是如果将Tom中的studyJava()方法改成javaStudy() 那么高层的调用就也需要修改了。这样的话就提高了修改造成的风险,而且程序的耦合性强。
接下来做一下修改:高层和低层都依赖抽象:
首先建立抽象(课程的抽象接口):
public interface ICourse {
void study();
}
然后创建Java和Python的学习类,并实现课程接口:
public class JavaStudy implements ICourse {
@Override
public void study() {
System.out.println("学习Java");
}
}
public class PythonStudy implements ICourse {
@Override
public void study() {
System.out.println("学习Python");
}
}
这时Tom类(低层)依赖抽象:
public class Tom {
// 依赖了抽象ICourse
public void study(ICourse iCourse) {
iCourse.study();
}
}
这时我们的测试类(高层)就可以修改为依赖抽象而不是细节:
public static void main(String[] args) {
Tom tom = new Tom();
tom.study(new JavaStudy());
tom.study(new PythonStudy());
}
这样就降低了我们程序的耦合度,提高了程序稳定性和可读性。降低了修改程序带来的风险。
版本3:当然我们也可以通过构造方法来优化Tom类:
public class Tom {
private ICourse iCourse;
public Tom(ICourse iCourse) {
this.iCourse = iCourse;
}
public void study() {
iCourse.study();
}
}
这样的话只需要在测试类中传入ICourse的实现类来构造Tom即可,同样也可以使用Set等方式进行注入。
一句话总结:面向接口编程
单一职责原则-Simple ResponsiBility
定义:不要存在多于一个导致类变更的原因。说白了:一个类、接口、方法只负责一项职责。
优点:降低代码复杂度,提高程序可读性,提高系统可维护性,降低变更引起的风险
案例:观看两种课程,直播课不能快进,录播课可以快进
课程类(针对不同课程有不同处理逻辑):
public class Course {
public void study(String courseName) {
if ("直播课".equals(courseName)) {
System.out.println("直播课不能快进");
} else {
System.out.println("录播课可以快进");
}
}
}
测试类:
public class StudyDemo {
public static void main(String[] args) {
Course course = new Course();
course.study("直播课");
course.study("录播课");
}
}
这时候如果我们要对不同类型课程做不同的处理,比如编码解码处理,可能就需要针对Course#study方法进行修改,势必会增加代码复杂度,降低可读性。
接下来我们这样修改:分别在不同的类里面处理不同的课程:
直播课类
public class LiveCourse {
public void study(String courseName) {
System.out.println(courseName + "只能在线观看");
}
}
录播课类
public class ReplayCourse {
public void study(String courseName) {
System.out.println(courseName + "可以反复观看");
}
}
这样我们就可以直接调用不同的类进行处理。降低了复杂度,提高了可读性。
但是后面可能针对课程有很多新的职责:比如获取视频流、退款、学习课程、获取课程基本信息,这时候该怎么做呢?
接口级别:
如用户接口,可以看视频和退款:
public interface ICourseManager {
void readVideo();// 看视频
void refundCourse();// 退款
}
课程信息接口,可以用来获取课程信息:
public interface ICourseInfo {
String getCourseName();// 获取课程信息
}
这样的话,新增课程信息不会对其他职责造成影响,就满足了单一职责的定义
public class CourseImpl implements ICourseInfo, ICourseManager {
@Override
public String getCourseName() {
return null;
}
@Override
public void readVideo() {
}
@Override
public void refundCourse() {
}
}
方法级别:
以修改用户信息为例
public class LoginMethod {
private void updateUserName() {
// 修改用户名
}
private void updateUserPhone() {
// 修改用户手机号
}
private void updateUserAddress() {
// 修改用户地址
}
}
如上,较小颗粒度的拆分方法职责,使其看起来明了,并且职责清晰,不会相互影响。
接口隔离原则-Interface Segregation
定义:用多个专门的接口,而不是使用单一的总接口,客户端不应该依赖它不需要的接口
注意:一个类对应一个类的依赖应该建立在最小的接口上;建立单一接口,不要建立庞大臃肿的接口;尽量细化接口,接口中的方法尽量少;注意适度原则,一定要适度
优点:高内聚,低耦合,从而提高可读性,可扩展性和可维护性
比如我们有一个动物接口,内部提供了一些方法:
public interface IAnimal {
// 跑
void run();
// 飞
void fly();
// 游泳
void swim();
// 吃东西
void eat();
}
这时候假如我们有一个鸟类,实现该接口的话就需要实现一些不需要实现的方法,如游泳。
这时候我们可以针对:吃、跑、飞、游泳分别建立接口并提供方法,这时候鸟类只需要实现吃和飞的接口就可以了。
迪米特法则-Law of Demeter
定义:一个对象应该对其他对象保持最少的了解,又叫最少知道原则
强调只和朋友交流,不和陌生人说话
朋友的概念:出现在成员变量、方法输入、输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类
作用:一定程度上解耦
案例:团队领导让员工查询现有的课程数量
课程类:
public class Course {
private String name;
public String getName() {
return name;
}
public Course setName(String name) {
this.name = name;
return this;
}
}
员工类(用来查询课程数量):
public class Employee {
public int getCourseNumber(List<Course> courses) {
return courses.size();
}
}
团队领导类:
public class TeamLeader {
public int getNumbersByEmployee(Employee employee) {
List<Course> courses = new ArrayList<>();
for (int i = 0; i < 20; i++) {
courses.add(new Course());
}
return employee.getCourseNumber(courses);
}
}
接下来是测试方法:
public static void main(String[] args) {
Employee employee = new Employee();
TeamLeader teamLeader = new TeamLeader();
System.out.println(teamLeader.getNumbersByEmployee(employee));
}
这里就发现问题了:TeamLeader引用的Course类并不满足迪米特法则,即Course在TeamLeader中并不是其“朋友”。
里氏替换原则-Liskov Substitution
定义:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2就是类型T1的子类型。
定义扩展:一个软件实体如果适用一个父类的话,那一定适用其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能替换父类对象,而程序逻辑不变
引申意义:子类可以扩展父类的功能,但不能改变父类原有的功能,比如前面开闭原则的案例,打折课程类新写了一个打折方法:
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(String name, Double price, String category) {
super(name, price, category);
}
public Double getDiscountPrice() { // 获取折扣价格
return super.getPrice() * 0.8;
}
}
含义1:子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法
含义2:子类中可以增加自己特有的方法
含义3:子类方法重载父类的方法时,方法的前置条件(即方法的输入、入参)要比父类的输入参数更宽松
含义4:当子类的方法实现父类的方法时(重写、重载或实现抽象方法),方法的后置条件(即方法的输出、返回值)要比父类更严格或相等。
优点1:约束继承泛滥,开闭原则的一种体现
优点2:加强程序的健壮性,同时变更时也可以做到非常好的兼容性提高程序的维护性,扩展性。降低需求变更时引入的风险
例子:以正方形长方形为例,在测试类中获取长方形的宽高,如果宽大于高就修改参数,直至宽小于等于高,代码如下:
长方形类:
public class Rectangle {
private Integer width;// 宽
private Integer height;// 高
public Integer getWidth() {
return width;
}
public Rectangle setWidth(Integer width) {
this.width = width;
return this;
}
public Integer getHeight() {
return height;
}
public Rectangle setHeight(Integer height) {
this.height = height;
return this;
}
}
正方形类:
public class Square extends Rectangle {
private Integer length;
public Integer getLength() {
return length;
}
public Square setLength(Integer length) {
this.length = length;
return this;
}
@Override
public Integer getWidth() {
return this.length;
}
@Override
public Rectangle setWidth(Integer width) {
return setLength(width);
}
@Override
public Integer getHeight() {
return this.length;
}
@Override
public Rectangle setHeight(Integer height) {
return setLength(height);
}
}
可以看到为了满足正方形的边长相等的属性,我们修改了其父类的width height的get和set方法。
接下来是测试类,定义一个resize()方法,如果长方形的宽大于高,就在while中将高度+1:
public class TestDemo {
private static void resize(Rectangle rectangle) {
while (rectangle.getWidth() >= rectangle.getHeight()) {
rectangle.setHeight(rectangle.getHeight() + 1);
System.out.println("当前宽度:" + rectangle.getHeight());
}
}
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setWidth(20);
rectangle.setHeight(10);
resize(rectangle);
}
}
这里我们用父类进行测试,控制台打印如下:
当前宽度:11
当前宽度:12
当前宽度:13
当前宽度:14
当前宽度:15
当前宽度:16
当前宽度:17
当前宽度:18
当前宽度:19
当前宽度:20
当前宽度:21
Process finished with exit code 0
接下来用子类Square进行测试:
当前宽度:407744
当前宽度:407745
当前宽度:407746
当前宽度:407747
当前宽度:407748
当前宽度:407749
...无限循环
可以看到这个resize()由于传的是子类,从而破坏了该方法的正常逻辑,也不满足里氏替换原则。
再回顾一下定义:使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化
那么上面的问题如何解决呢?
定义一个四边形接口:
public interface Quadrangle {
Integer getHeight();// 获取高度
Integer getWidth();// 获取宽度
}
定义正方形类:
public class Square implements Quadrangle {
private Integer length;
public Square setLength(Integer length) {
this.length = length;
return this;
}
@Override
public Integer getHeight() {
return this.length;
}
@Override
public Integer getWidth() {
return this.length;
}
}
定义长方形类:
public class Rectangle implements Quadrangle {
private Integer width;// 宽
private Integer height;// 高
public Rectangle setWidth(Integer width) {
this.width = width;
return this;
}
public Rectangle setHeight(Integer height) {
this.height = height;
return this;
}
@Override
public Integer getHeight() {
return this.height;
}
@Override
public Integer getWidth() {
return this.width;
}
}
测试程序:
public class TestDemo {
private static void resize(Quadrangle quadrangle) {
while (quadrangle.getWidth() >= quadrangle.getHeight()) {
// 由于四边形没有提供set方法所以这里会报错
quadrangle.setHeight(quadrangle.getHeight() + 1);
System.out.println("当前宽度:" + quadrangle.getHeight());
}
}
public static void main(String[] args) {
Square square = new Square();
square.setLength(10);// 边长为10的正方形
resize(square);
}
}
这里由于四边形类并没提供setHeight()方法,所以这里的第五行代码会报错,从一定程度上避免了继承泛滥。
合成(组合)、复用原则-Composite&Aggregate Reuse
定义:尽量使用对象组合、聚合,而不是继承关系达到软件复用的目的
聚合:has - a , 比如电脑和U盘,可以在一起工作,电脑也可以单独工作
组合:contains - a,比如人体的各个部位,组合在一起才能有完整的生命周期
继承:is - a
优点:可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少
rangle.getWidth() >= quadrangle.getHeight()) {
// 由于四边形没有提供set方法所以这里会报错
quadrangle.setHeight(quadrangle.getHeight() + 1);
System.out.println(“当前宽度:” + quadrangle.getHeight());
}
}
public static void main(String[] args) {
Square square = new Square();
square.setLength(10);// 边长为10的正方形
resize(square);
}
}
这里由于四边形类并没提供setHeight()方法,所以这里的第五行代码会报错,从一定程度上避免了继承泛滥。
## 合成(组合)、复用原则-Composite&Aggregate Reuse
定义:**尽量使用对象组合、聚合,而不是继承关系达到软件复用的目的**
聚合:**has - a** , 比如电脑和U盘,可以在一起工作,电脑也可以单独工作
组合:**contains - a**,比如人体的各个部位,组合在一起才能有完整的生命周期
继承:**is - a**
优点:**可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少**
**一句话总结:能不用继承就不用继承**
最后总结:设计模式是对我们开发中做的一些规范和约束,在实际的开发中并非要追求完美,而是在时间、成本等各方面允许的情况下尽量遵守规范。