Spring软件架构设计原则

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<Course> coursesList) {
        System.out.println("目前已发布的课程数量是:" + coursesList.size());
    }
}

public class Boss {
    public void checkNumberOfCourses(TeamLeader teamLeader) {
        List<Course> 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<Course> 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 设计原则总结
理想与现实总是不能画上等号,为毛?因为现实开发中要考虑人力、时间、成本、质量等种种因素,不能只追求完美。适当的场景遵循合适的设计原则才是力求最好的标准,所以鱼和熊掌不可兼得。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章