6大设计原则详解(二)

4. 接口隔离原则(ISP)

(1)概念

接口隔离原则的定义是:建立单一的接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。

每个模块应该是单一的接口,提供给几个模块就应该有几个接口,而不是建立一个庞大臃肿的借口来容纳所有客户端访问。

与单一职责原则不同:比如一个接口的职责可能包含10个方法,这10个方法都放在一个接口中,并且提供给多个模块访问。各个模块按照规则的权限来访问,在系统外通过文档约束“不使用的方法不要访问”。按照单一职责原则是允许的,按照接口隔离原则是不允许的,因为ISP要求尽量使用多个专门的接口,而不是一个庞大臃肿的接口。

(2)举例

老师类和学生类实现工作的接口类:

实现代码如下:

//工作接口类
public interface DoWork {

    // 学生类要实现的方法
    public void doHomeWork();

    // 老师类要实现的方法
    public void correctingHomework(int StudentID);

    // 老师类和学生类共同需要实现的方法
    public void attendClass();

}
//老师类实现工作接口
public class Teacher implements DoWork {
    private int teacherID;

    @Override
    public void doHomeWork() {
        // 应该是学生类调用的方法,由于老师类实现了接口DoWork就必须实现接口所有的方法,这里只能为空
    }

    @Override
    public void correctingHomework(int StudentID) {
        System.out.println("老师批改作业...");

    }

    @Override
    public void attendClass() {
        System.out.println("老师开始上课...");
    }

}
//学生类实现工作接口
public class Student implements DoWork{
    private int studentID;

    @Override
    public void doHomeWork() {
        System.out.println("学生做作业...");
    }

    @Override
    public void correctingHomework(int StudentID) {
        // 应该是老师类调用的方法,由于学生类实现了接口DoWork就必须实现接口所有的方法,这里只能为空
        
    }

    @Override
    public void attendClass() {
        System.out.println("学生开始上课...");
    }
    
}

老师类需要实现correctingHomework()方法和attendClass()方法,学生类需要实现doHomework()方法和attendClass()方法,但这两个类都有不需要实现的方法在接口中。由于实现了接口必须要实现接口中所有的方法,这些不需要的方法的方法体只能为空,显然这不是一种好的设计。

按照接口隔离原则,对该接口进行拆分成3个接口,如下:

实现代码如下:

//老师接口类
public interface DoWorkT {
    
    // 批改作业
    public void correctingHomework(int studentID);
    
}
//老师、学生公共接口类
public interface DoWorkC {

    // 上课
    public void attendClass();

}
//学生接口类
public interface DoWorkS {

    // 做作业
    public void doHomeWork();

}
//老师类实现工作接口
public class Teacher implements DoWorkT ,DoWorkC{
    private int teacherID;

    @Override
    public void correctingHomework(int StudentID) {
        System.out.println("老师批改作业...");

    }

    @Override
    public void attendClass() {
        System.out.println("老师开始上课...");
    }

}
//学生类实现工作接口
public class Student implements DoWorkS, DoWorkC {
    private int studentID;

    @Override
    public void doHomeWork() {
        System.out.println("学生做作业...");
    }

    @Override
    public void attendClass() {
        System.out.println("学生开始上课...");
    }

}

(3)总结

接口隔离原则包含4层含义:

接口尽量要小;

接口要高内聚(即提高接口、类、模块的处理能力,减少对外的交互,也就是说要有一定的独立处理能力);

定制服务(即单独为一个个体提供优良的服务,比如为一个模块单独设计其接口);

接口设计是有限度的(接口的设计粒度越小,系统越灵活,但同时也带来了结构的复杂化,导致开发难度增加);

ISP的难点在于接口设计的这个“度”没有一个固化或可测量的标准,接口设计一定要注意适度,而这个“度”也只能根据实际情况和经验来进行判断。

5. 迪米特法则(LOD)

(1)概念

迪米特法则又称最少知道原则,定义是:一个对象应该对其他对象有最少的理解,即一个类应该对自己需要耦合或需要调用的类知道的最少。

