一个软件实体如类、模块、函数应该对扩展开放,对修改关闭。用抽象构建框架,用实现扩展细节。提高程序的复用性和可维护性。
案例:一个课程类,具有:课程类型、课程名称、售价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折 }
这样会带来的问题:
这时候该怎么办呢?当然是通过对扩展开放的定义来扩展我们的程序:
比如我们可以定义一个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() ); }
这样即符合了开闭原则的理念,又提高了我们程序的复用性和可维护性。
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象;针对接口编程,不针对实现编程
优点:可以减少类之间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险
高层低层的概念: 离调用者越近,层次越高。离被调用者越近,层次越低。如我去调用别人的接口的某个方法,那我的代码就是高层模块,被调用的就是底层。
高层依赖低层模块可能造成的问题:低层方法参数修饰符、返回值类型等发生了变化,那么高层调用者也要做出相应的变化。
案例: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等方式进行注入。
一句话总结:面向接口编程
定义:不要存在多于一个导致类变更的原因。说白了:一个类、接口、方法只负责一项职责。
优点:降低代码复杂度,提高程序可读性,提高系统可维护性,降低变更引起的风险
案例:观看两种课程,直播课不能快进,录播课可以快进
课程类(针对不同课程有不同处理逻辑):
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() { // 修改用户地址 } }
如上,较小颗粒度的拆分方法职责,使其看起来明了,并且职责清晰,不会相互影响。
定义:用多个专门的接口,而不是使用单一的总接口,客户端不应该依赖它不需要的接口
注意:一个类对应一个类的依赖应该建立在最小的接口上;建立单一接口,不要建立庞大臃肿的接口;尽量细化接口,接口中的方法尽量少;注意适度原则,一定要适度
优点:高内聚,低耦合,从而提高可读性,可扩展性和可维护性
比如我们有一个动物接口,内部提供了一些方法:
public interface IAnimal { // 跑 void run(); // 飞 void fly(); // 游泳 void swim(); // 吃东西 void eat(); }
这时候假如我们有一个鸟类,实现该接口的话就需要实现一些不需要实现的方法,如游泳。
这时候我们可以针对:吃、跑、飞、游泳分别建立接口并提供方法,这时候鸟类只需要实现吃和飞的接口就可以了。
定义:一个对象应该对其他对象保持最少的了解,又叫最少知道原则
强调只和朋友交流,不和陌生人说话
朋友的概念:出现在成员变量、方法输入、输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类
作用:一定程度上解耦
案例:团队领导让员工查询现有的课程数量
课程类:
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中并不是其“朋友”。
定义:如果对每一个类型为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()方法,所以这里的第五行代码会报错,从一定程度上避免了继承泛滥。
定义:尽量使用对象组合、聚合,而不是继承关系达到软件复用的目的
聚合: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** 优点:**可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少** **一句话总结:能不用继承就不用继承** 最后总结:设计模式是对我们开发中做的一些规范和约束,在实际的开发中并非要追求完美,而是在时间、成本等各方面允许的情况下尽量遵守规范。