第1章 软件架构设计原则

1.1 开闭原则

开闭原则(Open-Closed Principle,OCP)是指一个软件实体(如类、模块和函数)应该对扩展开放,对修改关闭。所谓的开闭,也正是对扩展和修改两个行为的一个原则。它强调的是用抽象构建框架,用实现扩展细节,可以提高软件系统的可复用性及可维护性。开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定、灵活的系统。例如版本更新,我们尽可能不修改源代码,但是可以增加新功能。

/**
 * @author WangYifei
 * @date 2020-12-03 10:36
 * @describe 人的接口,每个人都有id,名称,年龄,抽离出来
 */
public interface IUser {
    Integer getId();
    String getName();
    String getAge();
}
/**
 * @author WangYifei
 * @date 2020-12-03 10:37
 * @describe 一个叫罗永健,他也是一个人,所以实现了用户,会有getId,但是后来发生了变异
 * 这个时候怎么办,变异拥有了超能力,但是设计模式原则,对扩展开放,对修改关闭,那么怎么去增加呢?
 * Java的继承来了
 */
public class ILuoYongJian implements IUser{
    private Integer id;

    private String name;

    private String age;

    ILuoYongJian(Integer id, String name, String age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public Integer getId() {
        return id;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getAge() {
        return age;
    }
}
/**
 * @author WangYifei
 * @date 2020-12-03 10:40
 * @describe 继承了 罗永健原有的属性,进行一个扩展,新增了获取超能力的能力
 */
public class ISuperLuoYongJian extends ILuoYongJian{
    ISuperLuoYongJian(Integer id, String name, String age) {
        super(id, name, age);
    }

    @Override
    public String getName() {
        return super.getName();
    }

    public String getSuper() {
        return "隐身术";
    }
}

通过代码注释可以看到,总共使用了一个接口,两个类。一个是普通人,然后实现了IUser,基本的属性都有,这个时候突然发生了变异拥有了超能力,但是开闭原则是不修改原有的代码,这个好处是,以免版本更新或者新增功能,对原有功能造成了不好的影响,这是一个思想,希望自己以后在编写程序的时候可以立刻想到。

1.2 依赖倒置原则

依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。*:要依赖于抽象,不要依赖于具体。简单的说就是面向抽象接口进行编程,衍生出具体的实现,而不是固定写死实现,这样代码耦合性太高,改动一处可能全局崩盘。

/**
 * @author WangYifei
 * @date 2020-12-03 11:24
 * @describe 一个人叫lyj,他喜欢吃西瓜和苹果
 */
public class ILuoYongJian {
    public void eatXg() {
        System.out.println("吃西瓜");
    }

    public void eatPg() {
        System.out.println("吃苹果");
    }
}
/**
 * @author WangYifei
 * @date 2020-12-03 11:25
 * @describe 操作尝试
 */
public class Test {
    public static void main(String[] args) {
        ILuoYongJian iLuoYongJian = new ILuoYongJian();
        iLuoYongJian.eatPg();
        iLuoYongJian.eatXg();
    }
}

这样的话,就会打印出,吃苹果, 吃西瓜,如果这个B又想吃榴莲怎么办?

那我就要上去修改给ILuoYongJian.java 里面新增一个 eatLl();这样代码耦合性就很高,因为他可能吃很多东西,一会吃这个一会吃那个,现在对实现进行修改。

/**
 * @author WangYifei
 * @date 2020-12-03 11:23
 * @describe 每一个人都会吃
 */
public interface Eat {
    void start();
}

因为要吃不同的东西,但是吃这个动作是相同的,单独抽离出来。

/**
 * @author WangYifei
 * @date 2020-12-03 11:33
 * @describe
 */
public class EatPg implements Eat{
    @Override
    public void start() {
        System.out.println("吃苹果");
    }
}

之后吃不同的东西,去实现这个吃的接口,我不管你吃什么,反正你就是吃了。

/**
 * @author WangYifei
 * @date 2020-12-03 11:33
 * @describe
 */
public class EatXg implements Eat{
    @Override
    public void start() {
        System.out.println("吃西瓜");
    }
}
/**
 * @author WangYifei
 * @date 2020-12-03 11:24
 * @describe 一个人叫lyj,他喜欢吃西瓜和苹果
 */
public class ILuoYongJian {

    public void Eat(Eat eat) {
        eat.start();
    }
}

可以看到,这个时候,lyj只需要调用一个Eat,然后开始里面的吃,不知道吃什么,我吃什么看你传什么,你传什么我吃什么?!不挑食的好孩子。这样每次新增一个,不需要修改原有的代码,符合开闭原则,也减少了吃和对象之间的直接依赖关系,而是各自都面向抽象的Eat.java进行维护和修改。

看一下测试类。

/**
 * @author WangYifei
 * @date 2020-12-03 11:25
 * @describe
 */
public class Test {
    public static void main(String[] args) {
        ILuoYongJian iLuoYongJian = new ILuoYongJian();
        iLuoYongJian.Eat(new EatPg());
        iLuoYongJian.Eat(new EatXg());
    }
}

第一次喂食了苹果,第二次喂食了西瓜,如果还需要榴莲,那么只需要新增一个吃榴莲的类,去实现Eat,进行喂食就可以了。

1.3 单一职责原则

单一职责(Simple Responsibility Pinciple,SRP)是指不要存在多于一个导致类变更的原因。假设我们有一个类负责两个职责,一旦发生需求变更,修改其中一个职责的逻辑代码,有可能导致另一个职责的功能发生故障。这样一来,这个类就存在两个导致类变更的原因。如何解决这个问题呢?将两个职责用两个类来实现,进行解耦。后期需求变更维护互不影响。这样的设计,可以降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险。总体来说,就是一个类、接口或方法只负责一项职责。*: 个人觉得这是一个降低耦合的设计模式,让编码逻辑不相互影响。

/**
 * @author WangYifei
 * @date 2020-12-03 12:35
 * @describe 楼梯,上下楼梯
 */
public class Stairs {
    public void go(String operating) {
        if (operating.equals("上楼")) {
            System.out.println("上楼");
        } else if (operating.equals("下楼")) {
            System.out.println("下楼");
        }
    }
}

一个地方有楼房,需要上楼下楼,我传上楼命令就是上楼,传下楼命令就是下楼。

/**
 * @author WangYifei
 * @date 2020-12-03 12:37
 * @describe
 */
public class Test {
    public static void main(String[] args) {
        Stairs stairs = new Stairs();
        stairs.go("上楼");
        stairs.go("下楼");
    }
}

但是,如果我上楼和下楼换成了自动电梯,观光梯,但是可能是不同厂商的,效果不同速度不同,设置不同,逻辑不同等等...这个时候,我改了一个可能对下楼有影响,最主要的代码不好维护,不易检查,因为负责多种电梯的效果。

改造一下:

/**
 * @author WangYifei
 * @date 2020-12-03 12:35
 * @describe 楼梯,上下楼梯
 */
public interface Stairs {
    void go();
}

把都有的操作,比如上楼下楼,封装成一个接口。

/**
 * @author WangYifei
 * @date 2020-12-03 12:40
 * @describe
 */
public class ILyjStairs implements Stairs{
    @Override
    public void go() {
        System.out.println("lyj牌电梯,我速度快");
    }
}

lyj牌电梯,速度快,等一系列逻辑操作,都放在这,比如给电梯再加上关的操作就显而易见了。

/**
 * @author WangYifei
 * @date 2020-12-03 12:40
 * @describe
 */
public class IWyfStairs implements Stairs{
    @Override
    public void go() {
        System.out.println("wyf牌电梯,我速度慢");
    }
}

lyj牌和wyf牌的关闭按钮不在一起,但是共用在一个商场,一个需要蹲着关,一个需要站着关,如果写在一起,那么需要同时关注两种电梯的业务逻辑,个人觉得这也是单一原则的用处。

1.4 接口隔离原则

/**
 * @author WangYifei
 * @date 2020-12-03 12:48
 * @describe 行为接口类
 */
public interface Behavior {
    // 游泳
    void swim();

    // 飞翔
    void fly();

    // 吃
    void eat();
}

先拟定一个 行为接口类,里面有游泳,飞翔,和吃。

/**
 * @author WangYifei
 * @date 2020-12-03 12:49
 * @describe 小鸟类
 */
public class Bird implements Behavior{
    @Override
    public void swim() {

    }

    @Override
    public void fly() {

    }

    @Override
    public void eat() {

    }
}
/**
 * @author WangYifei
 * @date 2020-12-03 12:49
 * @describe 狗类
 */
public class Dog implements Behavior{
    @Override
    public void swim() {

    }

    @Override
    public void fly() {

    }

    @Override
    public void eat() {

    }
}

再看两个小动物种类,一个是狗,一个是鸟。都实现了行为,因为他们都有行为,那么问题来了?狗能飞吗?鸟能游泳吗?可能有的鸟可以= =,就造成了不必要的重写,那么就将这些行为抽离出去。

图不好放,我就不放了= =太麻烦,看一下实现类吧。

/**
 * @author WangYifei
 * @date 2020-12-03 12:49
 * @describe 小鸟类
 */
public class Bird implements EatBehavior,FlyBehavior{

    @Override
    public void eat() {

    }

    @Override
    public void fly() {

    }
}

这个是小鸟的,利用java的多接口实现,这个是小鸟的,小鸟可以吃可以飞,但是不能游泳。

/**
 * @author WangYifei
 * @date 2020-12-03 12:49
 * @describe 狗类
 */
public class Dog implements EatBehavior,SwimBehavior{

    @Override
    public void eat() {

    }

    @Override
    public void swim() {

    }
}

小狗的,小狗可以吃,可以游泳,但是不能飞。接口隔离原则还是挺好理解的吧= =这样就每个接口隔离开来,不需要全部重写。

1.5 迪米特原则

迪米特原则(Law of Demeter LoD)是指一个对象应该对其他对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle,LKP),尽量降低类与类之间的耦合度。迪米特原则主要强调:只和朋友交流,不和陌生人说话。出现在成员变量、方法的输入、输出参数中的类都可以称为成员朋友类,而出现在方法体内部的类不属于朋友类。

/**
 * @author WangYifei
 * @date 2020-12-03 13:00
 * @describe
 */
public class Boss {
    public void commandCheckNumber(TeamLeader teamLeader) {
        List<String> courseList = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            courseList.add(new String());
        }
        teamLeader.checkNumberOfCourses(courseList);
    }
}

