设计模式 - 最简单最有趣的方式讲述

别名《我替你看Head First设计模式》

本文以故事的形式带你从0了解设计模式,在其中你仅仅是一名刚入职的实习生,在项目中摸爬滚打。(以没有一行真正代码的形式,让你无压力趣味学习)

设计模式

策略模式

故事背景:
    你们公司打算开发一款鸭塘模拟游戏《SimUDuck》,于是公司主管找上实习生的你,并提出了简单的需求:


功能需求:

需要表达两只鸭子,绿鸭子和红鸭子。它们都需要会嘎嘎叫游泳,并区分它们之间的颜色以显示(绿鸭子显示绿色、红鸭子显示红色)。


开始设计:

作为实习生的你十分自信,拿出了 "引以为豪" 的继承,并做出了设计。

  • 父类Duck :有三个方法嘎嘎叫quack游泳swim显示display
  • 子类GreenDuck、RedDuck :分别覆盖了显示display方法,实现各自的显示颜色。

故事背景:
    你最好的朋友产品经理找到你,让你改一个小小的功能:


功能需求:

需要加上一个的功能。


开始设计:

你临机一动,认为只需要在父类Duck中添加一个飞fly方法,所有鸭子就都会飞了,你十分的笃定。


故事背景:
    目前一切都很完美。过一段时间,产品经理又来找你了,你以为是夸奖你不错的,但实际上他是来告诉你,你的设计有bug!bug!bug!:


bug发现:

bug出现在,现在不止有绿鸭子红鸭子,后来又加入了橡皮鸭子,造成了橡皮鸭子在屏幕上飞来飞去的戏剧一幕。


开始设计:

作为父类继承给橡皮鸭子会飞的 "有趣特色" ,却不被理解的你很伤心,于是你的第一个想法是让橡皮鸭子覆盖掉父类继承的飞fly方法。

本就聪明的你这一次开始深度思考继承。发现,虽然这一次能够通过覆盖特殊行为勉强过关,但是如果又添加木制鸭子纸制鸭子呢?就需要在每个鸭子内部判断它是否会飞、是否会嘎嘎叫进行选择性覆盖,太复杂了。


故事背景:
    你收到一条备忘录,公司说将会每六个月为周期更新产品。没错!正是如你所想,因此你需要更干净利落的设计:


开始设计:

头脑风暴中你想到了 ++java / Qt++ 中的able结尾的接口,于是你开始往这个方向思索,能否将fly写为flyable接口,quack写为quackable接口,让只有鸭子具备这个行为时才实现该接口。


故事背景:
    在你给同事讲述后,同事却说:"这是你最愚蠢的主意,干嘛不直接说 '重复代码' 算了?当你需要对成百上千个子类的行为做不同的描述时,你感觉会怎么样?"。随后同事为你指出根本问题:


根本问题:

实际上是像flyquack这些方法,都是一直会随着需求的变化而变化,并不是所有的鸭子都会flyquack,因此继承并不是正确答案!采用able结尾的接口设计,也仅仅只是解决了橡皮鸭会飞的 "有趣特性" 的部分问题,对于变化部分任然需要不断的针对性实现(运维噩梦)。

你渴求的是:变更时对现有代码影响最小的方式。++你却将变化融入不变使得常常被迫往下追踪到定义了该行为的子类并修改它。++

设计原则:

识别应用中变化的方面,将它们和不变的方面分开。


开始设计:

"取得真经" 的你茅舍顿开,意识到正确的做法是取出变化的部分,并将它 "封装起来" 让它不会影响到其他代码。

设计原则:

针对接口(超类型/基类型)编程,而不是针对实现编程。 - 要点是通过针对超类型/基类型编程来利用多态
设计原则:

优先使用组合而不是继承。


故事背景:
   沉浸在技术提升中的你,通过自己发现了这个设计的又一个妙处,并在询问同事后的赞扬中,知道了通过setter方法可以动态的设计行为:


设计报告:

你如今的设计,关键在于Duck系列类将行为需求委托而出了,而不是通过自行实现行为方法。如:飞行行为,通过将行为需求委托给FlyBehavior* flyBehavior所引用的对象,而flyBehavior中引用的是哪一个对象无需关心,flyBehavior->fly()就对了,并且可以通过setter方式动态的设计行为。


伪代码:

测试:


故事背景:
    通过这次项目经历,作为实习生的你成长了,你明白了策略模式以及三个设计原则,你明白了不应该将鸭子的行为看作一组行为,而是开始将它们看作多种策略族(算法家族),如飞行算法家族中:普通飞是一个策略、不能飞是一个策略、飞的和火箭一样快也是一个策略。

策略模式: 定义了一个算法族,分别封装起来,使得它们之间可以互相变换。策略让算法的变化独立于使用它的客户。


故事概述:

面对场景:在日常开发中,往往渴望通过继承的方式让子类们继承父类的方法,实现代码的复用,但是仅使用继承终究是编译时决议,运行时的硬编码。导致变化融入不变从而常常需要向下追踪到行为独特的子类针对性实现。

策略模式:(在我看来)可以说是算法族模式,通过:++不变分离++ 、++针对封装的行为父类实现++ 、++采取组合的多态使用++。实现对于设计使用时,仅需为子类多态指针new算法族子类行为,子类自会通过继承于父类的多态调用方法,实际运行。


观察者模式

故事背景:
    你们公司喜欢培养实习生,于是你再一次挂帅出征。这一次是你们公司赢下了Weather-O-Rama公司想基于Internet气象观测台建立显示平台的合同:


功能需求:

Internet气象观测台提供:温度数据湿度数据气压数据,你需要编写代码作用于显示设备(告知显示设备新数据),让用户可以及时查看这三个数据的预报。并且Weather-O-Rama公司书写提供了一个WeatherData对象,你需要适配该对象(因为它可以获得更新的气象数据)。

  • 数据更新measurementsChanged会被调用。
  • measurementsChanged中可以通过getter系列方法获取数据。
  • 通过measurementsChanged可以高效告知所有用户新数据。

开始设计:

对于设计模式,你刚刚初窥门径,无法做到一蹴而就,所以对需求先给出了简易的第一版设计。


故事背景:
    有过《SimUDuck》游戏设计的经历的你,一眼就看出了这个设计的bug(运维噩梦):


bug发现:

针对具体实现编程了!你提出一个变化:气象站如果成功,很可能会使得未来多于三个数据显示需求,那么你便没有办法在不修改代码的情况下添加或移除其它显示元素。


故事背景:
    你:"看起来显示设备的update调用参数传递是一个变化的地方,我应该需要封装一下这里!并且gatter获取数据后,又指定特定显示设备的update调用,这明显是根据实现编程了"。但是你想了半天任然无明确头绪,于是决定询问 "大佬" 同事,他立马发现是你不具备一种思维。


根本问题:

你需要认识到观察者模式(出版者 + 订阅者 = 观察者模式),让我们换一个简单且有趣的例子来描述一下:报纸/杂志的订阅

  1. 报社开始运营,其作为出版者需负责出版报纸。
  2. 你到报社登记订阅,作为订阅者他们有新报纸就会交付给你。
  3. 你看腻了,告知报社取消订阅,新报纸就不再有你的份了。
  4. 而在其中有无数的订阅者

观察者模式: 定义对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。


故事背景:
    在此期间你提出疑问:"为什么是一对多,还是依赖关系"?同事:"因为主题是一个数据的拥有管理者,另一方面,众多的观察者是数据的需求者,并且主题是数据的唯一拥有者,所以观察者需要依赖于主题来获取数据!"。


开始设计:

受到触动你的立马开始了新的设计。

  • 观察者对象们需便于维护(多态指向):Observer*
  • 主题对象需一个结构用于维护订阅的观察者对象们:list<Observer*> observers;
  • 主题对象需提供维护是否订阅的方法:registerObservers()removeObservers()notifyObservers()

故事背景:
    大佬同事:"很不错,你已经了解我所表达的模式思维了,但你是不是忘记了分离变与不变?并且在其中有松耦合的魅力,你可以自己发现一下。"


开始设计:

你恍然大悟,对设计进行了优化。


松耦合:

当有太多依赖到另一个对象时,我们就说一个对象紧耦合到另一个对象。换一句话就是一个对象对另一个对象说:"你知道的太多了!"。而观察者模式是松耦合的一个很棒的例子。

  1. 主题只用知道观察者实现了某个接口update ------ 如同信件箱。
       无需知道观察者具体是什么样的观察者,想要做什么等。
  2. 主题可以在任意时刻添加依赖其的新观察者。
       因为主题依赖的是实现观察者多态对象列表。
  3. 观察者的新增无需修改主题。
       只需实现新类,在其中实现新观察者接口并注册新类为一个观察者。
  4. 任意一方修改都不会影响另一方,更可以彼此独立的复用主题或观察者。
       关联的地方仅在:++多态对象列表++ 、++多态对象的update方法++。

设计原则:

尽量做到交互的对象之间的松耦合设计。 - 主题与观察者的联系在于各自的接口上,仅需知道对象的接口,其他事情毫不知情。


伪代码:

测试:

为便于数据的显示,给WeatherData类添加一个setMeasurements()方法,以提供新数据。


故事背景:
    目前一切都很完美,"完美的" 解耦合。过了一段时间,你的好朋友产品经理又来了:"最新消息Weather-O-Rama公司最近进行了问卷采集,所以为了对齐用户需求,希望用户仅仅拿到他们所想要的数据,而不是强迫的接收所有的数据"。
    你:"呀!这不正是我最初的发现吗(未来很可能多于三个数据显示需求)?


