1. 里氏替换原则【Liskov Substitution Principle】
里氏替换原则的定义:
- 如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
- 所有引用基类的地方必须能透明地使用其子类的对象。
第二个定义是最清晰明确的,通俗点讲只要父类能出现的地方我子类就可以出现,而且调用子类还不产生任何的错误或异常,调用者可能根本就不需要知道是父类还是子类。但是反过来就不成了,有子类出现的地方,父类未必就能适应,里氏替换法则包含了四层意思:
- 子类必须完全的实现父类的方法。
- 子类可以有自己的个性。
- 覆盖或实现父类的方法时输入参数可以被放大。
- 覆盖或实现父类的方法时输出结果可以被缩小。
2.单一职责原则【Single Responsibility Principle】
-
单一职责原则:应该有且仅有一个原因引起类的变更

太 easy 的类图了,我相信即使一个初级的程序员也可以看出这个接口设计的有问题,用户的属性(Properties)和用户的行为(Behavior)没有分开,这是一个严重的错误!非常正确,确实是这个接口设计的一团糟,应该把用户的信息抽取成一个业务对象(Bussiness Object,简称 BO),把行为抽取成另外一到另外一个接口中,我们把这个类图重新画一下:

其实在实际的使用中,我们更倾向于使用两个不同的类或接口,一个就是 IUserBO, 一个是 IUserBiz,类图应该如如下图:

单一职责原则的好处:- 类的复杂性降低,实现什么职责都有清晰明确的定义;
- 可读性提高,复杂性降低,那当然可读性提高了;
- 可维护性提高,那当然了,可读性提高,那当然更容易维护了;
- 变更引起的风险降低,变更是必不可少的,接口的单一职责做的好的话,一个接口修改只对相应的实现类有影响,与其他的接口无影响,这个是对项目有非常大的帮助
3.依赖倒置原则【Dependence Inversion Principle】
依赖倒置原则定义:
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 依赖倒转(倒置)的中心思想是面向接口编程
- 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在 java 中,抽象指的是接口或抽象类,细节就是具体的实现类
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
依赖倒置原则的注意事项和细节
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好.
- 变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化
- 继承时遵循里氏替换原则
依赖关系传递的三种方式和应用案例
- 接口传递
应用代码案例
public class DependencyPass {
public static void main(String[] args) {
// 方式 1: 通过接口传递实现依赖
// 开关的接口
ChangHong changHong = new ChangHong();
OpenAndClose openAndClose = new OpenAndClose();
openAndClose.open(changHong);
}
interface IOpenAndClose {
public void open(ITV tv); //抽象方法,接收接口
}
interface ITV { //ITV 接口
public void play();
}
static class ChangHong implements ITV{
@Override
public void play() {
System.out.println("长虹电视机,打开");
}
}
static class OpenAndClose implements IOpenAndClose{
@Override
public void open(ITV tv) {
tv.play();
}
}
}
- 构造器传递
应用案例代码
public class DependencyPass2 {
public static void main(String[] args) {
ITV changHong = new ChangHong();
//通过构造器进行依赖传递
OpenAndClose openAndClose = new OpenAndClose(changHong);
openAndClose.open();
}
interface IOpenAndClose {
public void open(); //抽象方法,接收接口
}
interface ITV { //ITV 接口
public void play();
}
static class ChangHong implements ITV{
@Override
public void play() {
System.out.println("长虹电视机,打开");
}
}
static class OpenAndClose implements IOpenAndClose{
ITV tv;
public OpenAndClose(ITV tv){
this.tv = tv;
}
public void open() {
tv.play();
}
}
}
- setter 方式传递
应用案例
public class DependencyPass2 {
public static void main(String[] args) {
ITV itv = new ChangHong();
OpenAndClose openAndClose = new OpenAndClose();
openAndClose.setTv(itv);
openAndClose.open();
}
interface IOpenAndClose {
public void open(); //抽象方法,接收接口
}
interface ITV { //ITV 接口
public void play();
}
static class ChangHong implements ITV{
@Override
public void play() {
System.out.println("长虹电视机,打开");
}
}
static class OpenAndClose implements IOpenAndClose{
ITV tv;
public void setTv(ITV tv) {
this.tv = tv;
}
public void open() {
tv.play();
}
}
}
4.接口隔离原则【Interface Segregation Principle】
接口隔离原则定义:
- 客户端不应该依赖它不需用的接口
- 类间的依赖关系应该建立在最小的接口上
举例:
我们来举个例子来说明接口隔离原则到底对我们提出了什么要求。现在男生对小姑娘的称呼使用频率最高的应该是“美女”了吧,我们今天来定义一下什么是美女:首先要面貌好看,其次是身材要窈窕,然后要有气质,当然了,这三者各人的排列顺序不一样,总之要成为一名美女就必须具备:面貌、身材和气质,我们用类图类体现一下星探(当然,你也可以把你自己想想成星探)找美女的过程,看类图:

星探寻找美女的程序我们就开发完毕了,我们来想想这个程序有没有问题,思考一下 IPettyGirl 这个接口,这个接口是否做到了最优秀的设计。
我们的审美观点都在改变,美女的定义也在变化。一千多年前的唐朝杨贵妃如果活在现代这个年代非羞愧死不行,为什么?胖呀!但是胖不不影响她入选中国的四大美女行列,说明当时的审美和现在是有差异地,当然随着时代的发展我们的审美观也在变化,突然有一天,也不用突然有一天,就现在,你发现有一个女孩,脸蛋不怎么样,身材也一般般,但是气质非常好,我相信大部分人都会把这样的女孩叫美女,审美素质提升了,但是我们接口却定义了美女必须是三者都具备呀,可能你要说了,我重新扩展一个美女类,只实现 greatTemperament 方法其他两个方法置空,什么都不写,不就可以了吗?聪明,但是行不通!为什么呢?星探 AbstractSearcher 依赖的是 IPettyGirl 接口,它有三个方法,你只实现了两个方法,星探的方法是不是要修改?我们上面的程序打印出来的信息少了两条,还让星探怎么去辨别是不是美女呢?
好了,我们发现我们的接口 IPettyGirl 接口设计是有缺陷地,过于庞大了,容纳了一些可变的因素,根据接口隔离原则,星探 AbstractSearcher 应该依赖与具有部分特质的女孩子,而我们却把这些特质都封装了起来,放到了一个接口中了,封装过渡了!问题查找到了,我们重新修改一下类图:

把原 IPettyGirl 接口拆分为两个接口,一种是外形美的美女 IGoodBodyGirl,这类美女的特点就是脸蛋和身材极棒,超一流,但是没有审美素质,比如随地吐痰,出口就是 KAO,CAO 之类的,文化程度比较低;另外一种是气质美的美女 IGreatTemperamentGirl,谈吐和修养都非常高。我们从一个比较臃肿的接口拆分成了两个专门的接口,灵活性提高了,可维护性也增加了,不管以后是要外形美的美女还是气质美的美女都可以轻松的通过 PettyGirl 定义
接口各类原则的四层含义:
- 接口尽量要小,但是也要满足单一职责的原则
- 接口要高内聚,提高接口、类、模块的处理能力,减少对外的交互
- 定制服务,只提供访问者需要的方法
- 接口设计是有限度的,接口的设计粒度是越小系统越灵活,这是不争的事实,但是这就带来的结构的复杂化,开发难度增加,维护性降低,这不是一个项目或产品所期望看到的,所有接口设计一定要注意适度,适度的“度”怎么来判断的呢?根据经验和常识判断!
5.迪米特法则【Low Of Demeter】
迪米特法则包含以下四层意思:
- 只和朋友交流

Teacher.java 的源程序如下:
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);
}
}
老师就有一个方法,发布命令给体育委员,去清查一下女生的数量。下面是体育委员 GroupLeader.java
的源程序:
public class GroupLeader {
//有清查女生的工作
public void countGirls(List<Girl> listGirls){
System.out.println("女生数量是:"+listGirls.size());
}
}
下面是 Girl.java,就声明一个类,没有任何的代码:
public class Girl {
}
我们来看这个业务调用类 Client:
public class Client {
public static void main(String[] args) {
Teacher teacher= new Teacher();
//老师发布命令
teacher.commond(new GroupLeader());
}
}
我们回过头来看这个程序有什么问题,首先来看 Teacher 有几个朋友,就一个 GroupLeader 类,这个就是朋友类,朋友类是怎么定义的呢?出现在成员变量、方法的输入输出参数中的类被称为成员朋友类,迪米特法则说是一个类只和朋友类交流, 但是 commond 方法中我们与 Girl 类有了交流,声明了一个List动态数组,也就是与一个陌生的类 Girl 有了交流,这个不好,那我们再来修改一下,类图还是不变,先修改一下 GroupLeader 类,看源码:
public class GroupLeader {
//有清查女生的工作
public void countGirls(){
List<Girl> listGirls = new ArrayList();
//初始化女生
for(int i=0;i<20;i++){
listGirls.add(new Girl());
}
System.out.println("女生数量是:"+listGirls.size());
}
}
public class Teacher {
//老师对学生发布命令,清一下女生
public void commond(GroupLeader groupLeader){
//告诉体育委员开始执行清查任务
groupLeader.countGirls();
}
}
- 朋友间也是有距离的

很简单的类图,实现软件安装过程的第一步做什么、第二步做什么、第三步做什么这样一个过程,我们来看三个类的源代码,先看 Wizard 的源代码:
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);
}
}
public class InstallSoftware {
public void installWizard(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();
}
}
}
}
}
这个程序很简单,运行结果和随机数有关,我就不粘贴上来了。我们想想这个程序有什么问题吗?Wizard 类把太多的方法暴露给 InstallSoftware 类了,这样耦合关系就非常紧了,我想修改一个方法的返回值,本来是 int 的,现在修改为 boolean,你看就需要修改其他的类,这样的耦合是极度不合适的,迪米特法则就要求类“小气”一点,尽量不要对外公布太多的 public 方法和非静态的 public 变量,尽量内敛,多使用 private,package-private、protected 等访问权限。我们来修改一下类图