(2)举例

例A:一个类只能和朋友类交流

老师让班长清点全班人数的类图如下:

实现代码如下:

public class Teacher {

    // 老师下发命令让班长清点学生人数
    public void commond(Monitor monitor) {
        // 初始化学生数量
        List<Student> students = new ArrayList<Student>();
        for (int i = 0; i < 30; i++) {
            students.add(new Student());
        }
        
        //通知班长开始清点人数
        monitor.countStudents(students);
    }

}
public class Monitor {

    // 清点学生人数
    public void countStudents(List<Student> students) {
        System.out.println("学生数量是" + students.size());
    }

}
public class Student {

}
//场景调用类
public class Scene {

    public static void main(String[] args) {
        Teacher teacher = new Teacher();
        teacher.commond(new Monitor());
    }

}

朋友类是这样定义的:出现在成员变量、方法的输入参数中的类称为朋友类,出现在方法体内的类不能称为朋友类。

上例中的Teacher类与Student类不是朋友类,却与一个陌生类Student有了交流,这是违反了LOD的。将List<Student>初始化操作移动到场景类中,同时在Monitor类中注入List<Student>,避免Teacher类对Student类(陌生类)的访问。改进后的类图如下:

实现代码如下:

public class Teacher {

    public void commond(Monitor monitor) {

        // 通知班长开始清点人数
        monitor.countStudents();
    }

}
public class Monitor {
    private List<Student> students;

    // 构造函数注入
    public Monitor(List<Student> students) {
        this.students = students;
    }

    // 清点学生人数
    public void countStudents() {
        System.out.println("学生数量是" + students.size());
    }

}
public class Student {

}
//场景调用类
public class Scene {

    public static void main(String[] args) {
        // 初始化学生数量
        List<Student> students = new ArrayList<Student>();
        for (int i = 0; i < 30; i++) {
            students.add(new Student());
        }

        // 老师下发命令让班长清点学生人数
        Teacher teacher = new Teacher();
        teacher.commond(new Monitor(students));
    }

}

例B:类与类之间的交流也是有距离的

模拟软件安装的向导:第一步,第二步(根据第一步判断是否进行),第三步(根据第二步判断是否进行)...,其类图如下:

实现代码如下:

//安装向导类
public class Wizard {
    // 产生随机数模拟用户的不同选择
    private Random rand = new Random();

    // 第一步
    public int first() {
        System.out.println("安装第一步...");
        // 返回0-99之间的随机数
        return rand.nextInt(100);
    }

    // 第二步
    public int second() {
        System.out.println("安装第二步...");
        return rand.nextInt(100);
    }

    // 第三步
    public int third() {
        System.out.println("安装第三步...");
        return rand.nextInt(100);
    }

}
//安装类
public class InstallSoftware {
    
    public void installWizard(Wizard wizard) {
        int first = wizard.first();
        // 根据第一步返回的数值判断是否执行第二步
        if (first > 50) {
            int second = wizard.second();
            if (second < 50) {
                int third = wizard.third();
            }
        }
    }
    
}
//场景调用类
public class Scene {

    public static void main(String[] args) {
        InstallSoftware install = new InstallSoftware();
        install.installWizard(new Wizard());
    }

}

上例的Wizard类把太多的方法暴露给InstallSoftware类,耦合关系变得异常牢固。如果将Wizard类中的first方法的返回类型由int更改为boolean,随之就需要更改InstallSoftware类了,从而把修改变更的风险扩散开了。根据LOD原则,将Wizard类中的3个public方法修改为private方法,对安装过程封装在一个对外开放的InstallWizard中。对设计进行重构后的类图如下:

实现代码如下:

//安装向导类
public class Wizard {
    // 产生随机数模拟用户的不同选择
    private Random rand = new Random();

    // 第一步
    private int first() {
        System.out.println("安装第一步...");
        // 返回0-99之间的随机数
        return rand.nextInt(100);
    }

    // 第二步
    private int second() {
        System.out.println("安装第二步...");
        return rand.nextInt(100);
    }

