← 返回 Skills 市场
quochungto

Observer Pattern Implementor

作者 Hung Quoc To · GitHub ↗ · v1.0.0 · MIT-0
cross-platform ✓ 安全检测通过
121
总下载
0
收藏
0
当前安装
1
版本数
在 OpenClaw 中安装
/install bookforge-observer-pattern-implementor
功能描述
Implement the Observer pattern to establish one-to-many dependencies where changing one object automatically notifies and updates all dependents. Use when a...
使用说明 (SKILL.md)

Observer Pattern Implementor

When to Use

A data source is directly calling update methods on its consumers, making it impossible to add or remove consumers without modifying the source. Apply this skill when:

  • An abstraction has two separable aspects — a subject (data) and observers (views/reactions) — and tightly coupling them limits reuse of each independently
  • A change to one object requires notifying an unknown or variable number of other objects, and you don't want to hard-code who those are
  • Objects should be able to notify others without making assumptions about who those objects are — you want zero coupling from subject to observer

Do not apply if:

  • Only one consumer will ever exist and that is certain by design (direct call is simpler)
  • Observers trigger further changes that trigger more observers — cascade control is complex; use ChangeManager (Step 9)
  • The language/framework already provides this mechanism natively (e.g., React state, RxJS, Qt signals) — use those instead

Before starting, verify you can answer: What is the subject (the thing that changes)? What are the observers (the things that react)? What state do observers need from the subject after a change?


Process

Step 1: Set Up Tracking and Audit Existing Coupling

ACTION: Use TodoWrite to track progress, then read the codebase to map current subject-observer coupling.

WHY: Observer refactoring touches at least three participants (Subject, ConcreteSubject, ConcreteObserver) and requires consistent interface changes across all of them. Without tracking, it is easy to partially refactor — adding the Observer interface while leaving direct calls in the subject — which produces a hybrid that is worse than either approach. The audit reveals whether existing coupling is simple (one direct call) or complex (multiple consumers with different update logic), which determines whether ChangeManager is needed.

TodoWrite:
- [ ] Step 1: Audit existing coupling — map subjects and consumers
- [ ] Step 2: Define Subject and Observer interfaces
- [ ] Step 3: Implement ConcreteSubject with Attach/Detach/Notify
- [ ] Step 4: Resolve trigger strategy (who calls Notify)
- [ ] Step 5: Ensure state consistency before Notify
- [ ] Step 6: Choose push vs pull model and implement Update
- [ ] Step 7: Apply aspect-based registration if multiple event types
- [ ] Step 8: Evaluate whether ChangeManager is needed
- [ ] Step 9: Implement ChangeManager if warranted
- [ ] Step 10: Wire up and verify — no dangling references

Use Read and Grep to answer:

  • Which class(es) change state and need to broadcast changes? (Subject candidates)
  • Which class(es) react to those changes? (Observer candidates)
  • How many consumers are there? Are they stable or do they change at runtime?
  • Are there multiple types of events, or one general "something changed" notification?

Step 2: Define the Subject and Observer Interfaces

ACTION: Create abstract Subject and Observer base classes (or interfaces). Do not write any concrete logic yet — define only the contracts.

WHY: The power of Observer comes from the subject knowing only the abstract Observer interface — not any concrete observer type. If you skip this and call concrete methods directly, you recreate the coupling you're trying to eliminate. Defining the interfaces first forces you to commit to the notification contract before any implementation details leak in.

Subject interface — three responsibilities:

class Subject {
public:
    virtual ~Subject();
    virtual void Attach(Observer*);   // register a new observer
    virtual void Detach(Observer*);   // deregister an observer
    virtual void Notify();            // broadcast change to all observers
protected:
    Subject();
private:
    List\x3CObserver*> _observers;       // or a hash map — see Step 3
};

Observer interface — one responsibility:

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

Note: Update receives the subject pointer (not state data). This is the pull model default — observers call back on the subject for what they need. See Step 6 for the push vs pull decision.


