Observer Pattern

このエントリの目指すところ

Observer Pattern は大きく以下の観点があると仮説をたて,それが妥当なのか,あるいは過不足があるのかを考える.

  1. データモデルとそれに対する操作の参照関係を 1..* を実現しつつ操作同士を疎結合に保つ
    • Subject が Model だとすると,そのモデルが保持する値の変更に伴って処理を担う ViewModel や Controller が呼ばれなければならない.しかし Model のある値が変化したからといって,Model 自身はどんな処理をすべきかは知らない (知るべきではない).
    • したがって Model は「値が変化した」という事実を外部に公開し,それをトリガとして操作を受け付ける必要がある.これを実現する典型的な実装が Callback の登録である.

例にもれず実装はする.

調査

語られる像

整理

疑問

要点

実例

設計

環境

$ cat /etc/os-release 
PRETTY_NAME="Ubuntu 22.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.2 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
$ clang++ --version
Ubuntu clang version 14.0.0-1ubuntu1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
$ scons --version
SCons by Steven Knight et al.:
        SCons: v4.0.1.c289977f8b34786ab6c334311e232886da7e8df1, 2020-07-17 01:50:03, by bdbaddog on ProDog2020
        SCons path: ['/usr/lib/python3/dist-packages/SCons']
Copyright (c) 2001 - 2020 The SCons Foundation

実装

関連するファイル数が多いため,重要と思われるファイルをデフォルトで展開するようにした.

design_pattern/observer_pattern.cpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#include "observer_pattern/observer_pattern.hpp"

using design_pattern::observer_pattern::Display;
using design_pattern::observer_pattern::ObservableLine;
using design_pattern::observer_pattern::ObservableTriangle;

/**
 * 画面上に図形を配置するようなケースを考える.
 * ObservableDiagram がモデル,Display はモデルを利用するプレゼンテーションのイメージである.
 * モデルの状態が変化すると Display に通知され,モデルが自動的に操作 (画面更新,ここでは標準出力) される.
 */
int main() {
  // モデルを利用するオブジェクト (Observer)
  auto display = Display();

  // 描画するモデルを生成する.
  auto line = std::make_shared<ObservableLine>();
  auto triangle = std::make_shared<ObservableTriangle>();

  // 画面に図形を配置する.実態は Observer が Obsavable なオブジェクトの購読を始める.
  display.Register(line);
  display.Register(triangle);

  // diagram の状態を変化させるごとに自動的に dump される.
  // モデルは自身がどう扱われるか気にしないが Observer が状態を監視しており,状態が変化した後になされる操作が
  // 自動的に実行される.プログラマもモデルの状態を変化させるだけでよく,そのあと行われる処理は別の文脈で考えればよい.
  line->Resize(100, 100);
  line->Resize(0, 0);
  line->Resize(101, 101);
  triangle->Resize(100, 100);
  triangle->Resize(0, 0);
  triangle->Resize(101, 101);

  return 0;
}
design_pattern/observer_pattern/observer_pattern.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVER_PATTERN_HPP_
#define DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVER_PATTERN_HPP_

#include "observer_pattern/Display.hpp"
#include "observer_pattern/Observable.hpp"
#include "observer_pattern/ObservableDiagram.hpp"
#include "observer_pattern/ObservableLine.hpp"
#include "observer_pattern/ObservableTriangle.hpp"
#include "observer_pattern/Observer.hpp"

#endif  // DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVER_PATTERN_HPP_
design_pattern/observer_pattern/Display.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DESIGN_PATTERN_OBSERVER_PATTERN_DISPLAY_HPP_
#define DESIGN_PATTERN_OBSERVER_PATTERN_DISPLAY_HPP_

#include <algorithm>
#include <iostream>
#include <memory>
#include <string>
#include <vector>

#include "../../join.hpp"
#include "Observable.hpp"
#include "ObservableDiagram.hpp"
#include "Observer.hpp"