老板类

/**
 * @author WangYifei
 * @date 2020-12-03 13:01
 * @describe
 */
public class TeamLeader {
    public void checkNumberOfCourses(List<String> courseList) {
        System.out.println("这个人目前的课程数量:" + courseList.size());
    }
}

员工类

看一下调用类:

/**
 * @author WangYifei
 * @date 2020-12-03 13:04
 * @describe
 */
public class Test {
    public static void main(String[] args) {
        TeamLeader teamLeader = new TeamLeader();
        Boss boss = new Boss();
        boss.commandCheckNumber(teamLeader);
    }
}

这个人目前的课程数量:20(这个是结果)

看起来没什么问题, 这个员工有20个课或者任何你好理解的形式,比如这个手下手里有20个员工。

我需要知道我这个手下有多少员工?那你手里的员工跟我什么关系?你要你是干嘛的,要你统计的,你不统计,你让我去数你手底下的人?那你手底下的人的手底下的人呢?我再去数?我不需要跟你手底下的人打招呼,你自己数好跟我说就行。*:迪米特原则:跟我没关系的就不要让我去操作。

修改之后的:

/**
 * @author WangYifei
 * @date 2020-12-03 13:01
 * @describe
 */
public class TeamLeader {
    public void checkNumberOfCourses() {
        List<String> courseList = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            courseList.add(new String());
        }
        System.out.println("这个人目前的课程数量:" + courseList.size());
    }
}

