赞
踩
迪米特法则(Law of Demoter, LoD) 也称为最少知识原则(Least Knowledge Principle,LKP),虽然名字不同,但描述的是同一个规则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没有关系,那是你的事情,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不关心。
1. 迪米特法则还有一个英文解释是:Only talk to your immedate friends(只与直接的朋友通信。)
什么叫做直接的朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象都必要会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,
例如:组合、聚合、依赖等。下面举例说明如何才能做到只与直接的朋友交流。
传说中有这样一个故事,老师想让体育委员确认一下全班女生来齐没有,就对他说: “你去把全班女生清一下。” 体育委员没听清楚,就问道: “呀,…那亲哪个?” 老师无语了,我们来看这个笑话怎么用程序来实现,类图如下所示:
Teacher类的commond方法负责发送命令给体育会员,命令他清点女生,其实现过程如代码清单所示:
老师类
public class Teacher {
//老师对学生发布命令,清一下女生
public void commond(GroupLeader groupLeader){
List<Girl> listGirls = new ArrayList<>();
//初始化女生
for (int i = 0; i < 20; i++) {
listGirls.add(new Girl());
}
//告诉体育委员开始执行清查任务
groupLeader.countGirls(listGirls);
}
}
老师只有一个方法commond,先定义出所有的女生,然后发布命令给体育委员,去清点一下女生的数量。体育委员GroupLeader 的实现过程如代码清单:
体育委员类实现过程
public class GroupLeader {
public void countGirls(List<Girl> listGirls){
System.out.println("女生数量是:" + listGirls.size());
}
}
女生类
public class Girl {
}
故事中的三个角色都已经有了,再定义一个场景类来描述这个故事,其实现过程如下:
public class Client {
public static void main(String[] args) {
Teacher teacher = new Teacher();
//老师发布命令
teacher.commond(new GroupLeader());
}
}
运行结果如下:
女生数量是:20
体育委员按照老师的要求对女生进行了清单,并得出数量。我们回过头来思考一下这个程序有什么问题,首先确定Teacher类有几个朋友类,它仅有一个朋友类 - GroupLeader。为什么Girl不是朋友类呢?Teacher也对它产生了依赖关系呀!朋友类的定义是这样的:出现在成员变量、方法的输入输出参数中的类称为朋友类,而出现在方法体内部的类不属于朋友类,而Girl这个类就是出现在commond方法tinei-,因此不属于Teacher类的朋友类。
迪米特法则告诉我们定义的commond方法却与Girl类有了交流,声明了一个List动态数组,也就是与一个陌生的类Girl有了交流,这样就破坏了Teacher的健壮性。方法是类的一个行为,类竟然不知道自己的行为与其他类产生依赖关系,这是不允许的,严重违法了迪卡特法则。
问题已经发现,我们修改一下程序,讲类图稍作修改,在类图中去掉Teacher对Girl类的依赖关系,修改后的Teacher类对代码清单如下所示。
修改后的老师类:
public class Teachers {
//老师对学生发布命令,清一下女生
public void commond(GroupLeaders groupLeader){
//告诉体育委员开始执行清查任务
groupLeader.countGirls();
}
}
修改后的GroupLeaders类代码如下:
public class GroupLeaders {
private List<Girl> listGirls;
public GroupLeaders(List<Girl> _listGirls){
this.listGirls = _listGirls;
}
public void countGirls(){
System.out.println("女生数量是:" + this.listGirls.size());
}
}
修改后的体育委员类
public class GroupLeaders {
private List<Girl> listGirls;
public GroupLeaders(List<Girl> _listGirls){
this.listGirls = _listGirls;
}
public void countGirls(){
System.out.println("女生数量是:" + this.listGirls.size());
}
}
在GroupLeaders类中定义了一个构造函数,通过构造函数传递了依赖关系。同时,对场景类也进行了一些修改,如代码清单:
修改后的场景类
public class Client1 { public static void main(String[] args) { //产生一个女生群体 List<Girl> listGirls = new ArrayList<>(); //初始化女生 for (int i = 0; i < 20; i++) { listGirls.add(new Girl()); } Teachers teacher = new Teachers(); //老师发布命令 teacher.commond(new GroupLeaders(listGirls)); } }
对程序进行了简单的修改,把Teacher中对List的初始化移动到了场景类中,同时在GroupLeaders中增加了对Girl的注入,避开了Teacher类对陌生类Girl的访问,降低了系统间的耦合,提高了系统的键壮性。
注意:一个类只和朋友交流,不与陌生类交流,不要出getA().getB().getC()这种情况,类与类之间的关系是建立在类间的,而不是方法间,因此一个方法尽量不引入一个类中不存在的对象,当然,JDK API提供的类除外
人与人之间是有距离的,太远关系逐渐疏远,最终形同陌路;太近就相互刺伤。对朋友关系描述最贴切的故事就是:两只刺猬取暖,太远取不到暖,太近刺伤了对方,必须保持一个既能取暖又不刺伤对方的距离。迪米特法则就是对这个距离进行描述,即使是朋友类之间也不能无话不说,无所不知。
我们在安装软件的时候,经常会有一个导向动作,第一步是确认是否安装,第二步确认License,再然后选择安装目录…这是一个典型的顺序执行动作,具体到程序中就是:调用一个或多个类,先执行第一个方法,然后是第二个方法,根据返回结果再来看是否可以调用第三个方法,或者第四个方法,等等,其类图如下:
导向类:
public class Wizard { private Random rand = new Random(System.currentTimeMillis()); //第一步 public int first() { System.out.println("执行第一个方法...."); return rand.nextInt(100); } //第二步 public int second() { System.out.println("执行第二个方法..."); return rand.nextInt(100); } //第三步 public int third() { System.out.println("执行第三个方法..."); return rand.nextInt(100); } }
在Wizard类中分别定义了三个步骤方法,每个步骤中都有相关的业务逻辑完成指定的任务,我们使用一个随机函数来代替业务执行的返回值。软件安装InstallSoftWare类如下:
InstallSoftWare类:
public class InstallSoftWare { private int third; public void installSoftWare(Wizard wizard) { int first = wizard.first(); //根据first返回的结果,看是否需要执行second if(first > 50) { int second = wizard.second(); if(second > 50) { int third = wizard.third(); if(third > 50) { wizard.first(); } } } } }
根据每个方法执行的结果决定是否继续执行下一个方法,模拟人工的选择操作。场景类如代码清单所示。
场景类
public class Client {
public static void main(String[] args) {
InstallSoftWare install = new InstallSoftWare();
install.installSoftWare(new Wizard());
}
}
运行结果如下:
执行第一个方法....
执行第二个方法...
执行第三个方法...
执行第一个方法....
程序虽然简单,但是隐藏的问题可不简单,思考一下程序有什么问题。Wizard类把太多的方法暴露给InstallSoftWare类,两者的朋友关系太亲密,耦合关系变得异常牢固。如果要将Wizard类中的first方法返回值的类型由int改为boolean,就需要修改InstallSoftware类,从而把修改变更的风险kuosan-开了。因此,这样的耦合是极度不合适的,我们需要对设计进行重构,重构后的类图如下:
在Wizard类中增加一个installWizard方法,对安装过程进行封装,同时把原有的三个public方法修改为private方法,如代码清单所示:
修改后的导向类实现过程:
public class Wizards { private Random rand = new Random(System.currentTimeMillis()); //第一步 private int first() { System.out.println("执行第一个方法...."); 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(); //根据first返回的结果,看是否需要执行second if(first > 50) { int second = this.second(); if(second > 50) { int third = this.third(); if(third > 50) { this.first(); } } } } }
将三个步骤的访问权限修改为private,同时把InstallSoftWare中的方法installSoftWare移动到Wizard方法中。通过这样的重构后,Wizard类就只对外公布了一个public方法,即使要修改first方法的返回值,影响的也仅仅只是Wizard本身,其他类不受影响,这显示了类的高内聚特性。
对InstallSoftWare类进行少量的修改,如代码清单所示。
public class InstallSoftWares {
public void installSoftWare(Wizards wizard) {
wizard.installWizard();
}
}
场景类:
public class Client1 {
public static void main(String[] args) {
InstallSoftWares install = new InstallSoftWares();
install.installSoftWare(new Wizards());
}
}
运行结果如下:
执行第一个方法....
执行第二个方法...
执行第三个方法...
一个类公开的public属性或方法越多,修改时涉及的面也就越大,变更引起的风险扩散也就越大。 因此,为了保持朋友类间的距离,在设计时需要反复衡量:是否还可以减少public方法和属性,是否可以修改为private、package - private (包类型,在类、方法、变量前不加访问权限,则默认认为包类型)、protected等访问权限,是否可以加上final关键字等。
注意:迪米特法则要求类 “羞涩” 一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、package-private、protected等访问权限。
是自己的就是自己的,在实际应用中经常会出现这样一个方法:放在本类中也可以,放在其他类中也没有错,那怎么去衡量呢?你可以坚持这样一个原则:如果一个方法放在本类中,即不增加类间关系,也对本类不产生负面影响,就放置在本类中。
谨慎使用Serializable
在实际应用中,这个问题是很少出现的,即使出现也会立即被发现并得到解决。是怎么回事呢?举个例子来说,在一个项目中使用RMI(Remote Method Invocation,远程方法调用) 方式传递一个VO(Value Object,值对象), 这个对象就必须实现Serializable接口(仅仅是一个标志性的接口,不需要实现具体的方法),也就是把需要网络传输的对象进行序列化,否则就会出现NotSerializableException异常。 突然有一天,客户端的VO修改了一个属性的访问权限,从private变更为public,访问权限扩大了,如果服务器上没有做出相应的变更,就会报序列化失败,就这么简单。
迪卡特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。 其要求的结果就是产生了大量的中转或跳转类,导致系统的复用性提高,同时也为维护带来了难度。读者在采用迪米特法则时需要反复权衡,既做到让结构清晰,又做到高内聚、低耦合。
在哲学上,矛盾法则即对立统一的法则,是唯物辩证法的最基本法则。本章要讲的开闭原则是不是也有同样的重要性且具有普遍性呢?确定,开闭原则是Java世界里最基础的设计原则,它指导我们如何建立一个稳定的、灵活的系统,先来看开闭原则的定义,(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。)
我们做一件事情,或者选择一个方向,一般需要经历三个步骤:What一是什么,Why一为什么,How一取最后一个w。对于开闭原则,我们也采用这三步来分析,即什么是开闭原则,为什么要使用开闭原则,怎么使用开闭原则。
开闭原则的定义已经非常明确地告诉我们:软件实体应该对扩展开放,对修改关闭,其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。那什么又是软件实体呢?软实体包括以下几个部分:
项目或软件产品中按照一定的逻辑规划的模块
抽象和类
方法
一个软件产品只要在生命期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计时尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现 “拥抱变化”。开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来完成变化,它是为软件实体的未来事件而制定的对现行开放设计进行约束的一个原则。
我们举例说明什么是开闭原则,以书店销售书籍为例,其类图如图
IBook定义了数据的三个属性:名称、价格和作者。小说类NovelBook是一个具体的实现类,是所有小说书籍的总称,BookStore指的是书店,IBook接口如代码清单所示。
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; } }
注意:我们把价格定义为int类型并不是错误,在非金融类项目中对货币处理时,一般取2位精度,通常的设计方法是在运算过程中扩大100倍,在需要展示时再缩小100倍,减少精度带来的误差。
书店售书的过程如代码清单:
public class BookStore { private final static ArrayList<IBook> bookList = new ArrayList<IBook>(); //static静态模块初始化数据,实际项目中一般是由持久层完成 static { bookList.add(new NovelBook("天龙八部",3200,"金庸")); bookList.add(new NovelBook("巴黎圣母院",5600,"雨果")); bookList.add(new NovelBook("悲惨世界",3500,"雨果")); bookList.add(new NovelBook("金瓶梅",4300,"兰陵笑笑生")); } //模拟书店买书 public static void main(String[] args) { NumberFormat formatter = NumberFormat.getCurrencyInstance(); formatter.setMaximumFractionDigits(2); System.out.println("-----------书店卖出去的书籍记录如下:---------"); for (IBook book : bookList) { System.out.println("书籍名称:" + book.getName() + "\t书籍作者:" + book.getAuthor() + "\t书籍价格:" + formatter.format(book.getPrice()/ 100) + "元"); } } }
在BookStore中声明了一个静态模块,实现了数据的初始化,这部分应该是从持久层产生的,由持久层框架管理,运行结果如下:
-----------书店卖出去的书籍记录如下:---------
书籍名称:天龙八部 书籍作者:金庸 书籍价格:¥32.00元
书籍名称:巴黎圣母院 书籍作者:雨果 书籍价格:¥56.00元
书籍名称:悲惨世界 书籍作者:雨果 书籍价格:¥35.00元
书籍名称:金瓶梅 书籍作者:兰陵笑笑生 书籍价格:¥43.00元
项目投产了,书籍正常销售出去,书店也盈利了。从2008年开始,全球经济开始下滑,对零售业影响比较大,书店为了生存开始打折销售:所有40元以上的书籍9折销售,其他的8折销售。对已经投产的项目来说,这就是一个变化,我们应该如何应对这样一个需求变化?
通过扩展实现变化,增加一个子类OffNovelBook,覆写getPrice方法,高层次的模块(也就是static静态模块区)通过OffNoveIBook类产生新的对象,完成业务变化对系统的最小化开发。修改后的类图如下:
OffNovelBook类继承了NovelBook,并覆写了getPrice方法,不修改原有的代码。新增加的子类OffNovelBook如代码清单:
public class OffNovelBook extends NovelBook{ public OffNovelBook(String _name, int _price, String _author) { super(_name, _price, _author); } @Override public int getPrice() { //原价 int selfPrice = super.getPrice(); int offPrice = 0; if(selfPrice > 4000) { //原价大于40,则打9折 offPrice = selfPrice * 90 /100; } else { offPrice = selfPrice * 80 / 100; } return offPrice; } }
书店类BookStore需要依赖子类,代码稍作修改,如代码清单:
public class BookStore1 { private final static ArrayList<IBook> bookList = new ArrayList<IBook>(); //static静态模块初始化数据,实际项目中一般是由持久层完成 static { bookList.add(new OffNovelBook("天龙八部",3200,"金庸")); bookList.add(new OffNovelBook("巴黎圣母院",5600,"雨果")); bookList.add(new OffNovelBook("悲惨世界",3500,"雨果")); bookList.add(new OffNovelBook("金瓶梅",4300,"兰陵笑笑生")); } //模拟书店买书 public static void main(String[] args) { NumberFormat formatter = NumberFormat.getCurrencyInstance(); formatter.setMaximumFractionDigits(2); System.out.println("-----------书店卖出去的书籍记录如下:---------"); for (IBook book : bookList) { System.out.println("书籍名称:" + book.getName() + "\t书籍作者:" + book.getAuthor() + "\t书籍价格:" + formatter.format(book.getPrice()/ 100) + "元"); } } }
运行结果如下:
-----------书店卖出去的书籍记录如下:---------
书籍名称:天龙八部 书籍作者:金庸 书籍价格:¥25.00元
书籍名称:巴黎圣母院 书籍作者:雨果 书籍价格:¥50.00元
书籍名称:悲惨世界 书籍作者:雨果 书籍价格:¥28.00元
书籍名称:金瓶梅 书籍作者:兰陵笑笑生 书籍价格:¥38.00元
OK,打折销售开发完成了。看到这里,各位可能有想法了:增加了一个OffNoveBook类后,你的业务逻辑还是修改了,你修改了static静态模块区域,这部分确实修改了,该部分属于高层次的模块,是由持久层产生的,在业务规则改变的情况下高层模块必须有部分改变以适应新业务,改变要尽量地少,防止变化风险的扩散。
注意:开闭原则对外扩展开放,对修改关闭,并不意味着不做任何修改,底层模块的变更,必然要有高层模块进行耦合,否则就是一个独立无意义的代码片段。
这里可以把变化归纳为以下三种类型:
逻辑变化
子模块变化
可视化视图变化
开闭原则是最基础的一个原则,前五个原则都是开闭原则的具体形态,也就是说前五个原则就是指导设计的工具和方法,而开闭原则才是其精神领袖。
开闭原则对测试的影响,小说类的单元测试如下:
public class NovelBookTest extends TestCase {
private String name = "平凡的世界";
private int price = 6000;
private String author = "路遥";
private IBook novelBook = new NovelBook(name,price,author);
public void testGetPrice() {
assertEquals(this.price, novelBook.getPrice());
}
}
绿条显示通过
通过扩展来实现业务逻辑的变化,而不是修改,OffNoveIBookTest代码清单如下
打折销售的小说类单元测试
public class OffNovelBookTest extends TestCase {
private IBook below40NovelBook = new OffNovelBook("平凡的世界",3000,"路遥");
private IBook above40NovelBook = new OffNovelBook("平凡的世界",6000,"路遥");
//测试低于40元的数据是否打8折
public void testGetPriceBelow40() {
assertEquals(2400,this.below40NovelBook.getPrice());
}
//测试大于40元的数据是否打9折
public void testGetPriceAbove40() {
assertEquals(5400,this.above40NovelBook.getPrice());
}
}
运行均显示绿条通过
开闭原则是一个非常虚的原则前面5个原则是对开闭原则的具体解释,但是开闭原则并不局限于这么多,它 “虚” 得没有边界,就像 “好好学习,天天向上” 的口号一样,告诉我们要好好学习,但是学什么,怎么学并没有告诉我们,需要去体会和掌握,开闭原则也是一个口号,那我们怎么把这个口号应用到实际工作中呢?
1. 抽象约束
抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:第一,通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;第三,抽象层尽量保持稳定,一旦确定即不允许修改
还是以书籍做实例,类图如下:
增加了一个接口IComputerBook和实现类Computer-Book,而BookStore不用做任何修改就可以完成书店销售计算机书籍的业务。计算机书籍接口代码如下:
public interface IComputerBook extends IBook{
//计算机书籍是有一个范围
public String getScope();
}
计算机书籍增加了一个方法,就是获得该书籍的范围,同时继承IBook接口,毕竟计算机书籍也是书籍,其实现代码清单如下:
public class ComputerBook implements IComputerBook { private String name; private String scope; private String author; private int price; public ComputerBook(String _name,String _scope,String _author,int _price) { this.name = _name; this.scope = _scope; this.author = _author; this.price = _price; } @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; } }
BookStore类没有做任何的修改,只是往static静态模块中增加一条数据,如代码清单所示:
public class BookStore1 { private final static ArrayList<IBook> bookList = new ArrayList<IBook>(); //static静态模块初始化数据,实际项目中一般是由持久层完成 static { bookList.add(new OffNovelBook("天龙八部",3200,"金庸")); bookList.add(new OffNovelBook("巴黎圣母院",5600,"雨果")); bookList.add(new OffNovelBook("悲惨世界",3500,"雨果")); bookList.add(new OffNovelBook("金瓶梅",4300,"兰陵笑笑生")); bookList.add(new ComputerBook("Think in Java","编程语言","Bruce Eckel",4300)); } //模拟书店买书 public static void main(String[] args) { NumberFormat formatter = NumberFormat.getCurrencyInstance(); formatter.setMaximumFractionDigits(2); System.out.println("-----------书店卖出去的书籍记录如下:---------"); for (IBook book : bookList) { System.out.println("书籍名称:" + book.getName() + "\t书籍作者:" + book.getAuthor() + "\t书籍价格:" + formatter.format(book.getPrice()/ 100) + "元"); } } }
运行结果:
-----------书店卖出去的书籍记录如下:---------
书籍名称:天龙八部 书籍作者:金庸 书籍价格:¥25.00元
书籍名称:巴黎圣母院 书籍作者:雨果 书籍价格:¥50.00元
书籍名称:悲惨世界 书籍作者:雨果 书籍价格:¥28.00元
书籍名称:金瓶梅 书籍作者:兰陵笑笑生 书籍价格:¥38.00元
书籍名称:Think in Java 书籍作者:Bruce Eckel 书籍价格:¥43.00元
这样做的好处是只需要往原来业务添砖加瓦,不需要做任何改变。
设计原则:
Single Responsibility Principle:单一职责原则
Open Closed Principle:开闭原则
Liskov Substitution Principle:里氏替换原则
Law of Demeter:迪米特法则
Interface Segregation Principle:接口隔离原则
Dependence Inversion Principle:依赖倒置原则
把这6个原则的首字母(里氏替换原则和迪米特法则的首字母重复,只取一个)联合起来就是SOLID(solid,稳定的),其代表的含义也就是把这6个原则结合使用的好处:建立稳定、灵活、健壮的设计,而开闭原则又是重中之重,是最基础的原则,是其他5大原则的精神领袖。我们在使用开闭原则要注意以下几个问题:
开闭原则也只是一个原则
项目规章非常重要
预知变化
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。