namespace design_pattern::observer_pattern {

/**
 * 簡易的にプレゼンテーションを表現する.モデルが変更されるたびに画面を更新する.
 */
class Display : public Observer<ObservableDiagram> {
 protected:
  void OnPublished(std::shared_ptr<Observable> sender) override {
    std::cout << std::static_pointer_cast<ObservableDiagram>(sender)->ToString() << std::endl;
  }

 private:
  std::vector<std::shared_ptr<ObservableDiagram>> objects_;
  std::string ToString() {
    auto messages = std::vector<std::string>();
    std::transform(this->objects_.begin(), this->objects_.end(), std::back_inserter(messages),
                   [](std::shared_ptr<ObservableDiagram> &subject) { return subject->ToString(); });
    return Join("\n", messages);
  }
  void Dump() { std::cout << this->ToString() << std::endl; }
};

}  // namespace design_pattern::observer_pattern

#endif  // DESIGN_PATTERN_OBSERVER_PATTERN_DISPLAY_HPP_
design_pattern/observer_pattern/Observable.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVABLE_HPP_
#define DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVABLE_HPP_

#include <functional>
#include <memory>
#include <vector>

namespace design_pattern::observer_pattern {

/**
 * 監視可能なオブジェクトを表現する
 */
class Observable : public std::enable_shared_from_this<Observable> {
 public:
  void Subscribe(std::function<void(std::shared_ptr<Observable>)> &&func) { subscribers_.push_back(func); }

 protected:
  void Notify() {
    std::for_each(this->subscribers_.begin(), this->subscribers_.end(),
                  [this](auto &func) { func(shared_from_this()); });
  }

 private:
  std::vector<std::function<void(std::shared_ptr<Observable>)>> subscribers_;
};

}  // namespace design_pattern::observer_pattern

#endif  // DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVABLE_HPP_
design_pattern/observer_pattern/ObservableDiagram.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVABLEDIAGRAM_HPP_
#define DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVABLEDIAGRAM_HPP_

#include <memory>
#include <string>
#include <utility>

#include "../component/Diagram.hpp"
#include "Observable.hpp"

using design_pattern::component::Diagram;

namespace design_pattern::observer_pattern {

/**
 * 監視可能な図形を表現する.
*/
class ObservableDiagram : public Observable {
 public:
  virtual void Resize(int height, int width) {
    if (diagram()->height() == height && diagram()->width() == width) return;
    diagram()->Resize(height, width);
    this->Notify();
  }
  std::string ToString() { return diagram()->ToString(); }

 protected:
  explicit ObservableDiagram(std::unique_ptr<Diagram> diagram) : diagram_(std::move(diagram)) {}
  const std::unique_ptr<Diagram> &diagram() { return this->diagram_; }

 private:
  const std::unique_ptr<Diagram> diagram_;
};

}  // namespace design_pattern::observer_pattern

#endif  // DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVABLEDIAGRAM_HPP_
design_pattern/observer_pattern/ObservableLine.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVABLELINE_HPP_
#define DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVABLELINE_HPP_

#include <memory>

#include "../component/Line.hpp"
#include "observer_pattern/ObservableDiagram.hpp"

using design_pattern::component::Line;

namespace design_pattern::observer_pattern {

/**
 * 監視可能な線分を表現する.
*/
class ObservableLine : public ObservableDiagram {
 public:
  ObservableLine() : ObservableDiagram(std::make_unique<Line>()) {}
};

}  // namespace design_pattern::observer_pattern

#endif  // DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVABLELINE_HPP_
design_pattern/observer_pattern/ObservableTriangle.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVABLETRIANGLE_HPP_
#define DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVABLETRIANGLE_HPP_

#include <memory>

#include "../component/Triangle.hpp"
#include "ObservableDiagram.hpp"

using design_pattern::component::Triangle;

namespace design_pattern::observer_pattern {

/**
 * 監視可能な三角形を表現する.
*/
class ObservableTriangle : public ObservableDiagram {
 public:
  ObservableTriangle() : ObservableDiagram(std::make_unique<Triangle>()) {}
};

}  // namespace design_pattern::observer_pattern

