zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    所有选择器
    display:block、display:inline与displayinline:block的概念和区别
    jQuery 选择器
    JS日历制作获取时间
    HTML DOM 事件
    访问HTML元素(节点)
    HTML常用标签
    flask+mysql的Docker 部署
    docker(三)
    flask如何部署
  • 原文地址:https://www.cnblogs.com/LangZXG/p/6242927.html
Copyright © 2011-2022 走看看