Step 3: Implement ConcreteSubject and the Observer List

ACTION: Implement the concrete subject with state, state accessors (GetState/SetState), and a working Notify. Choose the storage strategy for the observer list.

WHY: The naive approach — storing observers directly in every subject instance — works when subjects are few and observers are many per subject. When subjects are many and observers are few (e.g., many small model objects each observed by one dashboard), the per-subject list wastes memory. The hash map trade-off is real: choose based on the ratio of subjects to observers in your system.

Storage decision:

Situation Storage Trade-off
Few subjects, many observers each List\x3CObserver*> in Subject Simple; O(1) attach; O(n) notify
Many subjects, few observers each External hash map (subject → observer list) Eliminates per-subject overhead; increases access cost

Standard Subject implementation:

void Subject::Attach(Observer* o) { _observers->Append(o); }
void Subject::Detach(Observer* o) { _observers->Remove(o); }

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

ConcreteSubject adds only state and accessors:

class ConcreteSubject : public Subject {
public:
    SubjectState GetState() const;
    void SetState(SubjectState s);
private:
    SubjectState _subjectState;
};

Step 4: Decide Who Triggers Notify

ACTION: Choose and implement one of two trigger strategies: automatic (subject triggers) or manual (client triggers).

WHY: This is the most common source of Observer bugs. Automatic triggering ensures observers are never out of sync but fires notifications on every state mutation — including intermediate states in a multi-step update. Manual triggering gives the client control to batch multiple mutations before broadcasting, but adds an obligation: every code path that mutates state must remember to call Notify. Forgetting one call produces stale observers that are hard to debug.

Option A — Automatic trigger (subject calls Notify in SetState):

void ConcreteSubject::SetState(SubjectState s) {
    _subjectState = s;
    Notify();              // automatic — client never calls Notify
}

Use when: mutations are typically single-step, or missing a notification is worse than extra notifications.

Option B — Manual trigger (client calls Notify):

// Client code
subject->SetState(s1);
subject->SetBoundary(b);  // multiple mutations
subject->Notify();         // one notification for both changes

Use when: performance matters and multiple state changes should batch into one notification round.

Recommendation: Default to Option A. Switch to Option B only if profiling shows notification overhead is significant, or if the use case explicitly requires batching (e.g., multi-step transaction commit).


Step 5: Ensure Subject State Is Self-Consistent Before Notify

ACTION: Audit every operation that calls Notify (directly or via SetState) and verify that all subject state is fully updated before Notify fires.

WHY: Observers call GetState on the subject inside their Update method to synchronize. If Notify fires while the subject is in a partially-updated state — for example, after a base class operation but before a subclass operation completes — observers will read inconsistent data and enter an inconsistent state of their own. This is a silent bug: observers receive a valid-looking notification but pull stale or partial data.

Common violation in inheritance:

// WRONG — notification fires before subclass state is updated
void MySubject::Operation(int newValue) {
    BaseClassSubject::Operation(newValue);  // triggers Notify here
    _myInstVar += newValue;                 // too late — observers already ran
}

Fix — use Template Method to control notification timing:

// In abstract Subject — Notify is last
void Text::Cut(TextRange r) {
    ReplaceRange(r);   // redefined in subclasses — all state updated here
    Notify();          // fires only after the complete operation
}

Always ensure Notify is the last operation in any state-mutating method. Document which operations trigger notifications in the class interface comment.


Step 6: Choose and Implement the Push or Pull Model

ACTION: Decide whether the subject sends state data to observers (push) or observers query the subject (pull). Implement Update accordingly.

WHY: This is the core design trade-off for observer usability versus coupling. The pull model makes the subject maximally ignorant of its observers (it sends no assumptions about what they need), but can be inefficient — each observer must figure out what changed on its own. The push model is efficient for observers with narrow needs but makes the subject assume it knows what observers care about, reducing reusability when different observers need different data.

Pull model — subject sends only its identity; observers pull what they need:

void DigitalClock::Update(Subject* theChangedSubject) {
    if (theChangedSubject == _subject) {  // guard: verify it's the right subject
        int hour = _subject->GetHour();
        int minute = _subject->GetMinute();
        Draw();  // re-render with pulled values
    }
}

Push model — subject sends relevant state as Update arguments:

// Subject
void Subject::Notify(int newValue) {
    for each observer: observer->Update(this, newValue);
}

// Observer
virtual void Update(Subject*, int newValue) = 0;

Decision:

Prefer pull when... Prefer push when...
Different observers need different parts of subject state All observers need the same data
Subject does not know what observers care about The changed data is cheap to pass and expensive to re-query
Maximum observer reusability is important Observer should not need to hold a reference to the subject

Pull is the default. Push reduces reusability because it encodes assumptions about observer needs into the Subject interface.


Step 7: Apply Aspect-Based Registration for Multiple Event Types

ACTION: If the subject produces multiple distinct event types and observers care about only some, extend Attach to accept an "aspect" (event category) parameter.

WHY: Without aspect filtering, every observer is notified of every change — even changes it does not care about. This wastes cycles and can cause incorrect behavior if an observer's Update logic assumes it is only called for its relevant event. Aspect-based registration lets observers declare their interest at subscription time, so Notify only calls the relevant observers.

Extended interfaces:

// Subject registration with aspect
void Subject::Attach(Observer* o, Aspect& interest);

// Observer Update with aspect parameter
void Observer::Update(Subject* s, Aspect& interest);

Notify implementation with aspect filtering:

void Subject::Notify(Aspect& changedAspect) {
    // only notify observers registered for this aspect
    for each (observer, registeredAspect) in _observerMap:
        if registeredAspect == changedAspect:
            observer->Update(this, changedAspect);
}

Use when: subjects produce heterogeneous events (e.g., a document that can have text changes, layout changes, and selection changes), and observers specialize in only one type.


Step 8: Evaluate Whether ChangeManager Is Needed

ACTION: Assess whether the subject-observer dependency graph is simple or complex. If complex, implement ChangeManager (Step 9). If simple, skip Step 9.

WHY: The standard Notify-loop works well when subjects and observers are independent. It breaks down in two scenarios: (1) a single operation modifies multiple interdependent subjects, causing observers to be notified multiple times for what is logically one change; (2) observers themselves observe multiple subjects, creating diamond-shaped dependency graphs where redundant updates proliferate. ChangeManager exists to handle these cases — it absorbs the complexity so subjects and observers don't have to.

Use ChangeManager if any of the following are true:

  • One high-level operation modifies two or more subjects whose observers overlap
  • Any observer observes more than one subject (creating potential for redundant updates)
  • Notification order matters and must be controlled globally
  • You need a specific update strategy (e.g., "update all after all subjects have changed")

If none of the above apply: ChangeManager adds complexity for no benefit. Keep the simple direct Notify loop from Step 3.


Step 9: Implement ChangeManager (Observer + Mediator + Singleton)

ACTION: If Step 8 determined ChangeManager is needed, implement it as a mediator that owns the subject-observer mapping, controls update strategy, and exists as a singleton.

WHY: ChangeManager combines three patterns with a specific purpose: it acts as a Mediator (centralizing the subject-observer communication logic), uses the Observer protocol (subjects and observers still implement the same interfaces), and is a Singleton (there is exactly one ChangeManager per subsystem — subjects and observers both need to find the same instance). This combination eliminates redundant updates when multiple subjects change in one operation, and allows the update strategy to be changed without touching Subject or Observer classes.

ChangeManager responsibilities:

  1. Maintains the subject-to-observer mapping (replaces per-subject observer lists)
  2. Defines the update strategy — when and in what order observers are notified
  3. Updates all dependent observers when a subject requests it

Two built-in strategies:

Strategy Behavior Use when
SimpleChangeManager For each subject, notify all its observers immediately Observers observe only one subject; no redundant update risk
DAGChangeManager Mark all affected observers; update each marked observer exactly once Observers observe multiple subjects (directed acyclic graph dependencies)