    // 第三步
    private int third() {
        System.out.println("安装第三步...");
        return rand.nextInt(100);
    }
    
    //对私有方法进行封装,只对外开放这一个方法
    public void installWizard(){
        int first = this.first();
        // 根据第一步返回的数值判断是否执行第二步
        if (first > 50) {
            int second = this.second();
            if (second < 50) {
                int third = this.third();
            }
        }
    }

}
//安装类
public class InstallSoftware {

    public void installWizard(Wizard wizard) {
        // 直接调用
        wizard.installWizard();
    }

}
//场景调用类
public class Scene {

    public static void main(String[] args) {
        InstallSoftware install = new InstallSoftware();
        install.installWizard(new Wizard());
    }

}

通过这样重构后,类之间的耦合关系变弱。Wizard类只对外公布了一个public方法,即使要修改first()的返回值,影响的也仅仅是Wizard一个类本身,其他类不受任何影响,这体现了该类的高内聚特性。

(3)总结

一个类不要访问陌生类(非朋友类),这样可以降低系统间的耦合,提高了系统的健壮性。

在设计类时应该尽量减少使用public的属性和方法,考虑是否可以修改为private,default,protected等访问权限,是否可以加上final等关键字。

一个类公开的public方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。

6. 开闭原则(OCP)

(1)概念

开闭原则的定义是:软件实体(类、模块、方法)应该对扩展开发,对修改关闭。

即当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

(2)举例

书店刚开始卖小说类书籍,后来要求小说类书籍打折处理(40元以上9折,其他8折),再后来书店增卖计算机类书籍(比小说类书籍多一个属性“类别”)。

书店刚开始卖小说类书籍的类图如下:

实现代码如下:

//书籍接口
public interface IBook {
    // 书籍名称
    public String getName();

    // 书籍售价
    public int getPrice();

    // 书籍作者
    public String getAuthor();
}
//小说类
public class NovelBook implements IBook {
    private String name;
    private int price;
    private String author;

    public NovelBook(String name, int price, String author) {
        this.name = name;
        this.price = price;
        this.author = author;
    }

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

    @Override
    public int getPrice() {
        return this.price;
    }

    @Override
    public String getAuthor() {
        return this.author;
    }

}
//书店售书类
public class BookStore {
    private static List<IBook> books = new ArrayList<IBook>();

    // 静态块初始化数据,在类加载时执行一次,先于构造函数
    // 实际项目中一般由持久层完成
    static {
        // 在非金融类项目中对货币的处理一般取两位精度
        // 通常的设计方法是在运算过程中扩大100倍,在显示时再缩小100倍,以减小精度带来的误差
        books.add(new NovelBook("小说A", 3200, "作者A"));
        books.add(new NovelBook("小说B", 5600, "作者B"));
        books.add(new NovelBook("小说C", 3500, "作者C"));
        books.add(new NovelBook("小说D", 4300, "作者D"));
    }

    // 模拟书店卖书
    public static void main(String[] args) {
        // 设置价格精度
        NumberFormat formatter = NumberFormat.getCurrencyInstance();
        formatter.setMaximumFractionDigits(2);

        // 展示所有书籍信息
        for (IBook book : books) {
            System.out.println("书籍名称:" + book.getName() + "	书籍作者"
                    + book.getAuthor() + "	书籍价格"
                    + formatter.format(book.getPrice() / 100.0) + "元");
        }
    }
}

输出结果如下:

书籍名称:小说A    书籍作者作者A    书籍价格¥32.00元
书籍名称:小说B    书籍作者作者B    书籍价格¥56.00元
书籍名称:小说C    书籍作者作者C    书籍价格¥35.00元
书籍名称:小说D    书籍作者作者D    书籍价格¥43.00元

后来要求小说类书籍打折处理(40元以上9折,其他8折)

如果通过修改接口,在接口上新增加一个方法getOffPrice()专门来处理打折书籍,所有实现类实现该方法。那么与IBook接口相关的类都需要修改。而且作为接口应该是稳定且可靠的,不应经常变化,否则接口作为契约的作用就失去效能了。因此,此方案行不通。

