0.. 想定読者
- 未経験で転職を目指す初学の人
- 未経験で転職した人
- デザインパターンを学びたい人
1.. はじめに
記事を見てきださった皆さんありがとうございます。株式会社Nucoで見習いエンジニアとして働いている@noshishiと申します。まさに、クソコードを量産する典型的な未経験転職者です。
最近、コードを'チョットカケル'レベルになりましたが、書いたコードやプログラム間の関係性がまだまだ理解不足です。「よしよし、ここまでうまく行って、、、、ええ!なんでそれ動くん?」とお祭り騒ぎです。そんな時に出会ったのが、デザインパターンです。
2.. デザインパターン
デザインパターンは、プログラムをどう意識して書けば良いかを、教えてくれる指南書。
ちなみにソフトウェア開発には、先人たちが苦労し作り上げてくれたデザインパターンがあるそうです。今回の記事では、Observerパターン(Publisher Subscriberパターン)について書いていきます。
3.. Observerパターン
このパターンでは、二人の登場人物がいて、各々に役割があります。なぜこのパターンがよく使われるかは後半で記述します。
[登場人物]
Subject(被観察者)
Observer(観察者)
[役割]
「Subject」は、自分自身で処理した内容をObserverに知らせる
「Observer」は、Subjectから知らせを受けて、自分の処理を進める
現実社会に置き換えると、、、
[登場人物]
Subject(被観察者) - アイドル
Observer(観察者) - ファン
[役割]
「アイドル」は、新曲ができたらファンに伝える。
「ファン」は、新曲の知らせを聞いてすぐタワレコに駆け込む。
といった感じでしょうか。
実際にコードを書いていきたいと思います。
コード例
Subjectは、(人感)センサーで、Observerは、電灯やタイマーだとしましょう。
# Subject
class Sensor(object):
def __init__(self) -> None:
self.observers = []
# 通知を送るリストに観察者を登録する
def add_observer(self, observer: object) -> None:
self.observers.append(observer)
# 通知を送るリストから観察者を削除する
def del_observer(self, observer: object) -> None:
self.observers.remove(observer)
# 人が通ったことを観察者に通知する
def notice(self) -> None:
for observer in self.observers:
observer.update()
# Observer
class Light(object):
def __init__(self) -> None:
# 内部のオン(1)オフ(0)の変数
self.switch = 0
# 通知が送られてきたら
def update(self) -> None:
# 内部のスイッチをオンにする
self.switch = 1
# 点灯を実行
self.turn_on()
def turn_on(self):
if self.switch == 1:
print('点灯!')
else:
print('点灯できません')
# Observer
class Timer(object):
def __init__(self) -> None:
# 内部のオン(1)オフ(0)の変数
self.switch = 0
# 通知が送られてきたら
def update(self) -> None:
# 内部のスイッチをオンにする
self.switch = 1
# タイマーを起動
self.start()
def start(self) -> None:
if self.switch == 1:
print('タイマー起動!')
else:
print('タイマー起動できません')
「Subject」は、通知を送るObserverたちを管理(今回であればリストで保持し、リストへの追加と削除を行なっています)し、お知らせを送ることができます。お知らせの処理の中で、Observeのメソッドが書かれているのが重要なポイントです。
「Observer」は、Subjectが通知を送った時に、自動的にupdateメソッドが呼び出されるので、呼び出された後の処理を記述します。Lightであれば電灯をつけるみたいなこと。
それでは、メインの処理を書いていきます。
sensor = Sensor()
light = Light()
timer = Timer()
sensor.add_observer(light)
sensor.add_observer(timer)
sensor.notice()
点灯!
タイマー起動!
「Subject」である Sensorが通知(notifyメソッドを読んだら)したら、LightとTimerが自動的に起動してくれました。
このパターンが効果を発揮するのは、以下のような状況です。
①Observerの柔軟な追加
もし新たに、lightをLEDlightにしたい場合どうしたらいいでしょうか。
# Observer
class LEDLight(object):
def __init__(self) -> None:
# 内部のオン(1)オフ(0)の変数
self.switch = 0
# 通知が送られてきたら
def update(self) -> None:
# 内部のスイッチをオンにする
self.switch = 1
# 点灯を実行
self.turn_on()
def turn_on(self) -> None:
if self.switch == 1:
print('LED点灯!')
else:
print('LED点灯できません')
led_light = LEDLight()
sensor.add_observer(led_light)
新しくLEDlightのクラスを作って、そのobjectを「Subject」の通知リストに追加するだけで同じ挙動が実現できます。
LEDlightクラスの実装は、突っ込んで言えば、通知を受けたいなら、updateメソッドがないといけないということになります。(Sensorクラスのnoticeメソッドで記述しているObserverが持つべきメソッドを、実装させることを約束させているともいえるかもしれません。)
②Subjectの柔軟な展開
今回はObserverのリストを一つしか作っていませんが、二つ作ってみるとどうでしょうか。
# Observer
class Sensor(object):
def __init__(self) -> None:
self.light_observers = []
self.timer_observers = []
# 通知を送るリストにライト関係の観察者を登録する
def add_light_observer(self, light_observer: object) -> None:
self.light_observers.append(light_observer)
# 通知を送るリストからライト関係の観察者を削除する
def del_light_observer(self, light_observer: object) -> None:
self.light_observers.remove(light_observer)
# 人が通ったことをライト関係の観察者に通知する
def notice_light_observer(self) -> None:
for light_observer in self.light_observers:
light_observer.update()
# 通知を送るリストにタイマー関係の観察者を登録する
def add_timer_observer(self, timer_observer: object) -> None:
self.timer_observers.append(timer_observer)
# 通知を送るリストからタイマー関係の観察者を削除する
def del_timer_observer(self, timer_observer: object) -> None:
self.timer_observers.remove(timer_observer)
# 人が通ったことをタイマー関係の観察者に通知する
def notice_timer_observer(self) -> None:
for timer_observer in self.timer_observers:
timer_observer.update()
こうすると、Subjectの挙動に応じて、反応するObserverを変えることができます。
③subjectの柔軟な変更
今度は(人感)センサーじゃなくて、単なるスイッチならどうなるでしょうか?(少しづつ通知という概念から離れて、実装するクラスに言葉をあわせてみたいと思います。)
# subject
class Switch(object):
def __init__(self) -> None:
self.machines = []
# スイッチが押されると起動したい機器を登録する
def add_machine(self, machine: object) -> None:
self.machines.append(machine)
# 上記で追加した登録機器を削除する
def del_observer(self, machine: object) -> None:
self.machines.remove(machine)
# スイッチが押されたことを各機器に流す
def send(self) -> None:
for machine in self.machines:
machine.run()
こうすると、同じような記述で、別のSubjectを作成することができました。
Observerパターンのよいところ
「Subject」や「Observer」の入れ替えことができること。この柔軟な入れ替えをよく疎結合(Low Coupling)と呼ばれたりします。入れ替えることができるということは、お互いに影響を与えていない、つまり、あらぬプログラムの実行を防ぐことが目的でもあります。
テトリスをされた方はなんとなくぴんと来るのではないでしょうか。テトリスでは同じ動きをする(同じルールである)のに見た目を色々変更することができます。つまり、テトリスのルールがSubjectでテトリスの見た目がObserverと考えることができる気がします。(実際どうかは知りません)
テトリスのテーマデザイン一覧
影響があるクラス同士だと常に両者を意識して開発したり、修正したりしなければならないので、コード数が多ければ多いほど煩雑になってしまいます。一方でこうしたパターンを活用すると、小規模の改修でソフトウェアをアップデートすることもできます。
ちなみに、このパターンとほぼ同意義としてMVCというソフトウェアアーキテクチャが存在するそうです。qiitaではとても投稿されている記事だと思いますので、私も勉強していきます。
4.. Publisher Subscriber パターン
Observerパターンの進化系、もっと疎結合(暗黙的な結合)を実現するパターンです。登場人物と役割を書いてみましょう。
[登場人物]
Publisher(出版者) 元Subject
Broker(仲介者) New!
Subscriber(購読者)元Observer
[役割]
「Publisher」は、自分自身で処理した内容をBrokerに知らせる
「Broker」は、Publisherから受けた通知を対象となるSubscriberに通知する
「Subscriber」は、Brokerから知らせを受けて、自分の処理を進める
Brokerという新しい登場人物が挟まっています。コードは少し増えて複雑に見えますが、この一人の登場で物語は大きく変わっていきます。名前の通り本の出版と購読でコードを書いてみようと思います。
コード例
# Publisher 本の著者
class BookWriter(object):
def __init__(self, name: str) -> None:
self.name = name
# 本を書く
def write(self, book_name: str, book_gerne: str) -> None:
self.book = [book_name, book_gerne]
# 指定した本を本屋に置いてもらう(通知する)
def send_bookstore(self, bookstore) -> None:
bookstore.get_book(self.name, self.book)
# Broker 本屋
class BookStore(object):
def __init__(self, name: str) -> None:
self.name = name
# 本はジャンルごとに保管する
self.book_bookshelf = {
'novels': [],
'comic books': []
}
# ジャンルごとに通知リストを作成する
self.novels_readers = []
self.comic_books_readers = []
# 興味のあるジャンルごとの新着通知リストに読者を追加する
def add_reader(self, readers) -> None:
for reader in readers:
if reader.favorite_gerne == 'novels':
self.novels_readers.append(reader)
elif reader.favorite_gerne == 'comic books':
self.comic_books_readers.append(reader)
else:
print(f'この本屋では{reader.favorite_gerne}を扱っていません')
# 興味のあるジャンルごとの新着通知リストから読者を削除する
def del_reader(self, readers) -> None:
for reader in readers:
if reader.favorite_gerne == 'novels':
self.novels_readers.remove(reader)
elif reader.favorite_gerne == 'comic books':
self.comic_books_readers.remove(reader)
else:
print(f'この本屋では{reader.favorite_gerne}を扱っていません')
# 本を入荷する
def get_book(self, writer_name: str, book: list) -> None:
book_name = book[0]
book_gerne = book[1]
self.book_bookshelf[book_gerne].append([book_name, writer_name])
# 読者にお知らせする
def notify(self) -> None:
for reader in self.novels_readers:
reader.get_info(self.book_bookshelf['novels'])
for reader in self.comic_books_readers:
reader.get_info(self.book_bookshelf['comic books'])
# Subscriber 読者
class Reader(object):
def __init__(self, name: str, favorite_gerne: str) -> None:
self.name = name
self.favorite_gerne = favorite_gerne
# 好きなジャンルの本が入荷したら本屋から通知を受け取る
def get_info(self, book: list) -> None:
self.read(book)
# (買って)本を読む
def read(self, book_infos: list) -> None:
print(f'[{self.name}]')
for i, book_info in enumerate(book_infos):
print(f'{i+1}冊目の{book_info[1]}さんの書いた「{book_info[0]}」って本、面白い!')
「Publisher」は、本を書いて本屋にお知らせします。
「Broker」は、作者からもらった本をジャンルごとに書店に並べ、ジャンルごとの読者を管理します。ある程度、本が溜まった時点で、対象となる読者に本をの入荷をお知らせします。
「Reader」は、本屋から通知をもらうと、すぐに本を読みはじめます。
実際に動かしてみると、
# 作者と本屋と読者のオブジェクトを作成
bookwriter = BookWriter(name='noshishi')
bookstore = BookStore(name='qiita bookstore')
readerA = Reader(name='A', favorite_gerne='novels')
readerB = Reader(name='B', favorite_gerne='novels')
readerC = Reader(name='C', favorite_gerne='comic books')
# 読者が自分の好きなジャンルの本のお知らせを受けるように本屋にお願いする
bookstore.add_reader([readerA, readerB, readerC])
# 作者が本を書いて、本屋に送る
# 一冊目
bookwriter.write(book_name='思い出と音楽', book_gerne='novels')
bookwriter.send_bookstore(bookstore=bookstore)
# 二冊目
bookwriter.write(book_name='星空の馬車', book_gerne='novels')
bookwriter.send_bookstore(bookstore=bookstore)
# 三冊目
bookwriter.write(book_name='てんてこ未経験', book_gerne='comic books')
bookwriter.send_bookstore(bookstore=bookstore)
# 本屋が読者の好きなジャンルごとに知らせを送る
bookstore.notify()
[A]
1冊目のnoshishiさんの書いた「思い出と音楽」って本、面白い!
2冊目のnoshishiさんの書いた「星空の馬車」って本、面白い!
[B]
1冊目のnoshishiさんの書いた「思い出と音楽」って本、面白い!
2冊目のnoshishiさんの書いた「星空の馬車」って本、面白い!
[C]
1冊目のnoshishiさんの書いた「てんてこ未経験」って本、面白い!
自分の好きなジャンルの本だけ読むことができています。(上手くいって嬉しい)
Publisher Sbscriberパターンでは、本屋(Broker)がいることで作者(Publisher)と読者(Subscriber)の関係をより間接的にしています。この関係性のおかげで、作者は読者を意識(読者を操作するコードの実装を)する必要がなく、一方で読者も作者を意識する必要がありません。この実装を行うと、Observerパターンの柔軟性という恩恵をさらに受けることができます。
作者が頑張って読者を管理しなければならなかったのが、新しい作者は本を本屋に置くだけでいい。読者は、作者ごとに何度も購読のお願いをしなければならなかったのが、一回のサブスクでジャンルごとで通知を受けることができる。全ては本屋という仲介者がコントロールしてくれる。
5.. さいごに
最後までご覧いただきありがとうございました。デザインパターンの魅力を感じていただけたでしょうか?(クソコードであることはご容赦ください。)他にも、AdapterやStrategyやDecoratorなど面白いデザインパターンがあるみたいです。もっとコードが書けるようになれば、一個づつ書いていければなと思っています。もし、このデザインパターンが見たいとかあればぜひコメントいただけると、喜んで書きます。また、ご指摘もありがたくいただきたいです!
ちなみに、私はデザインパターンというものにお目にかかったのは、オンライン大学でソフトウェア工学という授業でした。その時のテキストがオープンソースなので、下記の通り共有させていただきます。
Marsic, I. (2012, September 10). Software engineering. Rutgers The State University of New Jersey.
株式会社Nucoについて
現在、弊社は採用活動中です!もしご興味を頂けたらぜひ下記フォームよりご応募いただけると泣いて喜びます。
ちなみに、私は完全未経験者です。元々、関西の田舎の公務員をしていて、入社するまでコードを書いたことすらありませんでした。
でも、政策を考えて実行することとソフトウェアを開発して実行する過程は、似ている部分はあるのかなと思います。どうゆう構造で、どうゆう順序で、どんな手段でより良いものを生み出すか。このデザインパターンもそうですが、俯瞰的に考えたり、見たりするのがとても好きです。なので、プログラミングを今は得意ではなくても、ものづくりが好きとか、事象の抽象化が好きな方には、とても魅力的な仕事であり、弊社は魅力的な職場だと感じています。
ぜひご応募お待ちしております!
参考サイト・書籍
Refactoring.Guru
いろんなデザインパターンをさまざまな言語を使いながら、非常にコンパクトに学べるサイトです。
Observer vs Pub-Sub pattern(by Ahmed shamim hassan)
ObserverパターンとPub/Subパターンの比較をしている記事です。めちゃくちゃわかりやすい。
Java言語で学ぶデザインパターン入門(by 結城浩)
デザインパターンを学ぶ登竜門のような書籍です。
Python Design Patterns(by Brandon Rhodes)
pythonでデザインパターンを学べるサイトです。