ChangeManager interface:

class ChangeManager {
public:
    static ChangeManager* Instance();   // Singleton access
    virtual void Register(Subject*, Observer*);
    virtual void Unregister(Subject*, Observer*);
    virtual void Notify() = 0;          // strategy-defined update dispatch
protected:
    ChangeManager();
private:
    static ChangeManager* _instance;
};

Subject delegates to ChangeManager:

// Subject no longer stores observer list — delegates to ChangeManager
void Subject::Attach(Observer* o) {
    ChangeManager::Instance()->Register(this, o);
}
void Subject::Notify() {
    ChangeManager::Instance()->Notify();
}

DAGChangeManager Notify — prevents redundant updates:

void DAGChangeManager::Notify() {
    // Pass 1: mark all observers that need updating
    for each subject s in _subjects:
        for each observer o of s:
            mark o as pending

    // Pass 2: update each marked observer exactly once
    for each marked observer o:
        o->Update(relevantSubject);
        unmark o;
}

Step 10: Wire Up and Prevent Dangling References

ACTION: Wire all observers to their subjects via Attach. Add lifecycle management to prevent dangling references when subjects are deleted. Verify the full notification chain manually or with a test.

WHY: Deleting a subject that observers still hold a reference to produces dangling pointers — observers will attempt to call GetState on freed memory. Simply deleting the observers is not a valid solution because they may be observing other subjects or be referenced by other objects. The subject must notify observers of its own deletion so they can nullify their references.

Deletion protocol in subject destructor:

Subject::~Subject() {
    // Notify observers that this subject is going away
    // so they can reset their stored reference
    ListIterator\x3CObserver*> i(_observers);
    for (i.First(); !i.IsDone(); i.Next()) {
        i.CurrentItem()->SubjectDeleted(this);
    }
}

// Observer handles deletion
void ConcreteObserver::SubjectDeleted(Subject* s) {
    if (s == _subject) {
        _subject = nullptr;  // nullify — do not delete
    }
}

Wiring example:

ClockTimer* timer = new ClockTimer;
DigitalClock* digitalClock = new DigitalClock(timer);  // Attach in constructor
AnalogClock* analogClock = new AnalogClock(timer);     // Attach in constructor
// When timer ticks, both clocks update automatically

Verification checklist:

  • Attach is called before any state changes that should be observed
  • Detach is called in every observer destructor
  • Deleting a subject does not crash observers
  • Adding a new observer at runtime receives subsequent updates correctly
  • Notifications fire only when subject state is fully consistent (Step 5)

Pitfalls

1. Cascade updates. Observer A's Update changes subject B, which notifies observer C, which changes subject D... This is the most serious Observer failure mode, and it is difficult to detect because it appears during runtime. Prevention: avoid state changes inside Update that would notify other observers. If cascade is unavoidable, use DAGChangeManager to ensure each observer updates exactly once per logical change.

2. Dangling references to deleted subjects. Deleting a subject without notifying observers leaves them with stale pointers. The crash appears on the next notification cycle, not at the point of deletion, making it hard to locate. Prevention: always implement the deletion-notification protocol in Step 10.

3. Inconsistent state notification. Calling Notify before all subject state is updated (typically in inherited operations — see Step 5). Observers run during Update and pull stale data. Prevention: always use Template Method to put Notify last, and audit inheritance chains for calls to super.Operation() that trigger notifications mid-update.

4. Trigger ambiguity. Mixing automatic and manual trigger strategies across different subjects in the same system. Developers expect one behavior and get the other; observers are sometimes notified twice, sometimes not at all. Prevention: pick one strategy and apply it uniformly across the system. Document it in the Subject class comment.

5. Push overspecification. The push model embeds assumptions about what observers need directly into the Subject's Notify signature. When observer needs diverge (one needs the full new state, another only needs a delta, a third only needs to know that something changed), the push signature becomes a compromise that serves none of them well. Prevention: default to pull. Only add push parameters that are universally needed by all observers, and only after profiling shows that the extra pull calls are a measurable cost.