如果修改实现类NovelBook中的方法,直接在getPrice()中实现打折处理,也可以达到预期效果。但采购人员看到的价格是打折后的价格,而看不到原来的价格。

综上,按照OCP原则,应该通过扩展实现变化,增加一个子类OffNovelBook,重写getPrice()方法实现打折处理。改进后的类图如下:

修改后只需要增加一个子类OffNovelBook,修改BookStore类中static静态块中初始化方法即可。

修改代码如下:

//为实现小说打折处理增加的子类
public class OffNovelBook extends NovelBook {

    public OffNovelBook(String name, int price, String author) {
        super(name, price, author);
    }

    // 复写小说价格
    @Override
    public int getPrice() {
        // 获取原价
        int price = super.getPrice();
        // 打折后的处理价
        int offPrice = 0;
        // 如果价格大于40打9折
        if (price > 4000) {
            offPrice = price * 90 / 100;
        } else {
            // 其他打8折
            offPrice = price * 80 / 100;
        }
        return offPrice;
    }
}
//书店售书类
public class BookStore {
    private static List<IBook> books = new ArrayList<IBook>();

    // 静态块初始化数据,在类加载时执行一次,先于构造函数
    // 实际项目中一般由持久层完成
    static {
        // 在非金融类项目中对货币的处理一般取两位精度
        // 通常的设计方法是在运算过程中扩大100倍,在显示时再缩小100倍,以减小精度带来的误差
        books.add(new OffNovelBook("小说A", 3200, "作者A"));
        books.add(new OffNovelBook("小说B", 5600, "作者B"));
        books.add(new OffNovelBook("小说C", 3500, "作者C"));
        books.add(new OffNovelBook("小说D", 4300, "作者D"));
        // 打折处理后只需更改静态块部分即可
    }

    // 模拟书店卖书
    public static void main(String[] args) {
        // 设置价格精度
        NumberFormat formatter = NumberFormat.getCurrencyInstance();
        formatter.setMaximumFractionDigits(2);

        // 展示所有书籍信息
        for (IBook book : books) {
            System.out.println("书籍名称:" + book.getName() + "	书籍作者"
                    + book.getAuthor() + "	书籍价格"
                    + formatter.format(book.getPrice() / 100.0) + "元");
        }
    }
}

打折后的输出结果如下:

书籍名称:小说A    书籍作者作者A    书籍价格¥25.60元
书籍名称:小说B    书籍作者作者B    书籍价格¥50.40元
书籍名称:小说C    书籍作者作者C    书籍价格¥28.00元
书籍名称:小说D    书籍作者作者D    书籍价格¥38.70元

再后来书店增卖计算机类书籍(比小说类书籍多一个属性“类别”)

增加一个IComputerBook接口继承IBook接口,增加一个ComputerBook类实现IComputerBook接口即可,其类图如下:

增加两个类后还需在BookStore类的static静态块中增加初始化数据即可。

修改代码如下:

//增加的计算机书籍接口类
public interface IComputerBook extends IBook {
    // 声明计算机书籍特有的属性-类别
    public String getScope();
}
//增加的计算机书籍实现类
public class ComputerBook implements IComputerBook {
    private String name;
    private int price;
    private String author;
    private String scope;

    public ComputerBook(String name, int price, String author, String scope) {
        this.name = name;
        this.price = price;
        this.author = author;
        this.scope = scope;
    }

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

    @Override
    public int getPrice() {
        return this.price;
    }

    @Override
    public String getAuthor() {
        return this.author;
    }

    @Override
    public String getScope() {
        return this.scope;
    }

}
//书店售书类
public class BookStore {
    private static List<IBook> books = new ArrayList<IBook>();