可以看到,现在数员工的活(for循环模拟数员工)交给了,自己的手下去办了。

/**
 * @author WangYifei
 * @date 2020-12-03 13:00
 * @describe
 */
public class Boss {
    public void commandCheckNumber(TeamLeader teamLeader) {
        teamLeader.checkNumberOfCourses();
    }
}

而上司只需要得到一个结果,当然结果也是20,但是过程不一样了,我不需要管你的业务逻辑了。只需要指派我要查看哪一个手下的手里有多少人。

/**
 * @author WangYifei
 * @date 2020-12-03 13:04
 * @describe
 */
public class Test {
    public static void main(String[] args) {
        TeamLeader teamLeader = new TeamLeader();
        Boss boss = new Boss();
        boss.commandCheckNumber(teamLeader);
    }
}

1.6 里氏替换原则

里氏替换原则(Liskov Substitution Principle,LSP)是指如果对每一个类型为T1的对象o1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有的对象O1都替换成O2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。

这个定义看上去还是比较抽象的,我们重新理解一下。可以理解为一个软件实体如果适用于一个父类,那么一定适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。根据这个理解,引申含义为:子类可以扩展父类的功能,但不能改变父类原有的功能。

(1)子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。

(2)子类可以增加自己特有的方法。

(3)当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。