Examples

Example 1: Spreadsheet Model with Multiple Chart Views

Scenario: A spreadsheet has a data model that three chart widgets (bar chart, pie chart, line chart) read directly. Currently the model calls barChart.refresh(), pieChart.refresh(), and lineChart.refresh() in its SetCell method. Adding a fourth chart requires modifying the model.

Trigger: "Every new chart type requires touching the spreadsheet model."

Process:

  • Step 1: Audit reveals SpreadsheetModel directly references three concrete chart classes
  • Step 2: SpreadsheetModel becomes ConcreteSubject; each chart implements Observer
  • Step 3: List\x3CObserver*> in Subject (few subjects, multiple observers)
  • Step 4: Automatic trigger — SetCell calls Notify() after updating data
  • Step 5: Notify is last in SetCell — state is fully updated first
  • Step 6: Pull model — charts call GetCellValue(row, col) in their Update
  • Step 8: No complex dependencies — skip ChangeManager

Output: SpreadsheetModel has no reference to any chart class. New charts attach themselves; model never changes.


Example 2: Clock with Multiple Display Observers

Scenario: A ClockTimer (concrete subject) notifies time displays. A DigitalClock and AnalogClock both observe the same timer. This is the canonical sample from the GoF text.

Trigger: "Both clock displays must always show the same time, but neither should know about the other."

Key implementation details:

  • DigitalClock inherits from both Widget (UI toolkit) and Observer (combining concern 10 — Subject/Observer in one class hierarchy via multiple inheritance)
  • Constructor calls _subject->Attach(this); destructor calls _subject->Detach(this) — full lifecycle management
  • Update guards on subject identity: if (theChangedSubject == _subject) — handles potential future case of observing multiple subjects
void DigitalClock::Update(Subject* theChangedSubject) {
    if (theChangedSubject == _subject) {
        Draw();
    }
}

void DigitalClock::Draw() {
    int hour   = _subject->GetHour();
    int minute = _subject->GetMinute();
    // render digital display
}

Output: ClockTimer::Tick() calls Notify(); both displays update independently without knowing each other exist.


Example 3: Multi-Subject Dashboard with ChangeManager

Scenario: A financial dashboard has three data subjects: PriceSubject, VolumeSubject, and PositionSubject. A RiskSummaryObserver observes all three. When a trade executes, all three subjects change in one operation. Without ChangeManager, RiskSummaryObserver.Update fires three times for one logical event.

Trigger: "Our risk view recalculates three times per trade because it observes three subjects."

Process:

  • Step 8: Observer observes multiple subjects — DAG dependency. ChangeManager required.
  • Step 9: Implement DAGChangeManager. Each subject calls ChangeManager::Instance()->Notify(). DAGChangeManager marks RiskSummaryObserver when PriceSubject changes, marks it again when VolumeSubject changes (already marked — no-op), then updates it once.

Output: One trade → three subject changes → RiskSummaryObserver.Update called exactly once.


Reference Files

File Contents
references/observer-implementation-guide.md All 10 implementation concerns in full detail, ChangeManager class diagrams, push/pull protocol variants, aspect-based registration patterns, and language-specific notes

License

This skill is licensed under CC-BY-SA-4.0. Source: BookForge — Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides.

Related BookForge Skills

Install related skills from ClawhHub:

  • clawhub install bookforge-design-pattern-selector
  • clawhub install bookforge-behavioral-pattern-selector

Or install the full book set from GitHub: bookforge-skills