#endif  // DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVABLETRIANGLE_HPP_
design_pattern/observer_pattern/Observer.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVER_HPP_
#define DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVER_HPP_

#include <memory>
#include <vector>

namespace design_pattern::observer_pattern {

/**
 * 監視する責務を負う抽象クラスである.
 * テンプレートの型は Observable を継承したクラスでなければならない.
*/
template <typename T>
class Observer {
  static_assert(std::is_base_of<Observable, T>::value, "T must inherit from Observable");

 public:
  // NOLINTNEXTLINE(readability/inheritance)
  virtual void Register(std::shared_ptr<T> subject) final {
    subject->Subscribe([this](std::shared_ptr<Observable> sender) { this->OnPublished(sender); });
    this->objects_.push_back(subject);
  }

  virtual void OnPublished(std::shared_ptr<Observable> sender) = 0;

  virtual ~Observer() {}

 protected:
  std::vector<std::weak_ptr<T>> objects_;  // Weak pointers to avoid circular references
};

}  // namespace design_pattern::observer_pattern

#endif  // DESIGN_PATTERN_OBSERVER_PATTERN_OBSERVER_HPP_
design_pattern/component/Canvas.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DESIGN_PATTERN_COMPONENT_CANVAS_HPP_
#define DESIGN_PATTERN_COMPONENT_CANVAS_HPP_

#include <algorithm>
#include <iostream>
#include <iterator>
#include <memory>
#include <sstream>
#include <string>
#include <vector>

#include "../demangle.hpp"
#include "../join.hpp"
#include "Diagram.hpp"

namespace design_pattern::component {

/**
 * 図形が描画されるキャンバス
 */
class Canvas {
 public:
  void AddDiagram(std::shared_ptr<Diagram> diagram) {
    this->diagram_sets.push_back(std::make_shared<DiagramSet>(diagram));
  }
  void Dump() { std::cout << (this->diagram_sets.size() != 0 ? this->ToString() : std::string("empty")) << std::endl; }
  std::string ToString() {
    auto messages = std::vector<std::string>();
    std::transform(diagram_sets.begin(), diagram_sets.end(), std::back_inserter(messages),
                   [](std::shared_ptr<DiagramSet> diagram_set) { return diagram_set->ToString(); });
    return Join("\n", messages);
  }
  void Move(size_t index, int offset_x, int offset_y) { this->diagram_sets.at(index)->Move(offset_x, offset_y); }
  std::vector<std::shared_ptr<Diagram>> diagrams() {
    auto result = std::vector<std::shared_ptr<Diagram>>();
    std::transform(this->diagram_sets.begin(), this->diagram_sets.end(), std::back_inserter(result),
                   [](std::shared_ptr<DiagramSet> diagram_set_) { return diagram_set_->diagram(); });
    return result;
  }

 private:
  /**
   * Canvas が管理する Diagram のそれぞれがどの位置に存在するかを管理する
   */
  class DiagramSet {
   public:
    explicit DiagramSet(std::shared_ptr<Diagram> diagram, int x = 0, int y = 0) : diagram_(diagram), x_(x), y_(y) {}
    void Move(int offset_x, int offset_y) {
      this->x_ += offset_x;
      this->y_ += offset_y;
    }
    std::shared_ptr<Diagram> diagram() { return this->diagram_; }
    std::string ToString() {
      char buffer[0xFF];
      sprintf(buffer, "x: %d, y: %d - %s", this->x_, this->y_, this->diagram_->ToString().c_str());
      return buffer;
    }

   private:
    std::shared_ptr<Diagram> diagram_;
    int x_;
    int y_;
  };
  std::vector<std::shared_ptr<DiagramSet>> diagram_sets;
};

}  // namespace design_pattern::component

#endif  // DESIGN_PATTERN_COMPONENT_CANVAS_HPP_
design_pattern/component/Diagram.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DESIGN_PATTERN_COMPONENT_DIAGRAM_HPP_
#define DESIGN_PATTERN_COMPONENT_DIAGRAM_HPP_