    // 静态块初始化数据,在类加载时执行一次,先于构造函数
    // 实际项目中一般由持久层完成
    static {
        // 在非金融类项目中对货币的处理一般取两位精度
        // 通常的设计方法是在运算过程中扩大100倍,在显示时再缩小100倍,以减小精度带来的误差
        books.add(new OffNovelBook("小说A", 3200, "作者A"));
        books.add(new OffNovelBook("小说B", 5600, "作者B"));
        books.add(new OffNovelBook("小说C", 3500, "作者C"));
        books.add(new OffNovelBook("小说D", 4300, "作者D"));
        // 打折处理后只需更改静态块部分即可

        // 添加计算机类书籍
        books.add(new ComputerBook("计算机E", 3800, "作者E", "编程"));
        books.add(new ComputerBook("计算机F", 5400, "作者F", "编程"));
    }

    // 模拟书店卖书
    public static void main(String[] args) {
        // 设置价格精度
        NumberFormat formatter = NumberFormat.getCurrencyInstance();
        formatter.setMaximumFractionDigits(2);

        // 展示所有书籍信息
        for (IBook book : books) {
            System.out.println("书籍名称:" + book.getName() + "	书籍作者"
                    + book.getAuthor() + "	书籍价格"
                    + formatter.format(book.getPrice() / 100.0) + "元");
        }
    }
}

增加计算机类书籍后的输出结果如下:

书籍名称:小说A       书籍作者作者A      书籍价格¥25.60元
书籍名称:小说B       书籍作者作者B      书籍价格¥50.40元
书籍名称:小说C       书籍作者作者C      书籍价格¥28.00元
书籍名称:小说D       书籍作者作者D      书籍价格¥38.70元
书籍名称:计算机E     书籍作者作者E      书籍价格¥38.00元
书籍名称:计算机F     书籍作者作者F      书籍价格¥54.00元

(3)总结

在业务规则改变的情况下,高层模块必须有部分改变以适应新业务,但这种改变是很少的,也防止了变化风险的扩散。

开闭原则对测试是非常有利的,只需要测试增加的类即可。若改动原有的代码实现新功能则需要重新进行大量的测试工作(回归测试等)。

开闭原则是面向对象设计中“可复用设计”的基石。

开闭原则是面向对象设计的终极目标,其他原则可以看做是开闭原则的实现方法。

(补充)组合/聚合原则(CARP)

(1)概念

在面向对象的设计中,复用已有的设计或实现有两种方法:继承和聚合/组合。

而继承有一些明显的缺点:继承破坏了封装--基类的实现细节暴露给了子类;基类发生改变,子类随着发生改变;子类继承基类的方法是静态的,不能在运行时发生改变,因此没有足够的灵活性。

组合/聚合原则的定义是:在一个新的对象里使用一些已有的对象,使之成为新对象的一部分。新对象通过调用已有对象的方法来达到复用的目的。

(2)举例

教学管理系统部分数据库访问类设计如下图:

如果需要更换数据库连接方式,如原来采用JDBC连接数据库,现在需要采用数据库连接池进行连接。或者StudentDAO采用JDBC连接,TeacherDAO采用数据库连接池连接。此时则需要增加一个新的DBUtil类,并修改StudentDAO类和TeacherDAO类的源代码,违反了开闭原则。

现使用组合/聚合原则对其进行重构如下:

此时若需要增加新的数据库连接方式,再增加一个DBUtil的子类即可:

(3)总结

当要复用代码时首先想到使用组合/聚合的方式,其次才是使用继承的方法。

只有“Is-A”关系才符合继承关系,“Has-A”关系应当使用聚合来描述(”Is-A”代表一个类是另外一个类的一种(包含关系),而“Has-A”代表一个类是另外一个类的一个部分(属于关系))。

6大设计原则详解(一):http://www.cnblogs.com/LangZXG/p/6242925.html

6大设计原则,与常见设计模式(概述):http://www.cnblogs.com/LangZXG/p/6204142.html

类图基础知识:http://www.cnblogs.com/LangZXG/p/6208716.html

注:转载请注明出处   http://www.cnblogs.com/LangZXG/p/6242927.html

原文地址:https://www.cnblogs.com/LangZXG/p/6242927.html