安全使用建议
This skill appears internally consistent and intended to refactor your code to implement the Observer pattern. Before running it: 1) Make a backup or create a feature branch so you can review changes; 2) Require the agent to produce a diff/PR rather than applying changes directly if you want human review; 3) Ensure automated tests run after the refactor (observer changes can introduce subtle ordering, consistency, or lifetime bugs); 4) Limit the agent's file-write scope to the relevant project directories; 5) Review any produced changes for dangling-reference handling, notification timing, and whether a ChangeManager (more complex) is actually needed. If you want extra assurance, ask the agent to list exactly which files it will modify before applying edits.
功能分析
Type: OpenClaw Skill Name: bookforge-observer-pattern-implementor Version: 1.0.0 The skill bundle is a pedagogical tool designed to guide an AI agent through implementing the Observer design pattern based on the Gang of Four (GoF) principles. The instructions in SKILL.md and the detailed guide in references/observer-implementation-guide.md provide a structured, 10-step process for refactoring code to decouple subjects from observers. The bundle uses standard file manipulation tools (Read, Write, Grep) and contains no evidence of malicious intent, data exfiltration, or harmful prompt injection. All content is strictly aligned with its stated purpose of software architectural improvement.
能力评估
Purpose & Capability
Name, description, and declared required tools (Read, Write, TodoWrite, optional Grep/Glob) match an implementation/refactor skill for the Observer pattern. There are no unrelated environment variables, binaries, or install steps requested.
Instruction Scope
SKILL.md instructs the agent to audit code, define interfaces, implement Attach/Detach/Notify, consider push vs pull, and optionally implement a ChangeManager; it explicitly requires a codebase or detailed design. The instructions require reading and writing the user's codebase (expected for a refactor) but do not direct the agent to read unrelated system files, access secrets, or send data to external endpoints.
Install Mechanism
No install spec and no code files to write to disk by the skill itself. This is an instruction-only skill (lowest install risk).
Credentials
The skill requires no environment variables, credentials, or config paths. The only required capabilities are code-reading and code-writing tools, which are proportionate to the described refactor task.
Persistence & Privilege
always:false (default) and model invocation not disabled — normal for a user-invocable skill. The skill does not request permanent elevated privileges or modification of other skills' configurations.
如何使用
  1. 确保已安装 OpenClaw(本地或 Docker 部署)
  2. 在对话框中输入安装命令:/install bookforge-observer-pattern-implementor
  3. 安装完成后,直接呼叫该 Skill 的名称或使用 /bookforge-observer-pattern-implementor 触发
  4. 根据 Skill 的参数说明提供必要输入,即可获得结构化输出
版本历史
v1.0.0
Initial release of the Observer Pattern Implementor skill. - Provides a step-by-step process to refactor tightly-coupled notification logic into the Observer pattern. - Includes detailed guidance on interface design, storage strategies, push vs pull update models, and handling complex cases like cascading updates. - Supplies checklists and code snippets for robust implementation and refactoring. - Highlights when not to use the pattern (e.g., for single consumers or when native alternatives exist). - Requires code reading and writing tools to analyze and update the existing codebase.
元数据
Slug bookforge-observer-pattern-implementor
版本 1.0.0
许可证 MIT-0
累计安装 0
当前安装数 0
历史版本数 1
常见问题

Observer Pattern Implementor 是什么?

Implement the Observer pattern to establish one-to-many dependencies where changing one object automatically notifies and updates all dependents. Use when a... 它是一个面向 Claude Code / OpenClaw 的 AI Agent Skill 插件,目前累计下载 121 次。

如何安装 Observer Pattern Implementor?

在 OpenClaw 或 Claude Code 对话框中运行命令「/install bookforge-observer-pattern-implementor」即可一键安装,无需额外配置。

Observer Pattern Implementor 是免费的吗?

是的,Observer Pattern Implementor 完全免费,采用 MIT-0 许可证,可自由下载、安装和使用。

Observer Pattern Implementor 支持哪些平台?

Observer Pattern Implementor 跨平台运行,可在任意部署了 OpenClaw / Claude Code 的环境中使用(cross-platform)。

谁开发了 Observer Pattern Implementor?

由 Hung Quoc To(@quochungto)开发并维护,当前版本 v1.0.0。

💬 留言讨论