5
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

デザインパターン : オブザーバー (C++)

5
Last updated at Posted at 2022-01-13

デザインパターン : オブザーバー (C++)

デザインパターンとは

問題に対する解決策をパターン化(抽象化)したものです。

オブザーバーとは

オブザーバーとは、通知する側と通知される側の関係が1対多で、両者の直接的な依存関係を防ぎつつ、通知を実現する手法のことです。直接的な依存関係を防ぐために、継承を利用します。通知する側は通知される側の親クラスのインターフェースだけ知っておけばOKということになります。本来、class Hoge1がclass Hoge2に通知したい場合は、class Hoge2のヘッダーファイルをclass Hoge1の実装コードに取り入れる必要があります。同様に、class Hoge1がclass Hoge2~10に通知したい場合は、class Hoge2~10のヘッダーファイルをclass Hoge1の実装コードに取り入れる必要があります。面倒ですが、通知するためには、class Hoge2~10のインターフェースをclass Hoge1が知る必要があります。オブザーバーは、コールバックと似ています。コールバックも呼び出し元が呼び出し先の詳細を知る必要はないからです。

※「通知する」と「通知される」とは、class Hoge1がclass Hoge2の関数を呼び出した時、Hoge1はHoge2に通知すると言います。Hoge2はHoge1から通知されると言います。
※インターフェースというのは、ここではC++特有の話とかではなく、クラス名や関数名や入力や戻り値の組み合わせのことを指しています。

実装例1

実装例を2つ用意しました。最初の実装例は、通知する側(Subjectクラス)が通知される側(Observer1=3)のUpdate関数を呼び出すだけの実装例です。

オブザーバーを利用しないで実装

まずはデザインパターンを利用しないで、実装していきます。
main.cpp
#include "Subject.h"
#include "Observer1.h"
#include "Observer2.h"
#include "Observer3.h"

using namespace std;

int main(){
    Observer1 observer1;
    Observer2 observer2;
    Observer3 observer3;

    Subject subject(observer1, observer2, observer3);

    subject.NofifyAll();
	return 0;
}

main.cppでは、通知される側(Observer1~3クラス)と通知する側(Subjectクラス)を用意して、SubjectクラスのNofifyAll()を実行しているだけです。

Observer1.h
class Observer1{
    public:
        void Update();
};
Observer1.cpp
#include <iostream>
#include "Observer1.h"
using namespace std;

void Observer1::Update(){
    cout << "Observer1::Updateが呼び出された!!" << endl;
}

Observer1.hはObserver1クラスのインターフェースを公開し、Observer1.cppでは、Observer1::Update関数を実装しているだけです。Observer1::Update関数と名前がついていますが、文字列を画面に表示するだけの実装になっています。実際の運用では、Observer1の状態(メンバ変数とか)が変更されるような実装になるはずです。

Observer2.h
class Observer2{
    public:
        void Update();
};
Observer2.cpp
#include <iostream>
#include "Observer2.h"
using namespace std;

void Observer2::Update(){
    cout << "Observer2::Updateが呼び出された!!" << endl;
}
Observer3.h
class Observer3{
    public:
        void Update();
};
Observer3.cpp
#include <iostream>
#include "Observer3.h"
using namespace std;

void Observer3::Update(){
    cout << "Observer3::Updateが呼び出された!!" << endl;
}


Observer2~3は同じなので、添付だけしておきます。

Subject.h
class Observer1;
class Observer2;
class Observer3;

class Subject{
    public:
        Subject(Observer1& observer1, Observer2& observser2, Observer3& observser3);
        void NofifyAll();//全てのオブザーバーに通知する。
    private:
        Observer1* observer1_;
        Observer2* observer2_;
        Observer3* observer3_;
};
Subject.cpp
#include "Subject.h"
#include "Observer1.h"
#include "Observer2.h"
#include "Observer3.h"

Subject::Subject(Observer1& observer1, Observer2& observer2, Observer3& observer3){
    this->observer1_ = &observer1;
    this->observer2_ = &observer2;
    this->observer3_ = &observer3;
}

void Subject::NofifyAll(){
    this->observer1_->Update();
    this->observer2_->Update();
    this->observer3_->Update();
}

Subject.cppでは、Subject::NofifyAll()関数を実装したり、初期化したりしています。デザインパターンを利用しないと、Subject.cppではObserver1~3のヘッダーファイルをインクルードしているところに注目してください。デザインパターンを利用すると、Subject.cppから、Observer1~3のヘッダーファイルが消えます。

上のファイルをビルドしましょう。

clang++ -g -O0 -o main.o -c main.cpp -std=c++11 
clang++ -g -O0 -o Subject.o -c Subject.cpp -std=c++11
clang++ -g -O0 -o Observer1.o -c  Observer1.cpp -std=c++11
clang++ -g -O0 -o Observer2.o -c  Observer2.cpp -std=c++11
clang++ -g -O0 -o Observer3.o -c  Observer3.cpp -std=c++11
clang++ -o obserber main.o Subject.o Observer1.o Observer2.o Observer3.o 

