设计模式之“Observer”注疏#01

时间:2021-12-10 16:35:58

设计模式之“Observer”注疏#01

原文首发于我的微信公众号:GeekArtT.

Observer设计模式是为了解决“信息同步更新”的问题而存在的。它试图解决这样一个问题:如果有“一堆对象”都跟随“某一对象”的变化而变化,那么,如何能够保持“这堆对象”能够同步更新呢?特别是,“这堆对象”很可能在运行时(run-time)不断被添加或者被删除,你设计的机制该如何既能保持增添/删除的灵活性,而又能使“当前这堆对象”同步更新呢?

仅仅是抽象地看待上述这个问题,或许很难有思路。解决方案的形成可以依托于生活中具体业务场景的类比。

很多书籍介绍观察者(Observer)这个设计模式喜欢运用“内容订阅者”和“发布信息平台”的对应关系。这个比喻从生产流程上讲,完全符合,但是作为比喻,还是显得有些生硬。我认为,一个更好的、更富有启发性的类比是“品牌商店”与“旗下加盟商/分店”在价格上的依存关系。好不夸张的说,一旦做出这个类比,解决方案就会自然浮现。

例如,McDonald在一座城市开店,其价格都是高度统一。如果某一个分店的价格低于别的分店,就会打乱跨国大公司的整体战略部署,是绝不允许的。所以,一个核心的目标是,每一个分店的价格都必须时刻同步统一。另一方面,每个地区的分店,都有可能随着市场的反馈而做出调整:或者因为销量过低而关闭,或者因为某个新的商圈的出现而增加分店。那么,在分店的数目、位置都在随时变动的情形下,应该用一套什么机制才能保证分布在城市各地的分店都使用统一的价格呢?

一旦有了恰当的比喻,问题自然迎刃而解。保持分店的价格有什么困难的?

在总店的通讯本上,记录下各个分店的电话号码。一旦有了新的价格策略,立马根据通讯本上记录的电话号码,拨打各个分店的电话,通知各个分店。如果建立了新的分店,就在总店的通讯本上添加新分店的电话号码;如果关闭了一家分店,就在总店通讯本上去掉相应分店的电话号码。由此,各分店的价格便能够同步统一,且保持相对好的灵活性。

有了这段生活场景作为指导纲领,我们便可以写出具体的Observer设计模式的代码了。考虑到GoF(Design Patterns)这本书的巨大影响,我首先展示如何运用上述类比一步步写出GoF中Observer设计模式这一节的示例代码。

在GoF中,示例代码的应用场景是:如果我有一个class能够实时获取当前时间,但是,我又想用两种方式Digital Clock和Analog Clock的方式去做展示(可以想象为`10:28`和`10h28min`这样不同的展示方式)。

首先设计“分店”类。它最主要的功能就是根据“总店”通报的信息,将自己的信息更新。对应GoF的C++代码,Subject类充当了“总店”的角色。

class Subject;

class Observer {
public:
virtual ~Observer();
virtual void Update(Subject* theChangedSubject) = 0;
protected:
Observer();
}

这里Update()方法是需要有Subject类作为参数的。因为,作为分店,自己是不可以任意地改动价格的,你有且只能根据“总店”的消息去做变更,别的数据源都是禁止的。

然后设计“总店”类,用它来衍生出“能够实时获取当前时间的类”。按照之前的思路,这个类除了必要的构造/析构函数外:

  • 需要有一个“通讯本”记录各分店的电话号码。

  • 相应地,应该具备添加/删除电话号码的方法。

  • 另外,还应当有一个方法,能够根据通讯本上记录的信息拨打各个分店的电话号码。这一行为抽象地说,即是能够根据一个信息记录本,来逐一通知各个“分店”进行信息更新。

GoF中的C++的代码如下:

class Subject {
public:
virtual ~Subject(); virtual void Attach(Observer *);
virtual void Detach(Observer *);
virtual void Notify();
protected:
Subject();
private:
List<Observer*> *_observers;
};

这里,列表_observers充当了通讯本的角色,用它来保存各个“分店”的指针。本来,指针即是变量的门牌号。把“分店”的这个号码记录在通讯本上,就能够任意地访问“分店”。从这点上来说,它比仅仅记录的电话号码还要强大有用。

Attach()Detach()方法自然就是用于添加/删除这个通讯本的信息了。而Notify()即是字面意思所表述的,用于通知各个分店“价格有变”。而通知的目的,实质是让各分店做更新,所以,这里作为父类的Notify()是有统一的更新方法:

void Subject::Notify() {
ListIterator<Observer*> i(_observers); for(i.First(); !i.IsDone(); i.Next()) {
i.CurrentItem()->Update(this);
}
}

这里所用的方法,即是依次将通讯本上的分店调取出来,让它们各自调用自己的Update()方法,并将自己,this,作为参数传入。告知分店把自己作为信息源来进行同步更新。

构造完父类,下面可以构建具体的时钟类。首先是能够实时获取时间的“总店”类:

class ClockTimer : public Subject {
public:
ClockTimer(); virtual int GetHour();
virtual int GetMinute();
virtual int GetSecond(); void Tick();
} void ClockTimer::Tick() {
// update internal time-keeping state
// ...
Notify();
}

这里,Tick()方法用于定时抓取最新的时间状态信息。

然后是第一个“分店”类:

class DigitalClock: public Widget, public Observer {
private:
ClockTimer* _subject;
public:
DigitalClock(ClockTimer*);
virtual ~DigitalClock(); virtual void Update(Subject*);
// overrides Observer operation virtual void Draw();
// overrides Widget operation;
// defines how to draw the digital clock
}; DigitalClock::DigitalClock(ClockTimer* s) {
_subject = s;
_subject->Attach(this);
} DigitalClock::~DigitalClock() {
_subject->Detach(this);
}

这个类里面,运用了private变量_subject去保存它的数据源,并在构造函数中将其初始化。之后便可以运用这个_subject去核对Update是否是被正确的数据源通知,以此来作为更新与否的标准:

void DigitalClock::Update(Subject* theChangedSubject) {
if(theChangedSubject == _subject) {
Draw();
}
} void DigitalClock::Draw() {
// get the new values from the subject
int hour = _subject->GetHour();
int minute = _subject->GetMinute();
// etc. // draw the digital clock
}

之后是第二个“分店”AnalogClock的构建:

class AnalogClock : public Widget, public Observer {
private:
ClockTimer* _subject;
public:
AnalogClock(ClockTimer*);
virtual ~AnalogClock(); virtual void Update(Subject*);
virtual void Draw();
// ...
}; // ...

构建完了这三大部分,便可以直接使用上面完成的Observer模式了:

ClockTimer* timer = new ClockTimer;

AnalogClock* analogClock = new AnalogClock(timer);
DigitalClock* digitalClock = new DigitalClock(timer);

这样,每当timer中的Tick()方法抓取到了新的时间信息,两个Clock就会得到及时更新,并作出相应地不同展示。


如果你喜欢我的文章或分享,请长按下面的二维码关注我的微信公众号,谢谢!

设计模式之“Observer”注疏#01