开闭原则
定义
开闭原则指面向抽象编程,就比如现实生活中实行弹性工作制度,7点上班三点下班和9点上班5点下班是一个性质,采用灵活扩展的方式,并不一定是固定的时间。所以在软件开发过程中,如果一个类要经常扩展,那么肯定不能直接改原来的类,所以就要定义一个接口,相当于定义了一个固定的函数规范,然后用一个类实现该接口。如果要修改该类逻辑,则定义一个子类重写父类方法即可,这样代码就具有了扩展性而不影响原来的逻辑。
比如说课程有很多课程,如java,python,php,此时定义一个接口icourse,然后有一门课程叫做java,于是定义一门课程javaCourse实现Icourse接口,同样的有三个属性id,name,price,此时重写接口的构造方法和get和set方法,此时我们就可以创建java课程了,同样的后面有python,php课程,我们就只要新建对应的pythonCourse和phpCourse实现Icourse接口就行。同样的如果遇到双十一这样的节日,我们要扩展一门java打折课程,那么也只要创建一个javaDiscountCourse继承javaCourse重写获取价格的方法就可以了。这样我们在编写代码的过程中要扩展业务逻辑就不要修改原来的代码,减少了项目中类与类之间相互依赖的耦合度。提高了代码的可扩展性。
代码示例
先定义一个Icourse接口,有三个get方法,分别获取id,名称,价格
package signrule.openCloseRule;
public interface ICourse {
Integer getId();
String getName();
Double getPrice();
}
课程有java,大数据,人工只能等,我们先定义一个java课程类。
package signrule.openCloseRule.impl;
import signrule.openCloseRule.ICourse;
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;
}
public Integer getId() {
return this.Id;
}
public String getName() {
return this.name;
}
public Double getPrice() {
return this.price;
}
}
如果我们遇到了双十一,需要搞一波活动,需要给java课程打折,如果我们改变原有的类,则可能会影响其它调用该类的地方,所以我们为了新业务需要,定义一个java课程打折的类JavaDiscountCourse来继承
JavaCourse重写获取价格的方法。编写新业务时,使用该类即可。这样的程序就具备了扩展性,而且不需要改变原来的逻辑。
package signrule.openCloseRule.impl;
public class JavaDiscountCourse extends JavaCourse {
public JavaDiscountCourse(Integer id, String name, Double price) {
super(id, name, price);
}
public Double getOriginPrice() {
return super.getPrice();
}
public Double getPrice() {
return super.getPrice() * 0.61;
}
}
总结类图如下:
依赖倒置原则
定义
依赖倒置原则是指在设计类的层级结构时,高层调用不用依赖底层结构,而是高层价格和底层结构都依赖
抽象。这样可以减少系统之间的耦合性。通过依赖倒置,可以降低修改代码所带来的风险,并且提升系统的可读性和可维护性。
代码示例
以tom学编程为例子,tom既可以学习java,还可以学习python,我们创建一个类tom来定义学习java和python两个过程。
package signrule.dependencyinvers;
public class Tom {
public void studyJavaCourse() {
System.out.println("Tom 在学习Java 的课程");
}
public void studyPythonCourse() {
System.out.println("Tom 在学习Python 的课程");
}
}
package signrule.dependencyinvers;
public class testTomStudy {
public static void main(String[] args) {
Tom tom = new Tom();
tom.studyJavaCourse();
tom.studyPythonCourse();
}
}
但是学习也会上瘾,最近新出的ai比较火,所以tom又想学习AI课程,如果按照一般的设想,是要在tom类中添加studyAi方法来进行ai学习的,但是这样修改了高层应用,在应用发布的过程中可能会有未知的风险,为了解决这个问题。我们同样定义一个接口Icourse,并且定义一个学习的方法。
package signrule.dependencyinvers;
public interface ICourse {
public void study();
}
如果tom想学java,则创建javaCourse实现Icourse接口,重写study方法学习java。
package signrule.dependencyinvers.impl;
import signrule.dependencyinvers.ICourse;
public class JavaCourse implements ICourse {
@Override
public void study() {
System.out.println("tom在学习java");
}
}
如果tom想学python,则创建pythonCourse实现Icourse接口,重写study方法学习python。
package signrule.dependencyinvers.impl;
import signrule.dependencyinvers.ICourse;
public class pythonCourse implements ICourse {
@Override
public void study() {
System.out.println("tom在学习python");
}
}
改写tom类代码,创建一个studyCourse的方法,令其方法参数为Icourse。实现则用course来调用study方法
package signrule.dependencyinvers;
public class Tom {
public void studyCourse(ICourse course) {
course.study();
}
}
测试调用一下tom学java和tom学习python的过程。
package signrule.dependencyinvers;
import signrule.dependencyinvers.impl.JavaCourse;
import signrule.dependencyinvers.impl.PythonCourse;
public class testTomStudy {
public static void main(String[] args) {
Tom tom = new Tom();
tom.studyCourse(new JavaCourse());
tom.studyCourse(new PythonCourse());
}
}
这时的tom想学习ai课程,则只需要定义一个Aicourse实现Icourse接口在tom的studyCourse方法注入即可。
package signrule.dependencyinvers.impl;
import signrule.dependencyinvers.ICourse;
public class AiCourse implements ICourse {
@Override
public void study() {
System.out.println("tom在学习Ai");
}
}
package signrule.dependencyinvers;
import signrule.dependencyinvers.impl.AiCourse;
public class testTomStudy {
public static void main(String[] args) {
Tom tom = new Tom();
// tom.studyCourse(new JavaCourse());
// tom.studyCourse(new PythonCourse());
tom.studyCourse(new AiCourse());
}
}
我们可以发现,利用这种方式,tom每次想学习其它课程时,则只需要定义一个新课程实现接口然后在高层调用注入即可,就不需要修改高层代码,避免了因为修改代码而造成的风险。我们称这种方式为依赖注入。通过依赖注入,减少了系统的耦合性,提高了代码的扩展性。
除了上述的注入方式,常用的还有构造器注入和set方法注入,我们先看一下构造器注入,重新定义Tom类。
package signrule.dependencyinvers;
public class Tom {
private ICourse course;
public Tom(ICourse course) {
this.course = course;
}
public void studyCourse() {
this.course.study();
}
}
此时我们要调用对应的学习方法只需要在创建Tom对象时传入对应的课程实例即可。
package signrule.dependencyinvers;
import signrule.dependencyinvers.impl.JavaCourse;
import signrule.dependencyinvers.impl.PythonCourse;
public class testTomStudy {
public static void main(String[] args) {
Tom tom = new Tom(new JavaCourse());
tom.studyCourse();
tom = new Tom(new PythonCourse());
tom.studyCourse();
}
}
构造器注入也很好的避免了系统的耦合性,但是如果我们遇到了全局只有一个单例的时候,这种方法就不太适用。所有我们来了解一下set方法注入,重新定义一下Tom类。
package signrule.dependencyinvers;
public class Tom {
private ICourse course;
public void setCourse(ICourse course) {
this.course = course;
}
public void studyCourse() {
this.course.study();
}
}
使用set方法设置实例信息就可以调用对应的学习方法
package signrule.dependencyinvers;
import signrule.dependencyinvers.impl.JavaCourse;
import signrule.dependencyinvers.impl.PythonCourse;
public class testTomStudy {
public static void main(String[] args) {
Tom tom = new Tom();
tom.setCourse(new JavaCourse());
tom.studyCourse();
tom.setCourse(new PythonCourse());
tom.studyCourse();
}
}
最终类图如下
总结:当我们接到一个需求时,面向抽象是肯定要比面向细节好维护的多,所以我们一开始就要定义抽象接口,采用依赖倒置的原则方便后期维护。
单一职责原则
定义
在软件开发过程中,我们在定义类或方法时,要尽量的区分业务功能逻辑,一个方法或者一个类做到只负责一块的业务逻辑,不要把多个不相干的业务逻辑定义在一个方法或者一个类中。这样做可以提高代码的可读性,以及后续在修改相关业务逻辑时,可以避免因为代码耦合度过高而导致的其它业务报错。
代码示例
就拿课程举例,有直播课程和录播课程。录播课程可以快进,而直播课程不能,如果把两块逻辑放在同一个方法中肯定是不合适的。
package signrule.singleresponsibility;
public class Course {
public void study(String courseName) {
if ("直播课".equals(courseName)) {
System.out.println("不能快进");
} else {
System.out.println("可以任意的来回播放");
}
}
}
此时把这个课程类拆成两个类,一个直播类,一个录播类。
package signrule.singleresponsibility;
public class LiveCourse {
public void study(String courseName) {
System.out.println(courseName + "不能快进看");
}
}
package signrule.singleresponsibility;
public class ReplayCourse {
public void study(String courseName) {
System.out.println("可以任意的来回播放");
}
}
这样我们修改代码就只要修改对应类的代码就可以了。能够避免因代码修改而导致的不可控制的风险。
接口隔离原则
定义
在定义接口时,一定要尽量的细致化,不要出现范围过大的接口,接口的方法不要太多,根据实际而定,
否则在定义实现类时要重写很多非必要方法,造成很多冗余代码,可读性和可维护性都不好。
代码示例
我们先定义一个动物接口,有三个方法,飞行,吃,游泳。
package signrule.Interfacesegregat;
public interface IAnimal {
void eat();
void fly();
void swim();
}
这时有一个鸟类,实现IAnimal接口。
package signrule.Interfacesegregat.impl;
import signrule.Interfacesegregat.IAnimal;
public class Bird implements IAnimal {
@Override
public void eat() {
System.out.println("鸟吃虫子");
}
@Override
public void fly() {
System.out.println("鸟飞行");
}
@Override
public void swim() {
}
}
此时可以发现,鸟肯定是不能游泳的,但是实现了IAnimal接口只能空实现游泳方法。
在定义一个狗类Dog实现IAnimal接口。
package signrule.Interfacesegregat.impl;
import signrule.Interfacesegregat.IAnimal;
public class Dog implements IAnimal {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
@Override
public void fly() {
}
@Override
public void swim() {
System.out.println("狗游泳");
}
}
此时可以发现,狗肯定是不能飞行的,但是实现了IAnimal接口只能空实现飞行方法。
像IAnimal这样的接口,对于实际业务,方法定义过多,肯定会出现以后子类实现时,必须要重写非必要冗余方法,这样是冗余的。所以我们将其拆分成三个接口。IFlyAnimal,IEatAnimal,ISwimAnimal。
package signrule.Interfacesegregat;
public interface IEatAnimal {
void eat();
}
package signrule.Interfacesegregat;
public interface IFlyAnimal {
void fly();
}
package signrule.Interfacesegregat;
public interface ISwimAnimal {
void swim();
}
此时在定义动物子类时就可以根据需要来实现接口了。
package signrule.Interfacesegregat.impl;
import signrule.Interfacesegregat.IEatAnimal;
import signrule.Interfacesegregat.IFlyAnimal;
public class Bird implements IEatAnimal, IFlyAnimal {
@Override
public void eat() {
System.out.println("吃虫子");
}
@Override
public void fly() {
System.out.println("鸟飞行");
}
}
package signrule.Interfacesegregat.impl;
import signrule.Interfacesegregat.IEatAnimal;
import signrule.Interfacesegregat.ISwimAnimal;
public class Dog implements IEatAnimal, ISwimAnimal {
@Override
public void eat() {
System.out.println("吃骨头");
}
@Override
public void swim() {
System.out.println("游泳");
}
}
迪米特法则
定义
迪米特原则是一个对象对另一个对象要保持最少的了解,又叫最少知道原则,一个类应该保持只和自己的朋友类打交道,朋友类包括成员变量,方法返回参数,方法参数。而避免和陌生类打交道,这样可以尽可能的减少系统的耦合度,降低系统复杂度。
代码示例
目前有一个线上课程系统,组长需要检查目前发布的课程总数,组长命令组员,组员统计课程数量。
对于这个案例我们先定义一个课程类。
package signrule.demeter;
public class Course {
}
在创建一个组员类,定义一个检查课程数量的方法
package signrule.demeter;
import java.util.List;
public class Employ {
public void checkCourseNum(List<Course> courses){
System.out.println("课程的总数量为:"+courses.size());
}
}
创建组长类调用组员类的统计方法统计课程数量。
package signrule.demeter;
import java.util.ArrayList;
import java.util.List;
public class TeamLeader {
public void checkCourseNum(){
List<Course> courses = new ArrayList<>();
for (int i = 0; i < 20; i++) {
courses.add(new Course());
}
Employ employ = new Employ();
employ.checkCourseNum(courses);
}
}
测试用例如下:
package signrule.demeter;
public class TestTeamLeader {
public static void main(String[] args) {
TeamLeader teamLeader = new TeamLeader();
teamLeader.checkCourseNum();
}
}
看这种代码,表面上是没有问题,但是违背了迪米特法则,因为对于TeamLeader,Course并不是它的朋友类,TeamLeader应该避免直接和它打交道。下面我们来对其进行改造,修改Employ类的checkCourseNum方法。
package signrule.demeter;
import java.util.ArrayList;
import java.util.List;
public class Employ {
public void checkCourseNum(){
List<Course> courses = new ArrayList<>();
for (int i = 0; i < 20; i++) {
courses.add(new Course());
}
System.out.println("课程的总数量为:"+courses.size());
}
}
这样就可以修改TeamLeader将引用Course的环节删掉,转成有Employ间接引用Course,这样避免了系统的耦合性。
package signrule.demeter;
public class TeamLeader {
public void checkCourseNum(){
Employ employ = new Employ();
employ.checkCourseNum();
}
}
测试用例
package signrule.demeter;
public class TestTeamLeader {
public static void main(String[] args) {
TeamLeader teamLeader = new TeamLeader();
teamLeader.checkCourseNum();
}
}
观看类图,TeamLeader和Course就没有关联了,降低了类与类之间的耦合度。
里氏替换原则
定义
当系统中存类型为T1的对象o1,类型为T2的对象o2,当在使用o1的地方能够完全用o2替代时,那么我们说T1和T2符合里氏替换原则。一句简单的话,听起来有点懵逼,但是如果我们用面向对象的思维展开来说,这就很好理解了。
很明显这是具备父子关系的类才会符合这样的原则。而且父子继承必须要满足以下几点。
1.子类可以重写父类的抽象方法,父类的非抽象方法不能改变原有逻辑。否则不能替换。
2.子类中可以增加自己的方法。
3.子类重写父类的方法时,必须要保证方法入参要比父类宽松。
4.子类重写父类的方法时,必须要保证方法的出参要比父类严格。
代码示例
接下来借用正方形和长方形的例子来简单了解一下,先定义一个长方形Rectangle类
package signrule.richtersubstitut;
public class Rectangle {
private long height;
private long width;
public long getHeight() {
return height;
}
public void setHeight(long height) {
this.height = height;
}
public long getWidth() {
return width;
}
public void setWidth(long width) {
this.width = width;
}
}
然后定义一个正方形类继承长方形类
package signrule.richtersubstitut;
public class Square extends Rectangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
@Override
public long getHeight() {
return getLength();
}
@Override
public void setHeight(long height) {
setLength(height);
}
@Override
public long getWidth() {
return getLength();
}
@Override
public void setWidth(long width) {
setLength(width);
}
}
定义一个测试方法测试长方形
package signrule.richtersubstitut;
public class TestRectangle {
public static void resize(Rectangle rectangle){
while (rectangle.getWidth() >= rectangle.getHeight()){
rectangle.setHeight(rectangle.getHeight() + 1);
System.out.println("Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight());
}
System.out.println("Resize End,Width:" +rectangle.getWidth() +",Height:" + rectangle.getHeight());
}
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setWidth(20);
rectangle.setHeight(10);
resize(rectangle);
}
}
然后用引用长方形的地方用正方形代替。
测试结果时引用长方形正常,而引用正方形时死循环,这两个类不符合里氏替换原则,原因是因为正方形在继承长方形类时重写的长方形的非抽象方法getWidth()和setWidth()
为了符合里氏替换原则,我们可以定义一个四边形接口QuadRangle,定义长方形和正方形的公共方法getWidth和getLenth,因为正方形和长方形的setWidth和setLenth方法逻辑不一致,则采用内部扩展的形式。
package signrule.richtersubstitut;
public interface QuadRangle {
long getWidth();
long getHeight();
}
package signrule.richtersubstitut.impl;
import signrule.richtersubstitut.QuadRangle;
public class Rectangle implements QuadRangle {
private long height;
private long width;
public long getHeight() {
return height;
}
public void setHeight(long height) {
this.height = height;
}
public long getWidth() {
return width;
}
public void setWidth(long width) {
this.width = width;
}
}
package signrule.richtersubstitut.impl;
import signrule.richtersubstitut.QuadRangle;
public class Square implements QuadRangle {
private long length;
public void setLength(long length) {
this.length = length;
}
@Override
public long getHeight() {
return length;
}
@Override
public long getWidth() {
return length;
}
}
此时在原来的位置可以用Rectangle 但是使用QuadRangle就会报错。
合成复用原则
定义
对象与对象之间依赖关系最好是hasA或者containA的关系,继承是黑箱复用,弊端就是父类要将细节全部暴漏给子类。采用合成复用最好是代码既符合开闭原则,采用继承进行扩展的方式,又要使继承符合里氏替换原则。
代码示例
以数据库获取链接为例子,先定义一个mysq连接类。
package signrule.syntheticreuse;
public class DBConnection {
public String getConnection() {
return "MySQL 数据库连接";
}
}
在定义一个dao类获取连接
package signrule.syntheticreuse;
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 + "增加产品");
}
}
我们此时的代码是没毛病的,但是如果想要在扩展一个oracle连接,可以在DBConnection 扩展一个获取oracle数据库连接的方法,然后修改dao层代码。这样代码的耦合性就高了,第一不符合开闭原则,然后不符合依赖倒置原则,高层代码和底层都应该依赖抽象,而不是直接依赖。所以我们在这里将DBConnection定义为抽象的,分别定义MysqlDBConnection和OracleDBConnection继承DBConnection重写getConnction方法。这样我们就不需要改动应用层代码了
package signrule.syntheticreuse;
public abstract class DBConnection {
abstract String getConnection();
}
package signrule.syntheticreuse;
public class MysqlDBConnection extends DBConnection{
@Override
String getConnection() {
return "获取mysql连接";
}
}
package signrule.syntheticreuse;
public class OracleDBConnection extends DBConnection{
@Override
String getConnection() {
return "获取oracle连接";
}
}