我们再来看一下程序的变更,先看 Wizard 程序:
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);
}
public void installWizard(){
int first = this.first();
//根据first返回的结果,看是否需要执行second
if (first > 50) {
int second = second();
if (second > 50) {
int third = third();
if (third > 50) {
first();
}
}
}
}
}
public class InstallSoftware {
public void installWizard(Wizard wizard) {
wizard.installWizard();
}
}
是自己的就是自己的。在项目中有一些方法,放在本类中也可以,放在其他类中也没有错误,那怎么去衡量呢?你可以坚持这样一个原则:如果一个方法放在本类中,即不增加类间关系,也对本类不产生负面影响,就放置在本类中。
6.开闭原则【Open Close Principle】
开闭原则的定义:一个软件实体应该对扩展开放,对修改关闭。其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化
我们思考这样一个问题:一个软件产品只要在生命期内,都会发生变化,变化既然是一个既定的事实,我们就应该在设计时候尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现“拥抱变化”,开闭原则告诉我们通过尽量通过扩展软件实体的行为来实现变化,而不通过修改来已有的代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则,我们举例什么是开闭原则,以书店销售书籍为例,类图如下:

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 name;
}
@Override
public int getPrice() {
return price;
}
@Override
public String getAuthor() {
return author;
}
}
然后我们看书店是怎么销售书籍的:
public class BookStore {
private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
//静态模块初始化,项目中一般是从持久层初始化产生
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)+"元");
}
}
}
注意,我们在 BookStore 中声明了一个静态模块,实现了数据的初始化,这部分应该是从持久层产生
的,由持久层工具进行管理。运行结果如下:
------------书店买出去的书籍记录如下:---------------------
书籍名称:天龙八部 书籍作者:金庸 书籍价格:¥32.00元
书籍名称:巴黎圣母院 书籍作者:雨果 书籍价格:¥56.00元
书籍名称:悲惨世界 书籍作者:雨果 书籍价格:¥35.00元
书籍名称:金瓶梅 书籍作者:兰陵笑笑生 书籍价格:¥43.00元
项目投产了,书籍正常销售出去,书店也盈利了。从 2008 年开始,全球经济都开始下滑,对零售业影响还是比较大,书店为了生存开始打折销售:所有 40 元以上的书籍 9 折销售,其他的 8 折销售。对已经投产的项目来说,这就是一个变化,我们来看看这样的一个需求变化,我们该怎么去应对,有三种方法可以解决这个问题:
修改接口。在 IBook 上新增加一个方法 getOffPrice(),专门进行打折处理,所有的实现类实现该方法。但是这样修改的后果就是实现类 NovelBook 要修改,BookStore 中的 main 方法也修改,同时 IBook 作为接口应该是稳定且可靠的,不应该经常发生变化,否则接口做为契约的作用就失去了效能,——因此,该方案否定。
修改实现类。修改 NovelBook 类中的方法,直接在 getPrice()中实现打折处理,好办法,我相信大家在项目中经常使用的就是这样办法,通过 class 文件替换的方式可以完成部分业务(或是缺陷修复)变化,该方法在项目有明确的章程(团队内约束)或优良的架构设计时,是一个非常优秀的方法,但是该方法还是有缺陷的,例如采购书籍人员也是要看价格的,由于该方法已经实现了打折处理价格,因此采购人员看到的也是打折后的价格,这就产生了信息的蒙蔽效果,导致信息不对称而出现决策失误的情况。——因此,该方案也不是一个最优的方案。
通过扩展实现变化。增加一个子类 OffNovelBook,覆写 getPrice 方法,高层次的模块(也就是 static静态模块区)通过 OffNovelBook 类产生新的对象,完成对业务变化开发任务。——好办法,修改也少,风险也小,我们来看类图:

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;
}
}
很简单,仅仅覆写了 getPrice 方法,通过扩展完成了新增加的业务。然后我们来看 BookStore 类的修
改:
public class BookStore {
private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
//静态模块初始化,项目中一般是从持久层初始化产生
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)+"元");
}
}
}
运行结果:
------------书店买出去的书籍记录如下:---------------------
书籍名称:天龙八部 书籍作者:金庸 书籍价格:¥25.60元
书籍名称:巴黎圣母院 书籍作者:雨果 书籍价格:¥50.40元
书籍名称:悲惨世界 书籍作者:雨果 书籍价格:¥28.00元
书籍名称:金瓶梅 书籍作者:兰陵笑笑生 书籍价格:¥38.70元
注意:开闭原则说是对扩展开放,对修改关闭,并不意味着不做任何的修改
开闭原则的重要性:
- 开闭原则对测试的影响:原来的代码完整性都是经过千锤百炼,现在只需要测试扩展的代码即可
- 开闭原则可以提高复用性:。在面向对象的设计中,我们所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑,只要这样代码才可以复用,粒度越小,被复用的可能性就越大。
- 开闭原则可以提高可维护性:一个软件投产后,维护人员的工作不仅仅是对数据进行维护,还可能对程序进行扩展,那维护人员最乐意做的事情,就是扩展一个类,而不是修改一个类

1918

被折叠的 条评论
为什么被折叠?