#include <sstream>
#include <string>

#include "../demangle.hpp"

namespace design_pattern::component {

/**
 * 描画される図形の抽象クラス
 */
class Diagram {
 public:
  Diagram() : height_(0), width_(0) {}
  virtual ~Diagram() = 0;
  /**
   * 自身の型と大きさを文字列で表現する.
   */
  std::string ToString() {
    std::stringstream ss;
    ss << Demangle(typeid(*this)) << " "
       << "(" << this->height() << "," << this->width() << ")";
    return ss.str();
  }
  /**
   * 自身の高さを返す.
   */
  int height() { return this->height_; }
  /**
   * 自身の幅を返す.
   */
  int width() { return this->width_; }
  /**
   * リサイズする.
   */
  virtual void Resize(int height, int width) {
    this->height_ = height;
    this->width_ = width;
  }

 private:
  int height_;
  int width_;
};
Diagram::~Diagram() {}

}  // namespace design_pattern::component

#endif  // DESIGN_PATTERN_COMPONENT_DIAGRAM_HPP_
design_pattern/component/Line.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DESIGN_PATTERN_COMPONENT_LINE_HPP_
#define DESIGN_PATTERN_COMPONENT_LINE_HPP_

#include "Diagram.hpp"

namespace design_pattern::component {

/**
 * 線分を表現するクラス
 */
class Line : public Diagram {};

}  // namespace design_pattern::component

#endif  // DESIGN_PATTERN_COMPONENT_LINE_HPP_
design_pattern/component/Triangle.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DESIGN_PATTERN_COMPONENT_TRIANGLE_HPP_
#define DESIGN_PATTERN_COMPONENT_TRIANGLE_HPP_

#include "Diagram.hpp"

namespace design_pattern::component {
/**
 * 三角形を表現するクラス
 */
class Triangle : public Diagram {};

}  // namespace design_pattern::component

#endif  // DESIGN_PATTERN_COMPONENT_TRIANGLE_HPP_
demangle.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DEMANGLE_HPP_
#define DEMANGLE_HPP_

#include <cxxabi.h>

#include <string>

/**
 * @brief 型名をデマングル化する.
 *
 * @param id デマングル化する型のtype_infoオブジェクト
 * @return std::string デマングル化された型名を表す文字列
 * @throws std::exception デマングル化が失敗した場合に投げられます
 *
 * @details
 * gcc および clang コンパイラでマングル化された型名をデマングル化します.
 */
std::string Demangle(const std::type_info& id) {
  int status;
  char* demangled_name = abi::__cxa_demangle(id.name(), 0, 0, &status);

  if (status == 0) {
    auto result = std::string(demangled_name);
    free(demangled_name);
    return result;
  }

  throw std::exception();
}

#endif  // DEMANGLE_HPP_
join.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef JOIN_HPP_
#define JOIN_HPP_

#include <numeric>
#include <string>
#include <vector>

/**
 * @brief 文字列の配列を指定したデリミタで結合する.
 *
 * @param delimitor 文字列を連結する際の区切り文字
 * @param messages 連結する文字列の配列
 * @return std::string 連結された文字列
 *
 * @details
 * 指定したデリミタで文字列の配列を連結する.
 * デリミタに "," を指定し,文字列の配列として {"Hello", * "World"}
 * を指定した場合,"Hello,World"を返します.
 */
std::string Join(const std::string &delimitor,
                 const std::vector<std::string> &messages) {
  return std::accumulate(
      std::next(messages.begin()), messages.end(), messages[0],
      [delimitor](std::string a, std::string b) { return a + delimitor + b; });
}

#endif  // JOIN_HPP_

実行結果

$ ./content/build/design_pattern/observer_pattern
design_pattern::component::Line (100,100)
design_pattern::component::Line (0,0)
design_pattern::component::Line (101,101)
design_pattern::component::Triangle (100,100)
design_pattern::component::Triangle (0,0)
design_pattern::component::Triangle (101,101)