开始设计:

显而易见正是你如今设计中的update参数传递部分的硬编码(上述伪代码中标红部分)行为所导致的。解决点在于:主题并不知观察者的数据所求意图,只有观察者懂自己,即观察者所需时自行通过getter系列函数拉取数据。


设计报告:

你如今的设计,关键在于主题与观察者皆采取组合(多态)编程。并优化为观察者所需时自行通过getter系列函数拉取数据,避免主题硬编码的强制发送数据行为。


故事背景:
    Weather-O-Rama公司来信:

观察者模式: 定义对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。


故事概述:

面对场景:对于日常开发中,渴望将数据类的新数据及时告知数据需求类们,这时新数据的来到仅数据类知道,而通过数据类的方法调用数据需求类们update方法传递数据,会出现针对实现的硬编码问题:update方法的固定调用。

观察者模式:通过组合的链表,实现主题观察者一对多关系,观察者的新增、删除仅需修改链表,主题的通知仅需调用观察者的特定接口update(),做到了交互对象之间的解耦合设计。


装饰者模式

故事背景:
    是的!又是你!由于你实在是太爱使用继承编写代码了,所以公司立志要将你磨成一把 "宝剑",但你听说小道消息,这是专门给你安排的项目就是为了给你打开 "设计之眼"!


功能需求:

星巴兹 (Starbuzz) 咖啡是最近兴起的连锁咖啡店,因为扩张速度太快所以他们着急更新下单系统。如下是他们老旧的下单系统设计:


故事背景:
    了解完的你,不经感叹:"确实是专门为我准备,和我曾经的设计习惯如出一辙,对于继承真是情有独钟!容我大胆想想这样设计的后果"。


设计分析:

按照他们的旧设计,在加上星巴兹 (Starbuzz) 咖啡如今蓬勃的发展,每多一个饮料就需要根据其价格进行实现 (真是针对实现编程的愚蠢设计...)。

(这才30个左右哟~~)更进一步来说,任性的客户就不能配料double?然后因此你需要完美预判写个HouseBlend1HouseBlend2HouseBlend3...?


故事背景:
    不多说了简直无理取闹,眼睛都看花了,问题很大!头炸了这么大!简直就是一个 "类爆炸" 的运维噩梦!


开始设计:

老旧的设计已为你将坑完美试探出:++不同种类的饮料价格是不同的 -> 选取的配料价格不同、单位量不同++。一个最随意的构思应运而生:


故事背景:
    看似解决,但任然存在问题。有过两次设计经验的你发现:"调料价格改变会迫使需要修改现有代码、新调料的增加需要修改父类中的cost()实现、不适合的调料会继承给对应的饮料"。


设计分析:

完美的触犯了:需将不变分离!配料就是极其有可能变化的代码,而父类作为一个不应该变化的部分(因为具有非常多的子类),它会影响到所有的子类(父类修改出bug,影响所有子类),也无法在运行时动态改变一个类的行为。


故事背景:
    碰巧周会悄然来临,说不定在其中可以找到解决方案,你需要做的就是讲述你如今对于继承的理解...


反思追寻:

  • 策略模式: 开发一款鸭塘模拟游戏《SimUDuck》。
  • 观察者模式: 开发基于Internet气象观测台建立显示平台。

设计原则:

类应该对扩展开放,但对修改关闭。


故事背景:
    "大佬" 同事来拯救你了:"小伙子不错嘛!成长的很快,但以防你 '中毒' 过深容我强调一下。开放-关闭原则其实通常情况下是办不到的,因为这需要时间和精力,过于的最求就是在浪费,它的实现往往也需要++引入新的抽象层次++,从而大大增加代码的复杂度。日常中你往往只需专注于最有可能改变的区域进行实现就足够了,而哪部分区域的变化是最重要的,就需要你具备熟能生巧的设计经验了,加油!"。


开始设计:

你现在正式从设计原则的角度开始着手设计,毫无疑问你所最熟悉的设计原则:

设计原则:

识别应用中变化的方面,将它们和不变的方面分开。
设计原则:

针对接口(超类型/基类型)编程,而不是针对实现编程。 - 要点是通过针对超类型/基类型编程来利用多态
设计原则:

优先使用组合而不是继承。

和两个理解不是太深的设计原则:

设计原则:

尽量做到交互的对象之间的松耦合设计。 - 主题与观察者的联系在于各自的接口上,仅需知道对象的接口,其他事情毫不知情。
设计原则:

类应该对扩展开放,但对修改关闭。 - 只需专注于最有可能改变的区域进行实现就足够了。