ターミナル上で.

./observer

と実行すると、

Observer1::Updateが呼び出された!!
Observer2::Updateが呼び出された!!
Observer3::Updateが呼び出された!!

と表示されます。

次は、オブザーバーというデザインパターンを採用して、上の実装コードを改善していきます。

オブザーバーを利用して実装

main.cpp
#include "Subject.h"
#include "MyObserver1.h"
#include "MyObserver2.h"
#include "MyObserver3.h"

using namespace std;

int main(){
    MyObserver1 my_observer1;
    MyObserver2 my_observer2;
    MyObserver3 my_observer3;

    Subject subject;
    subject.AddObserver(my_observer1);
    subject.AddObserver(my_observer2);
    subject.AddObserver(my_observer3);
    subject.NofifyAll();
	return 0;
}

今回のmain.cppでは、Subject::AddObserverを追加したことぐらいで、元のmain.cppとやっていることは大差ないです。ちなみに、MyObserver1~3はそれぞれ異なるのに、Subject::AddObserverを呼び出せる理由は、Subject::AddObserverの引数は親クラスへの参照を要求しているからです。Subjectは派生クラスの存在を知りません。オブザーバーというデザインパターンの強みです。クラスSubjectとMyObserver1~3の結合度は前章の実装よりも、低いことが分かります。

Observer.h
#pragma once
class Observer{
    public:
        virtual void Update() = 0;
};

Observer.hはObserverクラスのインターフェースを定義しています。Observer::Update関数に前と後ろにvirtualと=0がついています。これにより、抽象クラスとして、Observerを記述することができます。C++の仕様です。インターフェース部分だけを定義したものだと思ってください。このクラスをSubjectは知る必要がありますが、Observerの派生クラスをSubjectは知る必要がありません。

MyObserver1.h
#pragma once
#include "Observer.h"

class MyObserver1:public Observer{
    public:
        void Update();
};
MyObserver1.cpp
#include "MyObserver1.h"
#include <iostream>
using namespace std;

void MyObserver1::Update(){
    cout << "MyObserver1::Updateが呼び出された!!" << endl;
}
MyObserver2.h
#pragma once
#include "Observer.h"

class MyObserver2:public Observer{
    public:
        void Update();
};
MyObserver2.cpp
#include "MyObserver2.h"
#include <iostream>
using namespace std;

void MyObserver2::Update(){
    cout << "MyObserver2::Updateが呼び出された!!" << endl;
}
MyObserver3.h
#pragma once
#include "Observer.h"

class MyObserver3:public Observer{
    public:
        void Update();
};
MyObserver3.cpp
#include "MyObserver3.h"
#include <iostream>
using namespace std;

void MyObserver3::Update(){
    cout << "MyObserver3::Updateが呼び出された!!" << endl;
}

MyObserver1~3は同じような実装なので、まとめて紹介します。MyObserver1~3はObserverクラスを継承したクラスです。何度も言いますが、SubjectクラスはMyObserver1~3の存在を知りません。ですが、通知はできます。

Subject.h
#pragma once
#include <vector>
class Observer;

class Subject{
    public:
        void AddObserver(Observer& observer);
        void NofifyAll();//全てのオブザーバーに通知する。
    private:
        std::vector<Observer*> observers_;
};
Subject.cpp
#include "Observer.h"
#include "Subject.h"

void Subject::AddObserver(Observer& observer){
    this->observers_.push_back(&observer);
}

void Subject::NofifyAll(){
    for(int i=0; i<this->observers_.size(); i++){
        this->observers_[i]->Update();
    }
}

Subject.cppを読んだら分かりますが、MyObserver1~3のヘッダーファイルを取り込んでません!!親クラスへのポインタや参照に派生クラスの番地を与えると、親クラスは派生クラスとして振舞うようになっています。これを利用することにより、通知する側(Subjectクラス)は、通知される側(Observerの派生クラス)を知らなくても、済むようになってるわけです。

長くなりましたが、ビルドしてみます。

clang++ -g -O0 -o main.o -c main.cpp -std=c++11 
clang++ -g -O0 -o Subject.o -c Subject.cpp -std=c++11
clang++ -g -O0 -o MyObserver1.o -c MyObserver1.cpp -std=c++11
clang++ -g -O0 -o MyObserver2.o -c MyObserver2.cpp -std=c++11
clang++ -g -O0 -o MyObserver3.o -c MyObserver3.cpp -std=c++11
clang++ -o obserber main.o MyObserver1.o MyObserver2.o MyObserver3.o Subject.o

実行結果は、前章と同じようなもので、省略します。

※本当は通知する側(Subjectクラス)も継承すべきなのですが、今回は省略しました。

まとめ

メリットは通知する側が、通知される側の詳細(派生クラス)を知らなくても良いということです。どんなに派生クラスが増えても、通知する側は親クラスのインターフェースだけ知っておけば良いということになります。
5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?