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