咱们还是从以做系统为例来聊吧。
我们实现了一个很简单的仓库系统。早期的时候你只设计了存储胡萝卜的过程,大致流程就是拿起胡萝卜=》丢到仓库=》关闭仓库门,功能很快就实现了。
但是之后加需求了,仓库里可以存储液体了,可以认为多了一口大缸,比如现在我要存啤酒,那么与胡萝卜不同的是我不能直接把啤酒丢进去,我需要把啤酒倒进缸中,那之前的代码就不适用了,那咋办呀?
此时,一个大聪明提议,加个判断嘛,这不就ok了,然后风风火火的写完了,功能依旧正常,代码也还可以看得懂。
但是时间一长就有问题了,后续又需要往仓库中放更多种类的东西,并且存放过程并不一致,大家还是if else的堆叠。这个时候你看着现在的代码陷入了沉思,这好几千行,看都得看一会。幸好的是,目前还能跑得动,功能也正常。
但谁能想到,仓库搬家了,这就意味着我们原来把东西放到仓库步骤里存放过程都要改,好家伙,这可是个大工程,你看这好几千行的代码,只能开始了加班,心里想着要是最开始的项目架构设计的合理一点就好了,这样就不至于加班这么晚了,这个时候你才想起来了大学时期学习的那一门叫做设计模式的课程。
没错,设计模式简单来说就是前人总结的软件开发经验,让我们在后期需求增加,需求变动时不至于那么被动,并且可以极大的提高代码的可读性和重用性。
那我们怎么才能优化上面的仓库系统呢?
别着急,了解完设计模式你就清楚了。
六大原则
开闭原则
对扩展开放,对修改关闭。
相信大家已经读完一遍了,咋样,有没有豁然开朗的感觉!
反正我没有,这都是啥!!! 言归正传,咱还得看看到底咋回事。
这里的扩展可以理解为增加新的业务逻辑代码,修改则可以认为修改原有的代码。
那为啥得这么做呢?
试想一下,一个极其复杂逻辑的业务代码,经过你无数次的debug和修改之后终于可以运行了,并且功能也没问题。这个时候一个并不清楚你的实现逻辑的一个楞头小子要扩展你的业务,并且扩展的方式还是修改你的源代码,不出意外,又跑不起来了。
那如果我们在最初设计的时候就严格遵守了开闭原则,也就是说让后面的人添加新业务的时候只添加新代码而不修改源代码,那会不会好一点?
那要咋做呢?
看看下面的代码可以吗?
代码语言:javascript复制public interface Example {
public void operate();
public void setNextOne(Example example);
}
public class OneExample implements Example {
Example nextOne;
@Override
public void operate() {
System.out.println("一系列处理");
if(nextOne!=null)
nextOne.operate();
}
@Override
public void setNextOne(Example example) {
nextOne = example;
}
}
public class ExampleTest {
public static void main(String[] args) {
Example oneExample = new OneExample();
oneExample.operate();
}
}
看到上面的代码,你可能会疑惑,这就能在满足开闭原则的条件下增加功能嘛?
是的,可以的,为啥呢?
这里我们首先要明确一个点哈,这里我们要满足的开闭原则并不是严格意义上的开闭原则,只是对核心业务代码保证开闭原则。
听到这有点绕,这是啥意思呢?
也就是说我们并不会修改原来的核心业务代码,也就是OneExample 的operate方法,因为这里面的代码逻辑我们并不清楚,随意修改容易出现问题。但是我们会修改ExampleTest中的方法,把我们添加的新功能放进去即可。
那怎么放呢?添加代码如下
代码语言:javascript复制public class TwoExample implements Example {
Example nextOne;
@Override
public void operate() {
System.out.println("新添加的业务逻辑");
if(nextOne!=null)
nextOne.operate();
}
@Override
public void setNextOne(Example example) {
nextOne = example;
}
}
public class ExampleTest {
public static void main(String[] args) {
Example oneExample = new OneExample();
Example twoExample = new TwoExample();
oneExample.setNextOne(twoExample);
oneExample.operate();
}
}
看到这里你会发现,利用下面这个代码就可以很优雅的执行新添加的流程。
代码语言:javascript复制if(nextOne!=null)
nextOne.operate();
这样确实方便后续功能的添加,并且不会修改咱们原来的写的代码,这样就避免了大聪明把我们好不容易调好的代码又改成bug的问题。
这时候有小伙伴就会说了,那我怎么能想到这么做呢?
那就要好好学设计模式的那些设计模式了,然后灵活应用了,上面的这种模式就是责任链模式,后面我们会仔细的讲,大家只要了解这种思想就够了。
到现在为止,再回顾一下,你就会发现设计模式的妙处,确实可以帮我们有效的提升开发和维护效率,不知道是不是提起你对设计模式的兴趣了呢。
里氏代换原则
所有引用基类 (父类)的地方必须能透明地使用其子类的对象。
ok,不出意外,估计大家和我一样,还是看的一头雾水。
我们用通俗的话解释一下。其实就是要求子类必须要实现父类的所有方法。
这时候你不禁想问,这有啥好处呢?
我们可以把它作为开闭原则的扩展。具体有什么应用呢,我们来看看哈。
这里我们有一个汽车类Car,然后有三个实现类,比亚迪,蔚来,小鹏。Car有start,move,drift方法。现在我们有一辆比亚迪,我们要开出去,我们先看看不满足里氏代换的形式。
代码语言:javascript复制比亚迪 byd=new 比亚迪();
byd.start();
byd.move();
byd.drift();
由于父类无法完全调用子类的方法,我们只能使用子类来写代码,看着也好像还可以哈。
但是问题来了,我们换车了,开上蔚来了,那原来byd的代码不能用了呀,需要完全的写一遍,如下
代码语言:javascript复制蔚来 wl=new 蔚来();
wl.start();
wl.move();
wl.drift();
但如果遵循里氏代换之后呢?
代码语言:javascript复制Car car=new 比亚迪();
car.start();
car.move();
car.drift();
这个时候Car与比亚迪,蔚来,小鹏子类的方法一致,这个时候就可以直接使用父类接收子类对象,然后执行相应的操作。那我们怎么换成蔚来呢?
代码语言:javascript复制Car car=new 蔚来();
car.start();
car.move();
car.drift();
是不是这样就可以了,很轻松的进行修改。
结合上述两个例子,相信你也会发现所有开闭原则只是给了我们一个启发,但并不是说完全对修改关闭,而是核心业务代码对修改关闭即可。
看完这个之后,不知道你对设计模式是不是有一点新的体会了呢
依赖倒转原则
抽象不应该依赖于细节,细节应当依赖于抽象。
看得出来,这概念就挺抽象。
这又是啥意思呢?
以做菜为例子哈,我们想一下,我们要做一份西红柿炒鸡蛋,怎么写代码呢?
大致过程是不是这样
切西红柿,打鸡蛋=》炒西红柿和鸡蛋
逻辑非常合理,并且代码写的也很简单。
但现在出现一个问题,我们换口味了,想吃胡萝卜炒鸡蛋,怎么做呢?
是不是原来的流程就不可以了,我们需要写一个新的流程,需要推翻原来的流程,变成下面这样
切胡萝卜,打鸡蛋=》炒胡萝卜和鸡蛋
随着需求的更改需要大幅度修改代码,这就说明我们的结构设计并不合理,并不稳定。
那我们怎么才能再更改需求的时候并不会大幅度修改我们原来的架构呢?
这时候就可以聊聊依赖倒转原则了。
首先,我们要明白一个概念,越抽象,越稳定。
啥意思嘞
大家可能对抽象这个概念理解的就很抽象的,我们来通俗的聊聊。
其实,所谓的抽象的过程可以理解为总结事物或则流程的共同特征的过程。
例如哈,我们怎么抽象比亚迪,蔚来,小鹏,他们有一个共同点,都是汽车,还有什么共同点呢?都可以移动。
这样我们就生成了他们的抽象,也就是一个包含移动方法的汽车类。
这是总结事物,那怎么总结流程呢?
像我们刚才聊的做菜,切胡萝卜,打鸡蛋和切西红柿本质上是不是都可以抽象为备菜,炒胡萝卜,西红柿和鸡蛋是不是可以抽象为炒菜。那我们就可以改一下做菜的逻辑了。
备菜=》炒菜
很明显看出来,我们现在的流程实现就是依赖于抽象,而不是依赖实现了。
这样无论我们是要西红柿炒蛋还是胡萝卜炒蛋,都不需要修改原来的大体流程,只需要给备菜对象和炒菜对象赋值对应的对象即可,我们的项目结构就是很稳定的。这样就可以很轻松的应对需求的变化,并且对源代码的修改是极小的。
应用样例
看了这么多原则,我们举的例子都是尽量方便大家理解,并没有结合实际业务,接下来咱们看看设计模式结合业务,代码会不会变得更优雅。
业务简介
你需要写一个请假流程,请假流程由两部分组成,第一部分是申请流程,可能需要多人批准,第二部分是排班。需要考虑后续申请和排班业务的需求的变动。
怎么设计呢
我们首先可以想到的是依赖倒转,就是依赖抽象实现任务,都不需要我们抽象了,题目都给好了,就是俩抽象,申请和排班。
接下来就是考虑怎么可以满足开闭原则的前提下实现申请和排班业务?
我们是不是还是可以使用刚才用到的责任链模式去做,这样就可以保证不修改主要代码的情况下,对功能进行变更了。
这里简单讲一下责任链模式,不然大家容易看的云里雾里的。
我们画一张简单的图
他其实就是第一个链对象执行完毕,就会由下一个对象执行,直到执行到最后一个对象。
为什么说满足开闭原则呢?
因为我们在添加新功能的时候并不需要修改原来对象的业务逻辑,只需要把新添加的对象链接到链上即可,这样就增加了新的业务。
具体体现在代码是怎么弄的呢?
这样,就把twoexample链接到oneExample之后了,并且由于operate方法是这样的
这样,只要nextOne,也就是责任链的下一个链对象不为空,就会调用下一个对象的operate,这就实现了责任链。
那里氏代换呢
别着急,看看实现就知道了怎么回事了。
实现
申请
代码语言:javascript复制public interface Apply {
public void apply();
public void setNextOne(Apply apply);
}
public class GroupApply implements Apply {
Apply apply;
@Override
public void apply() {
System.out.println("组长批准请假流程");
// 这里就是可以像链路一样调用的关键
if (apply != null)
apply.apply();
}
@Override
public void setNextOne(Apply apply) {
this.apply = apply;
}
}
public class BossApply implements Apply {
Apply apply;
@Override
public void apply() {
System.out.println("老板批准请假流程");
// 这里就是可以像链路一样调用的关键
if (apply != null)
apply.apply();
}
@Override
public void setNextOne(Apply apply) {
this.apply = apply;
}
}
调班
代码语言:javascript复制public interface Adjust {
public void adjust();
public void setNextOne(Adjust adjust);
}
public class FirstAdjust implements Adjust {
Adjust adjust;
@Override
public void adjust() {
System.out.println("进行排班");
// 这里就是可以像链路一样调用的关键
if(adjust!=null)
adjust.adjust();
}
@Override
public void setNextOne(Adjust adjust) {
this.adjust=adjust;
}
}
请假流程
代码语言:javascript复制public class MTest {
public static void main(String[] args) {
// 如果Apply或则Adjust的具体实现类变化了,直接修改引用即可,下面的业务逻辑代码不需要修改,这就是里氏代换和依赖倒置结合的好处。
Apply groupApply = new GroupApply();
Apply bossApply = new BossApply();
// 把bossApply链接到groupApply之后,形成一条责任链
// 后期有新的添加,像这种方式一样,链接到groupApply后即可,不需要修改原有的代码
groupApply.setNextOne(bossApply);
Adjust adjust = new FirstAdjust();
groupApply.apply();
adjust.adjust();
}
}
可以看到哈,所有对象都是用的抽象接口,而不是实现,也就是我们之前提到的里氏代换。
总结
初次了解设计模式,很难理解为什么需要这些准则和规范,但真正到了开发项目的时候就会突然了解前人的智慧。这种智慧不仅仅可以应用到项目设计中,同样也可应用到我们的做人做事上。
后续的三大原则很快更新,先写到这了。