What

什么是设计模式?度娘是这样说的:

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

我的理解是,设计模式是被经验丰富的人(工程师)总结出来的一系列“套路”,针对不同的场景,使用不同的“套路”可以解决对应的问题。

一个简单的但不贴切的例子:有些人喜欢早上洗澡,他早上的时间安排是:起床->洗澡->刷牙->烧水->冲麦片->吃早餐->上班。后来他发现,烧水,洗澡,刷牙能一起做啊!所有流程就简化成:起床->(烧水,洗澡,刷牙)->冲麦片->吃早餐->上班

然后总结出一个“套路”:可以同时处理的事情同时做,能节省时间。

例子不贴切,想表达的意思:设计模式是一种经验主义,用来解决共性问题。

Why

《设计模式 可复用面向对象软件的基础》这本黑皮书在书柜里吃了大半年的灰了,我一直都知道设计模式很重要,但是每次翻了几页都坚持不下去。直到前段时间,我在阅读老项目的时候感到效率特别低,思考良久,觉得是因为自己不熟悉设计模式导致的。

为什么要熟悉设计模式?因为我在阅读公司成熟项目的代码时感觉到很吃力,代码的每一句话都不复杂,但是拼在一起,我就很难揣摩这样实现的缘由,也不能很快的知道哪些组件是通过什么原理能够协同工作的。这导致我看代码的效率低,具体表现是,我会经常在多个文件之间切换浏览,重复地看相同的代码,也会经常看到后面的代码,忘记了前面的代码。

而如果熟悉了常见的设计模式之后,我在阅读代码的时候,会思考这部分代码可能使用了哪些设计模式,并通过继续阅读代码,一点一点验证猜想。这样,无目的的学习过程变成了有目的的验证过程。学习效率和质量会大大提高,并且对设计模式会有更加感性的了解。

设计模式的优点我觉得有以下几点:

  1. 代码复用
  2. 方便理解历史代码,增加可维护性,提高效率
  3. 写出有逼格的代码,让别人觉得你不是菜逼

How

设计模式都是一些抽象的东西,而人习惯于用自己知识体系内的东西来理解新的事物。所以还是得用具体的例子来学习设计模式。

学习资料主要来自《设计模式 可复用面向对象软件的基础》以及网上那些粘贴来粘贴去的博客资料学习。

部分例子会从公司项目的代码里抽象出来。会做一些改动,换个应用场景,毕竟不能泄密对吧。

一个有B格的讲设计模式的网站:写最好的设计模式专栏

希望自己能坚持下去。

四个设计原则

开闭原则

“Software entities should be open for extension,but closed for modification”

开闭原则:模块应对扩展开放,而对修改关闭。即:模块应尽量在不修改原代码的情况下进行扩展。

那如何满足开闭原则呢?关键在于抽象化和对可变性的封装。 引用一下xingjiarong的话:

在像java这样的面向对象编程语言里面,可以给系统一个一劳永逸、不再改变的抽象设计,该设计允许有无穷无尽的行为在实现层被实现。在java语言里,可以给出一个或多个抽象java类或者java接口,规定出所有的具体类必须提供的方法的特征作为系统设计的抽象层。这个抽象层预见了所有的可能扩展,因此,在任何情况下都不会改变。这使得系统的抽象层不需要修改,从而满足了“开-闭”原则的第二条:对修改关闭。
同时,由于从抽象层导出的一个或多个新的具体类可以改变系统的行为,因此系统的设计对扩展是开放的,这就满足了“开-闭”原则的第一条:对扩展开放。

以及

考虑你的设计中有什么可能会发生变化。这一思想用一句话总结为:“找到一个系统的可变因素,将它封装起来”。
这意味着:
1、一种可变性不应该散落在代码的很多角落里,而应该被封装到一个对象里面。同一种可变性的不同表象意味着同一个继承等级结构中的具体子类。
2、一种可变性不应该与另一种可变性混合在一起。

需要注意的是,引用的讲解中,”一劳永逸、不再改变的抽象设计”可以说是不存在的,这也是实际工作中的烂代码、难维护的工程那么多的原因。高可用性,高可维护性项目的背后都是经验丰富的架构师的经验和他们掉的头发。

完全的“开闭”很难实现,我们需要做的是尽可能的使系统满足“开闭原则”

里氏替换原则

里氏替换原则:一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别。

里氏替换原则是继承复用的基石。只有当衍生类可以替换掉基类,软件单位的功能不会受到影响时,基类才能真正的被复用,而衍生类也才能够在基类的基础上增加新的行为。

里氏替换原则的一个例子是正方形与长方形例子。具体见设计原则(二)里氏替换原则(LSP) ,这个原则比较好理解,做过几个项目的人应该都能理解为何要这样做。

组合复用原则

组合复用原则:合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)经常又叫做合成复用原则。合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。它的设计原则是:要尽量使用合成/聚合,尽量不要使用继承

组合以及继承的优缺点:

组合复用:

  • 优势
    • 新对象存取成分对象的唯一方法是通过成分对象的接口。
    • 这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。
    • 这种复用支持包装。
    • 这种复用所需要的依赖较少。
    • 每一个新的类可以将焦点集中到一个任务上。
    • 这种复用可以在运行时间动态进行,新对象可以动态的引用与成分对象类型相同的对象。
  • 劣势:系统会有较多的对象需要管理。

继承:

  • 优势
    • 新的实现比较容易,因为基类的大部分功能都可以通过继承自动的进入子类。
    • 修改或扩展继承而来的实现较为容易。
  • 劣势
    • 继承复用破坏了包装,因为继承超类的的实现细节暴露给子类。由于超类的内部细节常常对子类是透明的,因此这种复用是透明的复用,又称“白箱”复用。
    • 如果超类的实现发生改变,那么子类的实现也不得不发生改变。因此,当一个基类发生改变时,这种改变就会像水中投入石子引起的水波一样,将变化一圈又一圈的传导到一级又一级的子类,使设计师不得不相应地改变这些子类,以适应超类的变化。
    • 从超类继承而来的实现是静态的,不可能在运行时间内发生改变,因此没有足够的灵活性。

一句话概括就是,继承用多了,之后改代码就是“牵一发而动全身”。

依赖倒置原则

在软件里面,把父类都替换成它的子类,程序的行为没有变化。简单的说,子类型能够替换掉它们的父类型。

具体的表现是:要针对接口编程,不要针对实现编程。

例如,我们使用JAVA的集合对象时,都是这样写的

List a = new ArrayList();

这样是部分依赖,前边的A是依赖于接口,这样当B换成其他的继承自A的子类的时候,其他的程序是不会受到影响的。

但是如果写成了:

ArrayList a = new ArrayList();

这就是完全依赖,我如果有天要换成别的List的实现类,比如LinkedList,而不用ArrayList,就不太方便了。

更贴切的实例:设计原则(四)依赖倒置原则(DIP)