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)
迪米特法则又称最少知道原则,定义是:一个对象应该对其他对象有最少的理解,即一个类应该对自己需要耦合或需要调用的类知道的最少。
例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一个类本身,其他类不受任何影响,这体现了该类的高内聚特性。
一个类不要访问陌生类(非朋友类),这样可以降低系统间的耦合,提高了系统的健壮性。
在设计类时应该尽量减少使用public的属性和方法,考虑是否可以修改为private,default,protected等访问权限,是否可以加上final等关键字。
一个类公开的public方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。
6. 开闭原则(OCP)
开闭原则的定义是:软件实体(类、模块、方法)应该对扩展开发,对修改关闭。
即当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
书店刚开始卖小说类书籍,后来要求小说类书籍打折处理(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() + "\t书籍作者"
+ book.getAuthor() + "\t书籍价格"
+ 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() + "\t书籍作者"
+ book.getAuthor() + "\t书籍价格"
+ 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() + "\t书籍作者"
+ book.getAuthor() + "\t书籍价格"
+ 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元
在业务规则改变的情况下,高层模块必须有部分改变以适应新业务,但这种改变是很少的,也防止了变化风险的扩散。
开闭原则对测试是非常有利的,只需要测试增加的类即可。若改动原有的代码实现新功能则需要重新进行大量的测试工作(回归测试等)。
开闭原则是面向对象设计中“可复用设计”的基石。
开闭原则是面向对象设计的终极目标,其他原则可以看做是开闭原则的实现方法。
(补充)组合/聚合原则(CARP)
在面向对象的设计中,复用已有的设计或实现有两种方法:继承和聚合/组合。
而继承有一些明显的缺点:继承破坏了封装--基类的实现细节暴露给了子类;基类发生改变,子类随着发生改变;子类继承基类的方法是静态的,不能在运行时发生改变,因此没有足够的灵活性。
组合/聚合原则的定义是:在一个新的对象里使用一些已有的对象,使之成为新对象的一部分。新对象通过调用已有对象的方法来达到复用的目的。
教学管理系统部分数据库访问类设计如下图:
如果需要更换数据库连接方式,如原来采用JDBC连接数据库,现在需要采用数据库连接池进行连接。或者StudentDAO采用JDBC连接,TeacherDAO采用数据库连接池连接。此时则需要增加一个新的DBUtil类,并修改StudentDAO类和TeacherDAO类的源代码,违反了开闭原则。
现使用组合/聚合原则对其进行重构如下:
此时若需要增加新的数据库连接方式,再增加一个DBUtil的子类即可:
当要复用代码时首先想到使用组合/聚合的方式,其次才是使用继承的方法。
只有“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