6大设计原则
第4章 接口隔离原则(用的上的功能才是最好的,用不上的功能,别往脸上贴)
我的理解:
接口隔离就是,在没有违背一个接口只有一个职责的情况下,竟可能少的去,扩展接口的方法
,做的,调用那些方法,我就封装那些方法成为一个单一的纯净的接口
接口的分为两类
● 实例接口
在Java中声明一个类,然后用new关键字产生一个实 例,它是对一个类型的事物的描述,这是一种接口 比如你定义Person这个类,然后使用 Person zhangSan=new Person()产生了一个实例,这个实例要遵从的标准就是Person这个 类,此时这个类就变成了,我们所想到的接口
● 类接口(Class Interface),Java中经常使用的interface关键字定义的接口。
原则:建立单一接口,不要建立臃肿庞大的接口。再通 俗一点讲:接口尽量细化,同时接口中的方法尽量少。
接下来我们来找找谁是美女,美女何其多,观点各不同
把原IPettyGirl接口拆分为两个接口,一种是外形美的美女IGoodBodyGirl,这类美女的特 点就是脸蛋和身材极棒,超一流;另 外一种是气质美的美女IGreatTemperamentGirl,谈吐和修养都非常高。
定义美女类型的接口
代码清单4-6 两种类型的美女定义
public interface IGoodBodyGirl {
//要有姣好的面孔
public void goodLooking();
//要有好身材
public void niceFigure();
}
public interface IGreatTemperamentGirl {
//要有气质
public void greatTemperament();
}
具体的实现
public class PettyGirl implements IGoodBodyGirl,IGreatTemperamentGirl {
private String name;
//美女都有名字
public PettyGirl(String _name){
this.name=_name;
}
//脸蛋漂亮
public void goodLooking() {
System.out.println(this.name + "---脸蛋很漂亮!");
}
//气质要好
public void greatTemperament() {
System.out.println(this.name + "---气质非常好!");
}
//身材要好
public void niceFigure() {
System.out.println(this.name + "---身材非常棒!");
}
}
通过这样的重构以后,不管以后是要气质美女还是要外形美女,都可以保持接口的稳 定。
以上把一个臃肿的接口变更为两个独立的接口所依赖的原则就是接口隔离原则,可以预防未来变更的扩散,提高系统的灵活性和可维 护性。
注意:
保证接口的纯洁性
● 接口要尽量小 这是接口隔离原则的核心定义,不出现臃肿的接口(Fat Interface),但是“小”是有限度 的,首先就是不能违反单一职责原则,不能为了划分小,把本来是同一个功能的接口给分出去了。
● 接口要高内聚
要求在接口中尽量 少公布public方法,接口是对外的承诺,这样有很多的风险,内容能处理就处理了。
规则去衡量
● 一个接口只服务于一个子模块或业务逻辑;
● 通过业务逻辑压缩接口中的public方法,接口时常去回顾,
● 已经被污染了的接口,尽量去修改,若变更的风险较大。
第5章 迪米特法则(和自己熟悉的朋友交流,不熟悉的不去说话)
我的理解:
知道最少原则,就是,一个类中我只使用你的功能,内部子再复杂和我没有关系,尽量不要出现我本类不认识的,其它类,这是不允许的
在一个方法里面竟然有我这类里面不认识的东西,这不是很可怕的事情,我创造的类对象,我肯定知道的,而且是完全了解,没有那一块我不认识的。
讲一个故事
传说中有这样一个故事,老师想让体育委员确认一下全班女生来齐没有,就对他 说:“你去把全班女生清一下。”体育委员没听清楚,就问道:“呀,……那亲哪个?”老师无 语了,我们来看这个笑话怎么用程序来实现。
首先我们来了解一下,什么叫做,朋友类
朋友类的定义是 这样的:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法体内 部的类不属于朋友类。
老师类
public class Teacher {
//老师对学生发布命令,清一下女生
public void commond(GroupLeader groupLeader){
//告诉体育委员开始执行清查任务
groupLeader.countGirls();
}
}
体委类
public class GroupLeader {
private List<Girl> listGirls;
//传递全班的女生进来
public GroupLeader(List<Girl> _listGirls){
this.listGirls = _listGirls;
}
//清查女生数量
public void countGirls(){
System.out.println("女生数量是:"+this.listGirls.size());
}
}
场景类
public class Client {
public static void main(String[] args) {
//产生一个女生群体
List<Girl> listGirls = new ArrayList<Girl>();
//初始化女生
for(int i=0;i<20;i++){
listGirls.add(new Girl());
}
Teacher teacher= new Teacher();
//老师发布命令
teacher.commond(new GroupLeader(listGirls));
}
}
对程序进行了简单的修改,把Teacher中对List的初始化移动到了场景类中,同时 在GroupLeader中增加了对Girl的注入,避开了Teacher类对陌生类Girl的访问,降低了系统间 的耦合,提高了系统的健壮性
是自己的就是自己的
在实际应用中经常会出现这样一个方法:放在本类中也可以,放在其他类中也没有错, 那怎么去衡量呢?你可以坚持这样一个原则:如果一个方法放在本类中,既不增加类间关 系,也对本类不产生负面影响,那就放置在本类中。
第6章 开闭原则(你可以扩展我,但是不能修改我)
我们先来看看它的定义是啥子
软件实体应该对扩展开放,对修改关闭。是不是太抽象了,那有人就要问了,怎么扩展开发,对内修改关闭如何实现。
我的的理解:
对使用来说,你只能对我的程序进行子在原有的基础上进行扩展,不能修改。
在Java的编程世界里面,有哥们就可能会说了继承不就是很好的例子,我继承了我不久可以在不动弹你的东西再去扩展我自己的,这个思路大体是正确的,但是存在漏洞,继承谁,怎么继承,这就是我们接下来需要思考的问题了。
我们要在不看父类源代码的基础上去"实现继承",有小伙伴就要问了,我都不知道你是啥怎么继承,
Java中给的解决办法就是抽象(接口和抽象类),把需要继承的东西抽象出来供给不同的类去扩展,尽量不要修改父类的方法,避免逻辑错误。
还有一种就是通过配置文件的形式读取,我需要的是去扩展子配置文件就可以,从而达到,扩展功能的效果。
对内修改关闭,什么意思呢,其实就是把自个本身的内部东西换成私有的,只能自己的访问,对外不开放就可呢,别动不动就动我的东西,这样可以在原有的基础,保存系统更加健壮,不易崩溃。
举个小栗子
以书店销售书籍为例,其类图如图6-1所示
IBook定义了数据的三个属性:名称、价格和作者。小说类NovelBook是一个具体的实现 类,是所有小说书籍的总称,BookStore指的是书店,IBook接口如代码清单6-1所示
代码清单6-1 书籍接口
public interface IBook {
//书籍有名称
public String getName();
//书籍有售价
public int getPrice();
//书籍有作者
public String getAuthor();
}
目前书店只出售小说类书籍,小说类如代码清单6-2所示。
代码清单6-2 小说类
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;
}
//获得作者是谁
public String getAuthor() {
return this.author;
}
//书籍叫什么名字
public String getName() {
return this.name;
}
//获得书籍的价格
public int getPrice() {
return this.price;
}
}
书店售书的过程如代码清单6-3所示。 代码清单6-3 书店售书类
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.0)+"元");
}
}
}
项目投产了,书籍正常销售出去,书店也赢利了。从2008年开始,全球经济开始下滑, 对零售业影响比较大,书店为了生存开始打折销售:所有40元以上的书籍9折销售,其他的8 折销售。对已经投产的项目来说,这就是一个变化。我们就要去修改价格,
我们就想到了一下解决方法
● 修改接口
在IBook上新增加一个方法getOffPrice(),专门用于进行打折处理,所有的实现类实现该 方法。
但是这样修改的后果就是,实现类NovelBook要修改,BookStore中的main方法也修 改,同时IBook作为接口应该是稳定且可靠的,不应该经常发生变化,否则接口作为契约的 作用就失去了效能。
你总不能一直变化,一变化,它的实现类都要改代码因此,该方案否定。
● 修改实现类
修改NovelBook类中的方法,直接在getPrice()中实现打折处理,好办法,我相信大家在 项目中经常使用的就是这样的办法,通过class文件替换的方式可以完成部分业务变化(或是 缺陷修复)。
我可以添加一个实现类的方法不久行了,但是有缺点,就是例如采购书籍人员也是要看价格的,由于该方法已 经实现了打折处理价格,因此采购人员看到的也是打折后的价格,看不到原来的价格了。
● 通过扩展实现变化(推荐使用这锅)
增加一个子类OffNovelBook,覆写getPrice方法,高层次的模块(也就是static静态模块 区)通过OffNovelBook类产生新的对象,完成业务变化对系统的最小化开发。好办法,修改 也少,风险也小,修改后的类图如图6-2所示。
OffNovelBook类继承了NovelBook,并覆写了getPrice方法,不修改原有的代码。新增加 的子类OffNovelBook如代码清单6-4所示。
代码清单6-4 打折销售的小说类
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;
}
}
很简单,仅仅覆写了getPrice方法,通过扩展完成了新增加的业务。书店类BookStore需 要依赖子类,代码稍作修改,不需要更改很多,我只是在原有的基础上扩展了我的方法,原来的方法价格还是可以通过父类来访问到的,如代码清单6-5所示。
代码清单6-5 书店打折销售类
public class BookStore {
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.0)+"元");
}
}
}
OK,打折销售开发完成了。看到这里,各位可能有想法了:增加了一个OffNoveBook类 后,你的业务逻辑还是修改了,你修改了static静态模块区域。这部分确实修改了,该部分属 于高层次的模块,是由持久层产生的,在业务规则改变的情况下高层模块必须有部分改变以 适应新业务,改变要尽量地少,防止变化风险的扩散。
注意 :开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变 更,就很有可能修改高层模块的东西,如果都没有联系,它不就是以孤立的代码片段了。
如何来划分程序的变化
我们可以把变化归纳为以下三种类型:
● 逻辑变化
只变化一个逻辑,而不涉及其他模块,比如原有的一个算法是ab+c,现在需要修改为 ab*c,可以通过修改原有类中的方法的方式来完成。
● 子模块变化
一个模块变化,会对其他的模块产生影响,特别是一个低层次的模块变化必然引起高层 模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的,刚刚的书籍打折处 理就是类似的处理模块,该部分的变化甚至会引起界面的变化。
我们来总结一下上面的买书问题
假设一个需求变化,我们通过扩展一个 子类拥抱了变化;最后把子类投入运行环境中,新逻辑正式投产,运行。通过分析,我们发现并没 有修改原有的模块代码,IBook接口没有改变,NovelBook类没有改变,这属于已有的业务代 码,我们保持了历史的纯洁性。
如何使用开闭原则
- 抽象约束
通过接口或抽象类可以约束一组可能变化的行为,并且 能够实现对扩展开放。
包含三层含义:第一,通过接口或抽象类约束扩展,对扩展进行边 界限定,不允许出现在接口或抽象类中不存在的public方法;
第二,参数类型、引用对象尽 量使用接口或者抽象类,而不是实现类;
第三,抽象层尽量保持稳定,一旦确定即不允许修 改。
例子还是以书店为例,目前只是销售小说类书籍,单一经营毕竟是有风险的,于是书店新增 加了计算机书籍,它不仅包含书籍名称、作者、价格等信息,还有一个独特的属性范围。
增加了一个接口IComputerBook和实现类Computer- Book,而BookStore不用做任何修改就 可以完成书店销售计算机书籍的业务。计算机书籍接口如代码清单6-8所示。
代码清单6-8 计算机书籍接口
public interface IComputerBook extends IBook{
//计算机书籍是有一个范围
public String getScope();
}
很简单,计算机书籍增加了一个方法,就是获得该书籍的范围,同时继承IBook接口, 毕竟计算机书籍也是书籍,其实现如代码清单6-9所示。
public class ComputerBook implements IComputerBook {
private String name;
private String scope;
private String author;
private int price;
public ComputerBook(String _name,int _price,String _author,String _scope){
this.name=_name;
this.price = _price;
this.author = _author;
this.scope = _scope;
}
public String getScope() {
return this.scope;
}
public String getAuthor() {
return this.author;
}
public String getName() {
return this.name;
}
public int getPrice() {
return this.price;
}
}
这也很简单,实现IComputerBook就可以,而BookStore类没有做任何的修改,只是在 static静态模块中增加一条数据,如代码清单6-10所示。
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,"兰陵笑笑生"));
//增加计算机书籍
bookList.add(new ComputerBook("Think in Java",4300,"Bruce Eckel","编程语言"));
}
//模拟书店卖书
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.0)+"元");
}
}
}
- 元数据(metadata)控制模块行为
通俗地说就是 配置参数,参数可以从文件中获得,也可以从数据库中获得。
其中达到极致的就是控制反转(Inversion of Control), 使用最多的就是Spring容器,在SpringContext配置文件中,基本配置如代码清单6-11所示
代码清单6-11 SpringContext的基本配置文件
<bean id="father" class="xxx.xxx.xxx.Father" />
<bean id="xx" class="xxx.xxx.xxx.xxx">
<property name="biz" ref="father"></property>
</bean>
然后,通过建立一个Father类的子类Son,完成一个新的业务,同时修改SpringContext文 件,修改后的文件如代码清单6-12所示。
代码清单6-12 扩展后的SpringContext配置文件
<bean id="son" class="xxx.xxx.xxx.Son" />
<bean id="xx" class="xxx.xxx.xxx.xxx">
<property name="biz" ref="son"></property>
</bean>
通过扩展一个子类,修改配置文件,完成了业务变化,这也是采用框架的好处。
{
System.out.println(“书籍名称:” + book.getName()+“\t书籍作者:” + book.getAuthor()+ “\t书籍价格:” + formatter.format (book.getPrice()/100.0)+“元”);
}
}
}
2. 元数据(metadata)控制模块行为
通俗地说就是 配置参数,参数可以从文件中获得,也可以从数据库中获得。
其中达到极致的就是控制反转(Inversion of Control), 使用最多的就是Spring容器,在SpringContext配置文件中,基本配置如代码清单6-11所示
代码清单6-11 SpringContext的基本配置文件
然后,通过建立一个Father类的子类Son,完成一个新的业务,同时修改SpringContext文 件,修改后的文件如代码清单6-12所示。
代码清单6-12 扩展后的SpringContext配置文件
通过扩展一个子类,修改配置文件,完成了业务变化,这也是采用框架的好处。