1.1 开闭原则
定义:
一个软件实体(类、模块和函数)应该对扩展开放,对修改关闭。强调用抽象构建框架,用实现扩展细节。
举例:
首先创建一个课程接口:
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;
}
}
这时突然提了一个需求,比如课程的价格有变动,需要对getPrice()方法进行修改,如果直接去改动这个方法,则其它调用这个方法的代码可能存在风险。可以通过如下方式:
public class JavaDiscussCourse extends JavaCourse{
public JavaDiscussCourse(Integer id, String name, Double price) {
super(id, name, price);
}
public Double getOriginPrice() {
return super.getPrice();
}
public Double getPrice() {
return super.getPrice() * 0.61;
}
}
1.2 依赖倒置原则
定义:
设计代码结构时,高层模块不应该依赖底层模块,抽象不应该依赖细节。
举例:
Tom正在学两门课程, 分别是Java和Python:
public class Tom {
public void studyJavaCourse() {
System.out.println("Tom在学习Java课程");
}
public void studyPythonCourse() {
System.out.println("Tom在学习Python课程");
}
public static void main(String[] args) {
Tom tom = new Tom();
tom.studyJavaCourse();
tom.studyPythonCourse();
}
}
此时,他突然还想学AI,于是准备在Tom类里在家一个study方法,这样的方式真的太差劲了,Tom和课程耦合度太高。
如下从Tom抽象出课程接口,然后Tom面向接口调用课程,具体的实现类由调用层自己指定。这样不管增加多少课程,Tom都不需要管。
首先创建一个课程接口,里面只有一个可以获取自己的名称方法:
public interface ICourse {
public String getName();
}
在分别创建三个课程实现类
public class JavaCourse implements ICourse {
private String name;
public JavaCourse() {
this.name = "Java";
}
@Override
public String getName() {
return name;
}
}
public class PythonCourse implements ICourse {
private String name;
public PythonCourse() {
this.name = "Python";
}
@Override
public String getName() {
return name;
}
}
public class AiCourse implements ICourse {
private String name;
public AiCourse() {
this.name = "Ai";
}
@Override
public String getName() {
return name;
}
}
最后在修改Tom类,使其面向课程接口调用:
public class Tom {
public void study(ICourse course) {
System.out.println("Tom正在学习" + course.getName());
}
public static void main(String[] args) {
Tom tom = new Tom();
tom.study(new JavaCourse());
tom.study(new PythonCourse());
tom.study(new AiCourse());
}
}
1.3 单一职责原则
定义:
不要存在多于一个导致类变更的原因。
举例:
假如课程分成了直播课和录播课,直播课不能快进,录播课可以,很明显这两种课程功能职责已经不一样。下面展示一端看似好像不复杂的代码,但其实已经埋下了复杂的隐患:
public class Course {
public void study(String courseName) {
if("直播课".equals(courseName)) {
System.out.println(courseName + "不能快进");
} else {
System.out.println(courseName + "可以反复观看");
}
}
public static void main(String[] args) {
Course course = new Course();
course.study("直播课");
course.study("录播课");
}
}
这个时候,如果需求变了,假如现在要对课程加密,直播课和录播课的加密逻辑不一样,那修改的逻辑就会相互影响,充满了风险。所以我们需要对上面的代码进行解耦,可以分别创建LiveCourse和ReplayCourse:
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 class Test {
public static void main(String[] args) {
LiveCourse liveCourse = new LiveCourse();
ReplayCourse replayCourse = new ReplayCourse();
liveCourse.study("直播课");
replayCourse.study("录播课");
}
}
1.4 接口隔离原则
定义:用多个专门的接口,而不适用单一的总接口。
举例:
先举一个不合适的例子,假如不遵从接口隔离原则,则设计的接口一般比较臃肿,比如如下的IAnimal接口:
public interface IAnimal {
void eat();
void fly();
void swim();
}
此时如果有两个接口的实现类,分别是Bird鸟类和Dog狗类,则将会出现和奇葩的问题。
public class Bird implements IAnimal {
@Override
public void eat() {
}
@Override
public void fly() {
}
@Override
public void swim() {
}
}
public class Dog implements IAnimal {
@Override
public void eat() {
}
@Override
public void fly() {
}
@Override
public void swim() {
}
}
什么问题,还没发现吗?仔细一看发现鸟居然有一个swim(),狗居然有一个fly()方法,你说奇怪不奇怪。所以啊,接口是需要细分的,在这里接口要针对不同的行为来细分,比如分别设计出IEatAnimal、IFlyAnimal和ISwimAnimal接口,那鸟和狗肯定都需要实现公共接口IEatAnimal,不然不饿死了嘛。除了实现公共接口,他们还需实现自己特有行为的接口,鸟实现IFlyAnimal接口,狗实现ISwimAnimal接口。
1.5 迪米特原则
定义:一个对象应该对其他对象保持最少的了解,尽量降低类与类之间的耦合度。
举例:
假如现在有个老板类Boss想要看看线上的课程数量,这个时候他去找到开发团队领导TeamLeader去进行统计,TeamLeader需要将结果给Boss。
public class Course {
}
public class TeamLeader {
public void checkNumberOfCourses(List coursesList) {
System.out.println("目前已发布的课程数量是:" + coursesList.size());
}
}
public class Boss {
public void checkNumberOfCourses(TeamLeader teamLeader) {
List coursesList = new ArrayList<>();
for(int i=0; i<20; i++) {
coursesList.add(new Course());
}
teamLeader.checkNumberOfCourses(coursesList);
}
}
如果是按上面的方法来完成课程数量统计,想必老板Boss早把开发团队领导TeamLeader炒掉了吧,为啥?很明显老板招人肯定是给他干活,他自己安排一件事,最希望的结果是你去干,我啥都不管,最后给我一个结果就行,你们说是不是,一个简单的事情都干不好怎么能不被炒,哈哈。看看下面的代码,一个合格的TeamLeader总是能表现的优秀(老板少操心):
public class TeamLeader {
public void checkNumberOfCourses() {
List coursesList = new ArrayList<>();
for(int i=0; i<20; i++) {
coursesList.add(new Course());
}
System.out.println("目前已发布的课程数量是:" + coursesList.size());
}
}
public class Boss {
public void checkNumberOfCourses(TeamLeader teamLeader) {
teamLeader.checkNumberOfCourses();
}
}
1.6 里式替换原则
定义:一个软件实体如果适用于一个父类,那么一定适用于子类。
隐含意思:子类可以扩展父类,但是不能改变父类原有功能。
(1)子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
(2)子类可以添加新的方法。
(3)重载父类时,方法的入参要比父类方法的输入参数更宽松。
(4)重载父类时,方法的出参要比父类方法的输出参数更严格。
在1.1描述开闭原则时,增加了一个获取父类属性的方法,还重写了父类的非抽象方法,很明显违背了里式替换原则。
举例:
正方形是一种特殊的长方形。
首先创建一个父类Rectangle:
public class Rectangle {
private long height;
private long weight;
public long getHeight() {
return height;
}
public void setHeight(long height) {
this.height = height;
}
public long getWeight() {
return weight;
}
public void setWeight(long weight) {
this.weight = weight;
}
}
违背里式替换原则创建一个正方形Square类:
public class Square extends Rectangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public long getHeight() {
return getLength();
}
public void setHeight(long height) {
setLength(height);
}
public long getWeight() {
return getLength();
}
public void setWeight(long weight) {
setLength(weight);
}
}
最后在测试类中创建resize()方法,长方形的宽应该大于等于高,我们让高一直增长,直到高和宽相等变成正方形:
public class Test {
public static void resize(Rectangle rectangle) {
while(rectangle.getWeight() >= rectangle.getHeight()) {
rectangle.setHeight(rectangle.getHeight() + 1);
System.out.println("width:" + rectangle.getWeight() + " ,height:" + rectangle.getHeight());
}
System.out.println("resize方法结束" + "\nwidth:" + rectangle.getWeight() + " ,height:" + rectangle.getHeight());
}
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setHeight(10);
rectangle.setWeight(20);
resize(rectangle);
}
}
现在将Rectangle类替换成子类Square。
public class Test {
public static void resize(Rectangle rectangle) {
while(rectangle.getWeight() >= rectangle.getHeight()) {
rectangle.setHeight(rectangle.getHeight() + 1);
System.out.println("width:" + rectangle.getWeight() + " ,height:" + rectangle.getHeight());
}
System.out.println("resize方法结束" + "\nwidth:" + rectangle.getWeight() + " ,height:" + rectangle.getHeight());
}
public static void main(String[] args) {
Square square = new Square();
square.setHeight(10);
square.setWeight(20);
resize(square);
}
}
上面代码运行时出现死循环,为啥?这个应该不用我解释了吧,因为重写了父类非抽象方法,使长方形的宽高始终相等,所以while就死了呗。
下面演示一个遵循里式替换原则的案例。
只需稍稍改动一下正方形类,使其不覆盖父类非抽象方法。
public class Square extends Rectangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
}
这个时候运行Test类,将Rectangle换成Square是不会报错的。
1.7 合成复用原则
定义:尽量使用对象组合/聚合而不是继承来达到软件复用。
继承叫白箱复用,因为实现细节暴露给子类了。
组合/聚合叫黑箱复用,因为无法获取到组合对象的实现细节。
举例:
下面举一个合成复用原则的案例。
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数据库连接";
}
}
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 + "增加产品");
}
}
1.8 设计原则总结
理想与现实总是不能画上等号,为毛?因为现实开发中要考虑人力、时间、成本、质量等种种因素,不能只追求完美。适当的场景遵循合适的设计原则才是力求最好的标准,所以鱼和熊掌不可兼得。