この記事は一休.comアドベントカレンダー2017の10日目です。
システム本部 CTO室 エンジニアの @yu-sa です。
今回はとある開発で、ドメイン駆動設計で,インターフェース指向を意識した環境での開発に携わった際の知見を記事にさせて頂きたいと思います。
自分は今まで、SmartUIな開発ばかりしてきたため、今回の開発では多くを勉強させていただきました。そんな経験談や調査内容をまとめて共有したいと思います。
参考記事
ドメイン駆動設計の道標
Python におけるドメイン駆動設計(戦術面)の勘どころ
[DDD]ドメイン駆動設計で実装を始めるのに一番とっつきやすいアーキテクチャは何か
ぼくのかんがえたさいきょうのうぇぶあぷりけーしょんふれーむわーく
最後のまとめをはじめに
- アーキテクチャと実装例を見て、ドメイン駆動設計のイメージを理解。
- ユビキタス言語についての理解を深める。
- ドメイン駆動設計とインターフェース指向を掛け合わせる事で再利用性と学習コストの低減を実現。
レイヤーの分離を強く意識したアーキテクチャ
ドメイン駆動設計で広く提案されているアーキテクチャはいくつかあります。
- レイヤードアーキテクチャ
- ヘキサゴナルアーキテクチャ
- オニオンアーキテクチャ
- クリーンアーキテクチャ
このアーキテクチャは、ドメイン駆動設計において絶対に守らなくてはいけない手法というよりも、この手法を雛形、もしくは参考にして各プロジェクトに合わせて最適なレイヤー構成を構築することが重要とされています。
ここでは、実際にドメイン駆動設計に触れて見た経験をいかして、簡単な例題実装とアーキテクチャを元に解説してきたいと思います。
実装
「料理情報を返すAPI」をpython Flaskで作って見たいと思います。
とりあえず、RDBにmysqlを利用している簡単なAPIを作成します。
今回は、見やすくするために1つのファイルに実装していきます。
# python 3.6.1
# pip install Flask
# pip install mysql-connector-python
from enum import Enum, unique
from flask import Flask, jsonify
from mysql import connector
app = Flask(__name__)
connect = connector.connect(
user='root',
password='',
host='127.0.0.1',
database='cooking',
charset='utf8'
)
db = connect.cursor(dictionary=True)
# ----------------------------------
# domain layer
# ----------------------------------
# ValueObject
@unique
class Cuisine(Enum):
Japanese = ('japanese', '和食')
Western = ('western', '洋食')
Chinese = ('chinese', '中華')
def __new__(cls, value, *args):
obj = object.__new__(cls)
obj._value_ = value
return obj
def __init__(self, *args):
self.values = args
def to_label(self):
return self.values[1]
# Entity
class Dish:
def __init__(self, id: int, name: str, cuisine: str) -> None:
self.id: int = id
self.name: str = name
self.cuisine: Cuisine = Cuisine(cuisine)
def to_dict(self):
return dict(
dish_id=self.id,
name=self.name,
cuisine=dict(
value=self.cuisine.value,
label=self.cuisine.to_label(),
)
)
# Repository
class DishRepository:
fields: list = ['id', 'name', 'cuisine']
tabel: str = 'dish'
@classmethod
def find(cls, db, conditions: dict={}) -> list:
query = "SELECT id, name, cuisine FROM dish {where}"
where = ""
if conditions:
c_list = ["{}={}".format(*k_v) for k_v in conditions.items()]
c_str = " AND ".join(c_list)
where = "WHERE {conditions}".format(conditions=c_str)
db.execute(query.format(where=where))
return db.fetchall()
@classmethod
def find_with_dishes(cls, db, conditions: dict={}) -> list:
rows = cls.find(db, conditions)
return [Dish(**val) for val in rows]
# ----------------------------------
# application layer
# ----------------------------------
# Controller
@app.route('/dishes')
def dishes():
dishes = DishRepository.find_with_dishes(db)
return jsonify(
dict(
dishes=[dish.to_dict() for dish in dishes]
)
)
if __name__ == '__main__':
app.debug = True
app.run(host='0.0.0.0', port=8080)
起動して
$ python app.py
* Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
こんな感じです。
アーキテクチャ
解説
- アーキテクチャの各レイヤーの内容がソースコードとして実装されています。
- ソースコードはアーキテクチャの低位レイヤーから順に実装しております。
- アーキテクチャは高位レイヤーから下に向かって依存(アクセス)可能となります。
- 低位レイヤーは高位のレイヤーに依存(アクセス)する事はできません。
- 今回はソースコード1つで実装していますが、記事としてみやすくするためであり、本来の開発であれば適切なファイル分割が必要です。
レイヤー解説
- 各レイヤーに対応したclassやmethodは担当する機能を適切に実装しております。
- ValueObject
- 属性という要素を強く持っており、基本的には不変な情報。一意に認識する必要のないモノです。
- 今回は、料理ジャンルとしてCuisineという値オブジェクトを作成しました。
- 属性というだけあって、付加情報を持っていることがあるため、今回はpythonのenumを利用して、属性情報と共にlabelに該当する付加情報を持たせています。
- Entity
- 一意な情報を表現するモノです。基本的には情報が適切に変化していきます。
- Repositoryよりも低位レイヤー。Repositoryにて利用されるため。また、EntityからRepositoryを呼び出して検索などを行わないため。
- Repository
- 検索や永続化を担当するレイヤー。
- Factoryという、EntityやValueObjectの複雑な生成を担当するレイヤーも概念として存在するのだが、今回は未実装。
- Controller
- ビジネスにとって意味があるものを実装し、基本的に薄く保たれるべきレイヤー。
- 薄くというと実装量が少なくというイメージも持たれるが、そういう意味ではなく複雑なロジックを含めないという意味合いが強い。
- ドメインレイヤーとは強く分離を意識して実装をする必要がある。
- 基本的に、MVCモデルでのコントローラの役割と変わらないイメージ。
- ValueObject
ユビキタス言語について
Eric Evans氏が『Domain Driven Design』において
開発者とユーザーとの間で共通の厳格な意味を持つ用語を構築するというプラクティスを表すために使用した用語である。
と言っています。
自分の理解では「全ての人にとっての共通して理解できる言語」を決めて使うことで「ビジネスに置いてのドメイン(問題領域)を一緒に解決する」為に役立てる事がこのユビキタス言語においての重要な部分だと理解しました。
そのような、ユビキタス言語を設計、実装のレイヤーで適応する事でのメリットについて、私見を述べたいと思います。
ユビキタス言語による恩恵
共通の言語を利用するということを実装において、意識する事は結局「誤解を招かない言葉を選定する」という事です。
日本語を主に利用している自分なんかは、この言葉の選定でつまづきやすいですし、時間を取られる部分です。ただ、その内容を熟考し適切かどうかを検討し、メンバにレビューを頼む事でこの内容が「チーム内での共通の言葉」になっていくと考えています。
今回の料理法の属性を表しているCuisineなんかも、ただ料理とした場合「food」「dish」「cooking」等の言葉が存在します。
ですが、今回の用途として料理法のカテゴリとしての位置付けが強い属性のため、Cuisineという単語を選びました。この選定により、このカテゴリがただ、料理ではなく料理法について言及しているということが明確になり、自己記述的または自己説明的なモノとなっていきます。
ユビキタス言語を導入することで「共通認識の言葉」と「誤解を発生させないコード」が構築され可読性も上がり、教育コストも低減されていくというメリットがあると考えています。
インターフェース指向の導入による効果
インターフェース指向を導入することでのメリットで一番にあげやすいのは「再利用性の向上」です。
このメリットを実現させるためのキーワードとして「疎結合」があります。
細分化された個々のコンポーネント同士の結びつきが比較的緩やかで、独立性が強い状態のことである。
疎結合なインターフェースを実現するためには「独立性」を実現する必要があり、その実現により「再利用性の向上」を実現できます。
ただ、この効果以外にも今回のドメイン駆動との併用によるシナジーについて、書きたいと思います。
ユビキタス言語とのシナジー
今回、開発において自分がもっとも勉強させて頂いたのは、ユビキタス言語を採用することによる「共通認識の言葉」でインターフェースを構築する事による「自己説明的なインターフェースの提供」を実現できた事です。
実際のコードを交えて、実現できた内容を説明したいと思います。
作りの悪い「エンジニア」の「仕事」に関しての実装例を見て見ましょう。
import datetime
class Engineer:
def work(self, start_time: datetime.datetime, end_time: datetime.datetime, work: dict):
self.start_time = start_time
self.meeting = work.get('meeting1')
self.coding = work.get('coding1')
self.lunch = work.get('lunch')
self.coding += work.get('coding2')
self.meeting += work.get('meeting2')
self.end_time = end_time
> import datetime
> company = Engineer()
> start_time = datetime.datetime.now()
・・・・・・・・・・・・・
退社時間まで待機
> end_time = datetime.datetime.now()
> work = {'meeting1': "朝会で◯◯の案件を提案", 'coding1': "**の実装開始", "lunch": "牛丼を食べた", "coding2": "**の実装でつまづいた"; "meeting2": "夕会で**の実装についてつまづいた部分のヘルプを要請"}
> company.work(start_time, end_time, work)
このように、全く独立性を実現せず、ユビキタス言語を採用しないで実装した場合
- meetingが朝会と夕会同じ扱いになっており、見た人が勘違いをしてしまう可能性がある。
- workに全ての情報を詰め込んでいるため、独立性が実現できておらず、使い勝手の悪いインターフェースとなっている。
- workとして実装しているのにlunchのような休憩時間までworkとして処理しているため、分離が行えていない。
とりあえず、他にも色々問題点がありますが、上記のようにひどい実装では利用者が幸せになれませんし、このコードを開発する担当者も辛い思いしかしません。
ただ、ここでユビキタス言語を採用し、インターフェース指向を導入する事で
import datetime
class MeetingMixin:
def morning_meeting(self, contents: str):
self.work['meeting.morning'] = contents
def evening_meeting(self, contents: str):
self.work['meeting.evening'] = contents
class AttendanceMixin:
def going_work(self, datetime: datetime.datetime):
self.work['attendance.going_work'] = datetime
def leaving_work(self, datetime: datetime.datetime):
self.work['attendance.leaving_work'] = datetime
class BreakingMixin:
def breaking_lunch(self, contents: str):
self.breaking['breaking.lunch'] = contents
class WorkMixin(MeetingMixin, AttendanceMixin):
def work_coding(self, contents: str):
self.work['coding'] = contents
class Engineer(WorkMixin, BreakingMixin):
pass
のような実装になり
> import datetime
> engineer = Engineer()
> engineer.going_work = datetime.datetime.now()
> engineer.morning_meeting = "朝会で◯◯の案件を提案"
> engineer.work_coding = "**の実装開始"
> engineer.breaking_lunch = "牛丼を食べた"
> engineer.work_coding = "**の実装でつまづいた"
> engineer.evening_meeting = "夕会で**の実装についてつまづいた部分のヘルプを要請"
・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
退勤時間に
> engineer.leaving_work = datetime.datetime.now()
のように、利用できます。
インターフェースを適切に分割しユビキタス言語を適応させた事で
- workとbreakingの分離が行えて、適切なレイヤー構成となり理解しやすくなりました。
- engineer.going_workで「エンジニアが出勤」したのだなと理解できるようになり、利用者に対しての機能の説明をしなくても理解できる実装となりました。
- 各行動(インターフェース)毎に分割した事により、インターフェースを利用者にとって使いやすいものとなり、利用者への布教に貢献できます。
- エンジニアが営業になったとしても必要なインターフェースを継承することで再利用できるようになりました。
のような、利益が得られました。
また、最も重要な内容なのですが「共通言語を利用し独立性を実現したインターフェースの実装」により「各インターフェースが自己説明的な実装となり学習コストが低減」を実現することができました。
自分が携わった開発では、ワークアラウンドな処理部分以外でのコメントはほぼ無いにもかかわらず、何をしているのかの内容がすぐに入ってくるような作りを実現できていました。
まとめ
- アーキテクチャと実装例を見て、ドメイン駆動設計をイメージをつけていただく。
- ユビキタス言語についての理解を深める。
- ドメイン駆動設計とインターフェース指向を掛け合わせる事で再利用性と学習コストの低減を実現できる。
明日は @kokojima の一休のデータフローをAirflowを使って実行してみる です。