从你最熟悉的三个设计原则入手,无非渴望能够将变化的部分进行独立封装(以接口(超类型/基类型)编程),随后用组合实现运行时弹性的设置。


故事背景:
    你开始展开思维:"饮料原料都是水,那我通过饮料 '制作' 为开始(原料价格),使用调料 '装饰' 为过程(原料 += 调料价格),最终饮料成型(售卖价格),是否可行?"。


根本问题:

确实是一个很好的想法,但也仅仅是一个想法,对于一个设计模式,需要关注的太多了,于是你开始上网找 "名师" 。于是你便发现了一个设计模式:装饰者模式,并对你的想法进行了优化。

装饰者模式: 动态地将额外责任附加到对象上。对于拓展功能,装饰者提供子类化之外的弹性替代方案


本图实在不易于理解,此处给出最简的代码概述:

伪代码:

测试:


根本问题:

继承 :++并不是仅仅只能带来行为的复用,还能达成类型的匹配++。

在此做出假设: CondimentDecorator 并不继承于 Beverage,那么便会出现一层配料包装影响类型 。

测试:

这是装饰者模式所需的(装饰者有着和所装饰的对象相同的父类型),通过这样的方式可以实现:

  • 鉴于装饰对象所装饰对象具有相同父类型,需要原始对象的场景,可以直接传递一个被装饰的对象
  • 用一个或多个装饰者包囊一个对象。
  • 装饰者委托给所装饰对象之前或之后添加自己的行为。

故事背景:
    "大佬"同事:"干的不错!自行摸索下能够如此迅速的初步了解到装饰者模式,但容许我验证一下你的思维:CondimentDecorator类是否并没有存在的意义?重点不是配料们吗?直接继承Beverage类效果不是一样吗?"。
    你:"这确实是一个有趣的问题,碰巧我在资料查询中有所遇到,并进行过思考..."。


问题思路:

星巴兹 (Starbuzz) 咖啡刚刚兴起,所以功能需求上极少,以此使得CondimentDecorator类看似"鸡肋",但我认为其中存在两个关键设计原则。

设计原则:

尽量做到交互的对象之间的松耦合设计。 - 主题与观察者的联系在于各自的接口上,仅需知道对象的接口,其他事情毫不知情。
设计原则:

识别应用中变化的方面,将它们和不变的方面分开。

删除CondimentDecorator这种做法似乎简化了结构,但实际上会减少装饰者模式的可扩展性和灵活性。

  • 现代码分析装饰链形成正是因为调料具备Beverage的引用(Beverage*)。如果直接继承Beverage,那么此共用逻辑就需要在每个具体装饰者中重复编写,或在Beverage中编写导致抽象组件具体装饰者之间的耦合增加,且让每个具体组件获得其无需属性。

正如上所述,CondimentDecorator作为抽象装饰者,可以定义一些通用的逻辑或状态管理,如果直接继承Beverage,会引起代码激增(维护噩梦)。

通用逻辑写入具体装饰者违反:识别应用中变化的方面,将它们和不变的方面分开。
通用逻辑写入抽象组件违反:尽量做到交互的对象之间的松耦合设计。


故事背景:
   "大佬"同事:"回答的真好!看来你开始理解松耦合的魅力了!"


故事概述:

面对场景:对于日常开发中,渴望将对象进行不断的修饰,并拿到最后的修饰结果,如果将对象类可进行的修饰属性进行类内维护,将会造成不可想象的运维噩梦。

装饰者模式:通过具体装饰继承于抽象装饰抽象装饰具体组件继承于抽象组件,使得装饰组件具备统一类型,让装饰可以通过父类引用的方式引用原组件被修饰的组件,实现修饰链。并以组合(父类引用)的方式实现组件装饰分离实现动态地装饰。


本文资料来源:《Head First设计模式 第二版》

本文画图工具: diagrams

本文编写形式: markdown

相关推荐
转世成为计算机大神3 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
小乖兽技术4 小时前
23种设计模式速记法
设计模式
小白不太白9505 小时前
设计模式之 外观模式
microsoft·设计模式·外观模式
小白不太白9505 小时前
设计模式之 原型模式
设计模式·原型模式
澄澈i5 小时前
设计模式学习[8]---原型模式
学习·设计模式·原型模式
小白不太白95012 小时前
设计模式之建造者模式
java·设计模式·建造者模式
菜菜-plus14 小时前
java 设计模式 模板方法模式
java·设计模式·模板方法模式
萨达大14 小时前
23种设计模式-模板方法(Template Method)设计模式
java·c++·设计模式·软考·模板方法模式·软件设计师·行为型设计模式
机器视觉知识推荐、就业指导16 小时前
C++设计模式:原型模式(Prototype)
c++·设计模式·原型模式
阳光开朗_大男孩儿16 小时前
组合模式和适配器模式的区别
设计模式·组合模式·适配器模式