背景
行为模型主要控制类与类之间的行为。这对于解耦来说非常重要,如何划分出各司其职的类,把握好粒度,控制他们消息传递的流程显得非常重要,这些模型有时候比较奇怪。要慢慢体会。
模型简介
下面我们来看看6种行为设计模式
1. MEMENTO(备忘录)
1.1 意图
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
1.2 适用性
在以下情况下使用备忘录模式:
- 必须保存一个对象在某一个时刻的 (部分)状态, 这样以后需要时它才能恢复到先前的状态。
- 如果一个用接口来让其它对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性。
1.3 结构图
1.4 效果
备忘录模式有以下一些效果:
- 保持封装边界 使用备忘录可以避免暴露一些只应由原发器管理却又必须存储在原发器之外的信息。该模式把可能很复杂的Orignator内部信息对其他对象屏蔽起来 , 从而保持了封装边界。
- 它简化了原发器 在其他的保持封装性的设计中, Originator负责保持客户请求过的内部状态版本。这就把所有存储管理的重任交给了 Originator。让客户管理它们请求的状态将会简化 Originator, 并且使得客户工作结束时无需通知原发器。
- 使用备忘录可能代价很高 如果原发器在生成备忘录时必须拷贝并存储大量的信息 , 或者客户非常频繁地创建备忘录和恢复原发器状态,可能会导致非常大的开销。除非封装和恢复 Originator状态的开销不大, 否则该模式可能并不合适。参见实现一节中关于增量式改变的讨论。
- 定义窄接口和宽接口 在一些语言中可能难以保证只有原发器可访问备忘录的状态。
- 维护备忘录的潜在代价 管理器负责删除它所维护的备忘录。然而 , 管理器不知道备忘录中有多少个状态。因此当存储备忘录时,一个本来很小的管理器,可能会产生大量的存储开销。
注意
这种模式仅仅是保存一个类中的变量。对这些变量进行封装。然后恢复。
2. OBSERVER(观察者)
2.1 意图
定义对象间的一种一对多的依赖关系 ,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。
2.2 适用性
在以下任一情况下可以使用观察者模式 :
- 当一个抽象模型有两个方面 , 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
- 当对一个对象的改变需要同时改变其它对象 , 而不知道具体有多少对象有待改变。
- 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之 , 你不希望这些
对象是紧密耦合的。
2.3 结构图
2.4 效果
Observer模式允许你独立的改变目标和观察者。你可以单独复用目标对象而无需同时复用其观察者, 反之亦然。它也使你可以在不改动目标和其他的观察者的前提下增加观察者。下面是观察者模式其它一些优缺点 :
- 目标和观察者间的抽象耦合 一个目标所知道的仅仅是它有一系列观察者 , 每个都符合抽象的Observer类的简单接口。目标不知道任何一个观察者属于哪一个具体的类。这样目标和观察者之间的耦合是抽象的和最小的。因为目标和观察者不是紧密耦合的 , 它们可以属于一个系统中的不同抽象层次。一个处于较低层次的目标对象可与一个处于较高层次的观察者通信并通知它 , 这样就保持了系统层次的完整。如果目标和观察者混在一块 , 那么得到的对象要么横贯两个层次 (违反了层次性), 要么必须放在这两层的某一层中(这可能会损害层次抽象)。
- 支持广播通信 不像通常的请求, 目标发送的通知不需指定它的接收者。通知被自动广播给所有已向该目标对象登记的有关对象。目标对象并不关心到底有多少对象对自己感兴趣 ;它唯一的责任就是通知它的各观察者。这给了你在任何时刻增加和删除观察者的自由。处理还是忽略一个通知取决于观察者。
- 意外的更新 因为一个观察者并不知道其它观察者的存在 , 它可能对改变目标的最终代价一无所知。在目标上一个看似无害的的操作可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新。此外 , 如果依赖准则的定义或维护不当,常常会引起错误的更新 , 这种错误通常很难捕捉。简单的更新协议不提供具体细节说明目标中什么被改变了 , 这就使得上述问题更加严重。如果没有其他协议帮助观察者发现什么发生了改变,它们可能会被迫尽力减少改变。
2.5 注意
这种对于解耦非常重要。其实我们要广义的理解通知,从根源上讲也就是通讯。从语法上讲也就是一个if语句决定通知给谁。如何处理。在android系统中广播是个典型的观察者模式,我认为contentprovider也是的。但是他决定了获取那个组件。这里还是通讯。
3. STATE(状态)
3.1 意图
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
3.2 适用性
在下面的两种情况下均可使用State模式:
- 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。
- 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常 , 有多个操作包含这一相同的条件结构。State模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。
3.3 结构图
3.4 效果
State模式有下面一些效果:
- 它将与特定状态相关的行为局部化,并且将不同状态的行为分割开来State模式将所有与一个特定的状态相关的行为都放入一个对象中。因为所有与状态相关的代码都存在于某一个State子类中, 所以通过定义新的子类可以很容易的增加新的状态和转换。另一个方法是使用数据值定义内部状态并且让 Context操作来显式地检查这些数据。但这样将会使整个Context的实现中遍布看起来很相似的条件语句或 case语句。增加一个新的状态可能需要改变若干个操作, 这就使得维护变得复杂了。State模式避免了这个问题 , 但可能会引入另一个问题 , 因为该模式将不同状态的行为分布在多个State子类中。这就增加了子类的数目,相对于单个类的实现来说不够紧凑。但是如果有许多状态时这样的分布实际上更好一些 , 否则需要使用巨大的条件语句。正如很长的过程一样,巨大的条件语句是不受欢迎的。它们形成一大整块并且使得代码不够清晰,这又使得它们难以修改和扩展。State模式提供了一个更好的方法来组织与特定状态相关的代码。决定状态转移的逻辑不在单块的if或switch语句中, 而是分布在State子类之间。将每一个状态转换和动作封装到一个类中,就把着眼点从执行状态提高到整个对象的状态。这将使代码结构化并使其意图更加清晰。
- 它使得状态转换显式化 当一个对象仅以内部数据值来定义当前状态时 , 其状态仅表现为对一些变量的赋值,这不够明确。为不同的状态引入独立的对象使得转换变得更加明确。而且, State对象可保证Context不会发生内部状态不一致的情况,因为从Context的角度看,状态转换是原子的 — 只需重新绑定一个变量(即Contextt的State对象变量),而无需为多个变量赋值
- State对象可被共享 如果State对象没有实例变量 — 即它们表示的状态完全以它们的类型来编码 — 那么各Context对象可以共享一个State对象。当状态以这种方式被共享时 , 它们必然是没有内部状态, 只有行为的轻量级对象(参见Flyweight)。
3.5 注意
这里有事根据情况使用不同的类。这里是为了封装,找到本质就是那个判断if或者switch。
4. STRATEGY(策略)
4.1 意图
定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
4.2 适用性
当存在以下情况时使用Strategy模式
- 许多相关的类仅仅是行为有异。 “策略”提供了一种用多个行为中的一个行为来配置一个类的方法。
- 需要使用一个算法的不同变体。例如,你可能会定义一些反映不同的空间 /时间权衡的算法。当这些变体实现为一个算法的类层次时 ,可以使用策略模式。
- 算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构。
- 一个类定义了多种行为 , 并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。
4.3 结构图
4.4 效果
Strategy模式有下面的一些优点和缺点:
- 相关算法系列 Strategy类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能。
- 一个替代继承的方法 继承提供了另一种支持多种算法或行为的方法。你可以直接生成一个Context类的子类,从而给它以不同的行为。但这会将行为硬行编制到 Context中,而将算法的实现与Context的实现混合起来 ,从而使Context难以理解、难以维护和难以扩展,而且还不能动态地改变算法。最后你得到一堆相关的类 , 它们之间的唯一差别是它们所使用的算法或行为。将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展。
- 消除了一些条件语句Strategy模式提供了用条件语句选择所需的行为以外的另一种选择。当不同的行为堆砌在一个类中时 , 很难避免使用条件语句来选择合适的行为。将行为封装在一个个独立的Strategy类中消除了这些条件语句。
- 实现的选择 Strategy模式可以提供相同行为的不同实现。客户可以根据不同时间 /空间权衡取舍要求从不同策略中进行选择。
- 客户必须了解不同的Strategy 本模式有一个潜在的缺点,就是一个客户要选择一个合适的Strategy就必须知道这些Strategy到底有何不同。此时可能不得不向客户暴露具体的实现问题。因此仅当这些不同行为变体与客户相关的行为时 , 才需要使用Strategy模式。
- Strategy和Context之间的通信开销 无论各个concretStrategy实现的算法是简单还是复杂, 它们都共享Strategy定义的接口。因此很可能某些 concretStrategy不会都用到所有通过这个接口传递给它们的信息;简单的 concretStrategy可能不使用其中的任何信息!这就意味着有时Context会创建和初始化一些永远不会用到的参数。如果存在这样问题 , 那么将需要在Strategy和Context之间更进行紧密的耦合。
- 增加了对象的数目 Strategy增加了一个应用中的对象的数目。有时你可以将Strategy实
现为可供各Context共享的无状态的对象来减少这一开销。任何其余的状态都由 Context维护。Context在一次对Strategy对象的请求中都将这个状态传递过去。共享的Strategy不应在各次调用之间维护状态.Flyweight模式更详细地描述了这一方法。
4.5 注意
有一个类用来处理,使逻辑清晰。解耦。还是switch或者if语句
5. TEMPLATE METHOD(模板方法)
5.1 意图
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Templatemethod使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
5.2 适用性
模板方法应用于下列情况:
- 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
- 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。这是Opdyke和Johnson所描述过的“重分解以一般化”的一个很好的例子。首先识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
- 控制子类扩展。模板方法只在特定点调用操作 ,这样就只允许在这些点进行扩展。
5.3 结构图
5.4 效果
模板方法是一种代码复用的基本技术。它们在类库中尤为重要,它们提取了类库中的公共行为。
模板方法导致一种反向的控制结构,这种结构有时被称为“好莱坞法则,即“别找我们,我们找你。这指的是一个父类调用一个子类的操作,而不是相反。模板方法调用下列类型的操作:
- 具体的操作(ConcretClass或对客户类的操作) 。
- 具体的Abstraclass的操作(即,通常对子类有用的操作) 。
- 原语操作(即,抽象操作) 。
- Factory Method。
- 钩子操作(hook operations) ,它提供了缺省的行为,子类可以在必要时进行扩展。一个
钩子操作在缺省操作通常是一个空操作。
5.5 注意
这里仅仅是使用继承的语法。对于必要的扩展或者算法的控制。
6. VISITOR(访问者)
6.1 意图
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
6.2 适用性
在下列情况下使用Visitor模式:
- 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。Visitorr使得你可以将相关的操作集中起来定义在一个类中。当该对象结构被很多应用共享时,用 Visitorr模式让每个应用仅包含需要用到的操作。
- 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需
6.3 结构图
6.4 效果
下面是访问者模式的一些优缺点:
1. 访问者模式使得易于增加新的操作 访问者使得增加依赖于复杂对象结构的构件的操作变得容易了。仅需增加一个新的访问者即可在一个对象结构上定义一个新的操作。相反,如果每个功能都分散在多个类之上的话,定义新的操作时必须修改每一类。
2. 访问者集中相关的操作而分离无关的操作 相关的行为不是分布在定义该对象结构的各个类上,而是集中在一个访问者中。无关行为却被分别放在它们各自的访问者子类中。这就既简化了这些元素的类,也简化了在这些访问者中定义的算法。所有与它的算法相关的数据结构都可以被隐藏在访问者中。
3. 增加新的ConcreteElement类很困难 Visitor模式使得难以增加新的E lement的子类。每添加一个新的 ConcreteElement都要在 Visitor中添加一个新的抽象操作,并在每一个ConcretVisitor类中实现相应的操作。有时可以在 Visitor中提供一个缺省的实现,这一实现可以被大多数的ConcreteVisitor继承,但这与其说是一个规律还不如说是一种例外。所以在应用访问者模式时考虑关键的问题是系统的哪个部分会经常变化,是作用于对象结构上的算法呢还是构成该结构的各个对象的类。如果老是有新的 ConcretElement类加入进来的话,vistor类层次将变得难以维护。在这种情况下,直接在构成该结构的类中定义这些操作
可能更容易一些。如果Element类层次是稳定的,而你不断地增加操作获修改算法,访问者模式可以帮助你管理这些改动。
4. 通过类层次进行访问 一个迭代器(参见Iterator)可以通过调用节点对象的特定操作来遍历整个对象结构,同时访问这些对象。但是迭代器不能对具有不同元素类型的对象结构进行操作。
5. 累积状态 当访问者访问对象结构中的每一个元素时,它可能会累积状态。如果没有访问者,这一状态将作为额外的参数传递给进行遍历的操作,或者定义为全局变量。
6. 破坏封装 访问者方法假定ConcreteElement接口的功能足够强,足以让访问者进行它们的工作。结果是,该模式常常迫使你提供访问元素内部状态的公共操作,这可能会破坏它的封装性。
注意
其实跟观察者模式非常接近,仅仅多了决定那个可以被接受。
分析
其实每种模式都是具有语法支持的,关键要知道特定的语法本质,关系模型的一切都是为了降低各个类中的耦合度。一定要思考本质。