• 大型Java进阶专题(三) 软件架构设计原则(下)


    前言

    ​ 今天开始我们专题的第二课了,本章节继续分享软件架构设计原则的下篇,将介绍:接口隔离原则、迪米特原则、里氏替换原则和合成复用原则。本章节参考资料书籍《Spring 5核心原理》中的第一篇 Spring 内功心法(没有电子档,都是我取其精华并结合自己的理解,一个字一个字手敲出来的)。

    接口隔离原则

    ​ 接口隔离原则(Interface Segregation Principke,ISP)是指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。这个原则知道我们在设计接口时应当注意以下几点:

    (1)一个类对另一个类的依赖应该建立在最小接口之上。

    (2)建立单一接口,不要建立庞大臃肿的接口。

    (3)尽量细化接口,接口中的方法尽量少(不是越少越好,一定要适度)。

    ​ 接口隔离原则符合我们常说的高内聚、低耦合的设计思想,可以使类有很好的可读性、可扩展性和可维护性。我们在设计接口的时候,要多花时间去思考,要考虑业务模型,包括对以后可能发生变化的地方做一些预判。所以,对于抽象、对于业务模型的理解是非常重要的。

    ​ 下面我们来看一段代码,对一个动物行为进行抽象描述。

    //描述动物行为的接口
    public interface IAnimal {
        void eat();
        void fly();
        void swim();
    }
    
    //鸟类
    public class Bird implements IAnimal {
        public void eat() {
        }
    
        public void fly() {
        }
    
        public void swim() {
        }
    }
    
    //狗
    public class Dog implements IAnimal {
        public void eat() {
        }
    
        public void fly() {
        }
    
        public void swim() {
        }
    }
    

    ​ 可以看出,Brid的swim()方法只能空着,并且Dog的fly()方法显然不可能的。这时候,我们针对不同动物行为来设计不同的接口,分别设计IEatAnimal、IFlyAnimal和ISwimAnimal接口,来看代码:

    public interface IEatAnimal {
        void eat();
    }
    
    public interface IFlyAnimal {
        void fly();
    }
    
    public interface ISwimAnimal {
        void swim();
    }
    

    此时Dog只需要实现IEatAnimal和ISwimAnimal接口即可,这样就清晰明了了。

    public class Dog implements IEatAnimal,ISwimAnimal {
    
        public void eat() {
        }
    
        public void swim() {
        }
    }
    

    迪米特原则

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

    ​ 现在设计一个权限系统,Boss需要查看目前发布到线上的课程数量。这时候,Boss要找到TeamLeader进行统计,TeamLeader再把统计结果告诉Boss,接下来我们来看看代码:

    //课程类
    public class Course {
    }
    
    //TeamLeader类
    public class TeamLeader {
        public void checkNumberOfCourses(List<Course> courses){
            System.out.println("目前已经发布的课程数量:"+courses.size());
        }
    }
    
    //Boss类
    public class Boss {
        public void commandCheckNumber(TeamLeader teamLeader){
            //模拟BOSS一页一页往下翻页,TeamLeader实时统计
            List<Course> courseList = new ArrayList<Course>();
            for (int i = 0; i < 20; i++) {
                courseList.add(new Course());
            }
            teamLeader.checkNumberOfCourses(courseList);
        }
    }
    
    //调用方代码
    public static void main(String[] args) {
            Boss boss = new Boss();
            TeamLeader teamLeader = new TeamLeader();
            boss.commandCheckNumber(teamLeader);
    }
    

    ​ 写到这里,其实功能已经实现,代码看上去没有什么问题,但是根据迪米特原则,Boss只想要结果,不希望跟Course直接交流。TeamLeader统计需要引用Course对象。Boss和Course并不是朋友,从下面的类图可以看出来:

    ​ 下面对代码进行改造:

    //TeamLeader做与course的交流
    public class TeamLeader {
        public void checkNumberOfCourses(){
            List<Course> courses = new ArrayList<Course>();
            for (int i = 0; i < 20; i++) {
                courses.add(new Course());
            }
            System.out.println("目前已经发布的课程数量:"+courses.size());
        }
    }
    
    //Boss直接与TeamLeader交流,不再直接与Course交流
    public class Boss {
        public void commandCheckNumber(TeamLeader teamLeader){
            //模拟BOSS一页一页往下翻页,TeamLeader实时统计
            teamLeader.checkNumberOfCourses();
        }
    }
    

    再看下类图,Boss与Course已经没有联系了

    ​ 这里切记,学习软件设计规则,千万不能形成强迫症,碰到业务复杂的场景,我们需要随机应变。

    里氏替换原则

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

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

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

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

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

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

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

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

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

    ​ 现在来描述一个经典的业务场景,用正方形、矩形和四边形的关系说明里氏替换原则,我们都知道正方形一个特殊的矩形,所以就可以创建一个父类Rectangle:

    //矩形类
    public class Rectangle {
        private long hight;
        private long width;
    
        public long getHight() {
            return hight;
        }
    
        public void setHight(long hight) {
            this.hight = hight;
        }
    
        public long getWidth() {
            return width;
        }
    
        public void setWidth(long width) {
            this.width = width;
        }
    }
    
    //正方形类
    public class Square extends Rectangle {
        private long length;
    
        public long getLength() {
            return length;
        }
    
        public void setLength(long length) {
            this.length = length;
        }
    
        @Override
        public long getHight() {
            return super.getHight();
        }
    
        @Override
        public void setHight(long hight) {
            super.setHight(hight);
        }
    
        @Override
        public long getWidth() {
            return super.getWidth();
        }
    
        @Override
        public void setWidth(long width) {
            super.setWidth(width);
        }
    }
    
    public class DemoTest {
        //在测试类中创建resize方法,长方形的宽应该大于等于高,我们让高一直增加,直至高等于宽,变成正方形。
        public static void resize(Rectangle rectangle) {
            while (rectangle.getWidth() >= rectangle.getHight()){
                rectangle.setHight(rectangle.getHight()+1);
                System.out.println("宽度:"+rectangle.getWidth()+"高度:"+rectangle.getHight());
            }
            System.out.println("resize方法结束!");
        }
    
    	//测试代码如下
        public static void main(String[] args) {
            Rectangle rectangle = new Rectangle();
            rectangle.setHight(10);
            rectangle.setWidth(20);
            resize(rectangle);
        }
    
    

    ​ 看下控制台输出,发现高度最后大于了宽度,这个情况在正方形中是正常的情况,现在我们把Rectangle类替换成它的子类的话,就不符合逻辑了,违背了里氏替换原则,将父类替换成子类后,程序运行结果没有达到预期。因此,我们的代码设计存在一定的风险的。里氏替换原则只存在于父类与子类之间,约束继承泛滥。我们再来创建一个基于正方形和长方形共同的抽象接口四边形接口Quadrangle:

    public interface Quadrangle { 
        long getWidth(); 
        long getHeight(); 
    }
    

    修改长方形 Rectangle 类:

    public class Rectangle implements Quadrangle {
        private long height;
        private long width;
    
        public long getHeight() {
            return height;
        }
    
        public long getWidth() {
            return width;
        }
    
        public void setHeight(long height) {
            this.height = height;
        }
    
        public void setWidth(long width) {
            this.width = width;
        }
    }
    

    修改正方形类 Square 类:

    public class Square implements Quadrangle {
        private long length;
    
        public long getLength() {
            return length;
        }
    
        public void setLength(long length) {
            this.length = length;
        }
    
        public long getWidth() {
            return 0;
        }
    
        public long getHeight() {
            return 0;
        }
    }
    

    ​ 此时,如果我们把 resize()方法的参数换成四边形 Quadrangle 类,方法内部就会报错。 因为正方形 Square 已经没有了 setWidth()和 setHeight()方法了。因此,为了约束继承 泛滥,resize()的方法参数只能用 Rectangle 长方形。当然,我们在后面的设计模式课程 中还会继续深入讲解。

    合成复用原则

    ​ 合成复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用对象组 合(has-a)/聚合(contanis-a),而不是继承关系达到软件复用的目的。可以使系统更加灵 活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。 继承我们叫做白箱复用,相当于把所有的实现细节暴露给子类。组合/聚合也称之为黑箱 复用,对类以外的对象是无法获取到实现细节的。要根据具体的业务场景来做代码设计, 其实也都需要遵循 OOP 模型。还是以数据库操作为例,先来创建 DBConnection 类:

    public class DBConnection { 
        public String getConnection(){ 
            return "MySQL 数据库连接"; 
        } 
    }
    

    创建 ProductDao 类:

    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,来看代码:

    public abstract class DBConnection { 
        public abstract String getConnection(); 
    }
    

    然后,将 MySQL 的逻辑抽离:

    public class MySQLConnection extends DBConnection { 
        @Override 
        public String getConnection() { 
            return "MySQL 数据库连接"; 
        } 
    }
    

    再创建 Oracle 支持的逻辑:

    public class OracleConnection extends DBConnection { 
        @Override 
        public String getConnection() { 
            return "Oracle 数据库连接"; 
        } 
    }
    

    具体选择交给应用层,来看一下类图:

    设计原则总结

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

  • 相关阅读:
    uva 11294 Wedding
    uvalive 4452 The Ministers’ Major Mess
    uvalive 3211 Now Or Later
    uvalive 3713 Astronauts
    uvalive 4288 Cat Vs. Dog
    uvalive 3276 The Great Wall Game
    uva 1411 Ants
    uva 11383 Golden Tiger Claw
    uva 11419 SAM I AM
    uvalive 3415 Guardian Of Decency
  • 原文地址:https://www.cnblogs.com/whgk/p/12465380.html
Copyright © 2020-2023  润新知