(4)当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或与父类一样。

通俗一点解释:*:子类的操作不能影响父类的操作,重载但是不能影响父类,不能重写。

使用里氏替换原则有以下优点:

(1)约束继承泛滥,是开闭原则的一种体现。

(2)加强程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的可维护性和扩展性,降低需求变更时引入的风险。

1.7 合成复用原则

合成复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用对象组合(has-a)/聚合(contanis-a)而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。

合成复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用对象组合(has-a)/聚合(contanis-a)而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。

继承叫作白箱复用,相当于把所有的实现细节暴露给子类。组合/聚合称为黑箱复用,我们是无法获取到类以外的对象的实现细节的。虽然我们要根据具体的业务场景来做代码设计,但也需要遵循OOP模型。以数据库操作为例先来创建DBConnection类::

/**
 * @author WangYifei
 * @date 2020-12-03 13:20
 * @describe
 */
public class DBConnection {
    public String getConnection() {
        return "Mysql 数据库连接";
    }
}

创建ProductDao类:

/**
 * @author WangYifei
 * @date 2020-12-03 13:21
 * @describe
 */
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 + "增加产品");
    }
}

这就是一种非常典型的合成复用原则的应用场景。但是,就目前的设计来说,DBConnection还不是一种抽象,不便于系统扩展。目前的系统支持 MySQL 数据库连接,假设业务发生变化,数据库操作层要支持Oracle数据库。当然,我们可以在DBConnection中增加对Oracle数据库的支持,但是这违背了开闭原则。其实,我们可以不修改 Dao 的代码,而将 DBConnection 修改为“abstract”的,来看代码:

/**
 * @author WangYifei
 * @date 2020-12-03 13:20
 * @describe
 */
public abstract class DBConnection {
    public abstract String getConnection();
}
/**
 * @author WangYifei
 * @date 2020-12-03 13:26
 * @describe
 */
public class MysqlConnection extends DBConnection{
    @Override
    public String getConnection() {
        return "Mysql连接";
    }
}
/**
 * @author WangYifei
 * @date 2020-12-03 13:26
 * @describe
 */
public class OracleConnection extends DBConnection{
    @Override
    public String getConnection() {
        return "Oracle数据库连接";
    }
}

ProductDao没有变,具体的选择哪一个数据库,就传入哪一款数据库。调用测试。

/**
 * @author WangYifei
 * @date 2020-12-03 13:27
 * @describe
 */
public class Test {
    public static void main(String[] args) {
        ProductDao productDao = new ProductDao();
        DBConnection dbConnection = new MysqlConnection();
        productDao.setDbConnection(dbConnection);
        productDao.addProduct();
    }
}

感觉无形之中,用到了以前了解过的抽象工厂模式和单一职责原则。

这一串代码,就选用了mysql作为数据库产品进行增加。

1.8 设计模式总结

学习设计原则是学习设计模式的基础。在实际开发过程中,并不要求所有代码都遵循设计原则,我们要考虑人力、时间、成本、质量,不能刻意追求完美,但要在适当的场景遵循设计原则,这体现的是一种平衡取舍,可以帮助我们设计出更加优雅的代码结构。

最后修改:2021 年 01 月 21 日 04 : 34 PM
如果觉得我的文章对你有用,请随意赞赏