GANMA!でDDDをやってみて
から1年くらい経った
Septeni × Scala #3
2015/06/16 杉谷
-配布用-
前説
• GANMA!をDDDで作ってみたので、実際
にどういう組み方をしたのかをご紹介し
ます。
– リリースしてから1年半分の反省も添えてみ
ました。
– 甘いところは多いので、突っ込み所を探す感
じでご覧ください
GANMA!について
• 2013/12〜
• スマホ向けの完全オリジナ
ルコミックを配信するアプ
リ
– アニメ化・単行本化も続々
と。
• 以下の3要素
– サーバー
– リーダー3種
– 管理ツール群
• 今日はサーバーでDDDを
やってみたのお話です。
前提@2013
• なんでDDD?
– なんか前職で流行ってた。
– 詳しくはSepteni×Scala#1の
発表資料をご覧ください。
• DDD扱ったことあった?
– なかった。有給消化中にDDD本を読んでみた
だけ。
– Scalaも初挑戦だった。
“マンガ”のモデリング
少年ジャンプをイメージしつつ
ユビキタス言語や構造を考えてみる。
“マンガ”のモデリング
• 始めにGANMA!のマンガ配信業務について
考えます。
– 紙とは違う点も多々ありますが、基本
「週3で漫画雑誌が公開される」というサー
ビスです。※今は雑誌形式ではなくなったけど。
• この漫画雑誌を全員で「マガジン」と呼
ぶことにして、ドメインを考えてみまし
た。
“マガジン”と”ストーリー”
• “マガジン”の具体例と
して、週刊少年ジャン
プを考えてみます。
• ジャンプには
「ナルト」の「第313
話」といったように
「第XXX話」が収録さ
れています。
• これを“ストーリー”と
呼ぶことにします編集
さんも含めて全員で)
週刊少年ジャンプ
<Magazine>
ナルト 第1話
<Story>
ワンピース 第1話
<Story> …
”ストーリー”と”シリーズ”
• またストーリーは「ナ
ルト」や「ワンピー
ス」など、一連の作品
群への名前があります。
• これを”シリーズ”と呼
ぶことにします。(全
員で以下略…
週刊少年ジャンプ
<Magazine>
ワンピース 第1話
<Story>
ワンピース
<Series>
…
マガジンに係わる業務を考える
閲覧系
• マガジン情報(と、
それに関するもの)
の取得
• マガジンの新刊一覧
を陳列する
• シリーズを通しで読
めるマガジンを陳列
する(いわゆる単行
本)
管理業務
• 作家の管理
• ストーリーの管理
• マガジンの編成
”ストーリー”ほりさげ(1)
• IDがあります(Entity)
• “シリーズ”がいます
• “作者”がいます
– 作者は原作・作画など複数の可能
性がありますが、横着して一つの
み保持で。
– 【悩んだ】クラス名を「Author」
としているが、日頃「おーさー」
ではなく「作者」と呼んでいるの
で「Sakusya」とすべきか?
• 日本語英語の対応さえ固定してれば
問題無し
• “ページ”が含まれます。
– 第一話
• 1ページ目
• 2ページ目
• …
– “ページ”とはS3上などのURLでア
クセスできる画像を指します。
• “サブタイトル”があります
– “第45話 にゃんこ襲来”
– “#183 eyes of the unknown”
– 読み切り等、サブタイトル無しも
存在なのでOption
pages:List[RemoteFile] =
[ S3(“storyId/1.jpg”),
S3(“storyId/2.jpg”),
… ]
id:StoryId subTitle:Option[String]
seriesId:SeriesId authorId:AuthorId
Storyクラス
”ストーリー”ほりさげ(2)
• “ストーリーリポジト
リ”が居ます
– 一覧 / 作者別一覧 / シ
リーズ別一覧業務
– 保存業務(ストーリー登
録・更新機能)
• インフラ層にいる
StoryTableとの仲介で
す。
• “ページ”はStoryに集
約されており、連動
して保存されたり読
み出されてたりしま
す。(リポジトリは居
ない)
StoryRepository
• get(storyId)
• list
• findBy(authorId)
• findBy(seriesId)
• entry(story)
• update(story)
• delete(storyId)
Story … Story
StoryTable
”シリーズ”ほりさげ(1)
• IDがあります(Entity)
• “タイトル”があります
– ≒作品名
– 読み切りでも作品名はあ
るので無題は無いはず?
• こっちにも“作者”がいま
す。
– ストーリーとシリーズで”
別作者が可能”を意味しま
す。
– 実際はほとんどストー
リー作者と一致しますが
例外あり
• Magazine: ドラクエ4コマ
漫画劇場①
• Series:ドラクエ4コマ漫画
劇場(作者:編集部
• Story: 柴田亜美作、衛藤ヒ
ロユキ作、…
title:Stringid:SeriesId
authorId:AuthorId
Seriesクラス
”シリーズ”ほりさげ(2)
• “シリーズリポジト
リ”が居ます
– 一覧 / 作者別一覧
– 保存業務(シリーズ登
録・更新機能)
• インフラ層にいる
SeriesTableとの仲介
です。
SeriesRepository
• get(seriesId)
• list
• find(authorId)
• entry(series)
• delete(seriesId)
Series … Series
SeriesTable
”マガジン”ほりさげ(1)
• IDがあります(Entity)
• “タイトル” があります
• “ストーリー”を収録していますが、
ストーリー以外も収録してそうで
す。
– 表紙とか広告とか告知とか。
– ストーリーと静止画を収録できるよ
うにし、”マガジンアイテム”と呼ぶ
ようにします
• “単行本”という考えが方がありま
す
– “同じシリーズ”の話をある程度の話
数でまとめた”マガジン”
– 単行本でないマガジンを”編成本”と
呼んでいます
– 陳列場所が違う
• 編成本 : 新刊タブ
• 単行本 : ランキングタブ
– seriedBindに値があるかどうかで
区別。
• 【悩み】呼び方も機能も違うわ
けだからクラスで別ける必要が
ある気がする
items:List[MagazineItem]=[
StoryItem(storyId),
ImageItem(imageId),
…]
id:MagazineId title:String
seriesBind
:Option[SeriesId]
Magazineクラス
”マガジン”ほりさげ(2)
• “マガジンリポジト
リ”が居ます
– 一覧
– 保存業務(シリーズ登
録・更新機能)
– ランキング
• 【悩み】ここに居
るのは不適切な気
がする
• インフラ層にいる
MagazineTableとの仲介
です。
• マガジンアイテムはマガ
ジンに集約されていて、
連動して保存されたり読
まれたりします。(リポ
ジトリ無し)
MagazineRepository
• get(magazineId)
• list()
• search()
• delete(magazineId)
• entry(magazine)
• update(magazine)
• ranking()
Magazine Magazine
MagazineTable
…
悩んだ: IDの振り方
• IDにはAUTO_INCREMENTを使うことが多いが、
INSERTするまでIDを振れない問題
– 採番用のテーブルを作る?
• 実装がすこし手間。
• パフォーマンスと負荷集中とデータ量が怖い
– UUID?
• UUIDにはv1とv4がある
– v1 : MACアドレスを使っているので生成機材問わず一意保証
– v4 : 乱数。 一意保証無し。怖い。JDK標準搭載のはこれ。
– そのときはSELECT UUID(); を利用した
• UUIDv1(NICが無いときはv4)。
• 極端には遅くないだろうし、Slaveでも使えるだろうし。
• ちゃんと調べたらSQL使わずともJavaからv1生成可能だった
– Java Uuid Generator (JUG)
IDの振り方別解
• Snowflake系
– タイムスタンプ + シーケンス番号 + 何らかの手段で
付与したデバイスID
– Scalaを用いて分散IDワーカを実装する
(ChwtworkCreator’sNote – かとじゅん氏)
• Timestamp(41bit) + データセンターID(5bit) + ワーカー
ID(5bit) + シーケンス(12bit)
– 軽量なTime-based ID生成器”shakeflake(仮称)”につい
て(Smartnews開発者ブログ)
• 遅延生成
– 素直に後でIDを生成してセット
– 実践ドメイン駆動設計の第5章”エンティティ”に遅延
生成の説明もあり(オススメはしていなかった)
悩んだ: DB → Entity(1)
• DB操作テーブルはInfra層 / Entityは
Domain層
• DB取得結果からEntityを作るとき、素
直に”Entityを作って返す”と書くのは
まずい気がした
– Infra層から上位であるDomain層のメソッ
ド呼び出し
– ID指定でのインスタンス化がどこからでも
出来るのもゆるい感じが。
• あずかり知らないIDを持つEntityが作れてし
まう。
• できれば、ID指定でインスタンス化する手
段はPublicにしたくない感がする
import domain.manga.Series
object SeriesTable {
def get(id) = {
val row = SQL(“select〜…
Series(row[“id”],
row[“title”],
…)
}
}
【インフラ層】
infra/db/manga/SeriesTable.scala
悩んだ: DB → Entity(2)
object SeriesTable{
def get(id){
val row = SQL(“select〜…
(row[“id”],
row[“title”],
…)
}
object SeriesRepository{
def get(row){…
(id, title, authorId,…)
= SeriesTable.get(id)
Series(id, title, authorId, …)
}
というわけで、最初はインフラ層から
はタプルで返して、Domain層でインス
タンスを作っていた。
【ドメイン層】
domain/manga/Series.scala
【インフラ層】
infra/db/manga/SeriesTable.scala
…が面倒すぎた
DB → Entity解法(1)
• 解法① - DAO
– ドメイン層に
生データ ⇔Entity変換のメ
ソッドかクラスを設ける。
– 素直な解法
class SeriesRepository {
def convertToEntity(row:Row)={
Series(row[“id”], row[“title”]…)
}
}
【ドメイン層】
domain/manga/Series.scala
DB → Entity解法(2)
• 解法② - 依存性逆転の原則
– 実践DDD p118
– 定義(trait/interface)は
domain層で、実装はinfra層
を可とする
• DB->Entityは普通にinfra層で
インスタンスを作って返す
– なんとなく気持ちが悪いが、
最近はこれ系を愛用中。
• 実践DDDには他の方法も
載っています。
class SeriesTableResult
extend Series {
…
}
trait Series {
val id:SeriesId
val title:String
…
}
【ドメイン層】
domain/manga/Series.scala
【インフラ層】
infra/db/manga/SeriesTable.scala
APP層でやっていること例
• 閲覧系
– マガジン情報(と、それに関
するもの)の取得
• MagazineRepositoryを叩くだけ
– マガジンの一覧やランキング
• MagazineRepositoryのlistやら
Rankingを叩くだけ
• 管理業務
– 作家の管理
1. 必要入力を受け取る
2. Authorオブジェクトを作る
3. entryする。
4. あとはget/list
5. JSON出力
– ストーリーの管理
1. 必要入力を受け取り”下書
き”を作る
– Storyと似たフィールド構成
だが、NULLABLEが多い
2. 下書きをストーリーに変換
する
3. entryする
4. あとはget/list
5. JSON出力
– マガジンの編成
• 格納したいマガジンアイテム
の一覧を受け取る
• Magazineを作る
• entryする
実装上の反省
• RepositoryとTable操作系をobjectにしてし
まった
– テストが大変
– コンテキストが絡まざるを得ない事をすると
きにも大変
• パフォーマンスチューニングやトランザクション
– DI出来るように徐々に改修中
• Repository系でFutureを使わなかった
– チューニングを極められない
やってみてどうだったか
• DDD良い
– 実践しきれてるかどうかはわからないけど、それでも良い
感じ。
– 編集者も含めて、用語を合わせるのは混乱が少なくて良い。
• 説明が同じになると、動員できる知恵が多くなり、ドメインを
捕らえる精度が上がって、ますます良い。
• ドメイン層の実装が、口頭説明と同じになるのはわかりやすい。
– ドメイン層に徹底的に余計なことを書かないようにするの
は、とても見通しが良い。
• 大きめのリファクタを決断しやすい。口頭説明でおかしさを感
じたら、リファクタ決断の時
だいたい以上です。
ご静聴ありがとうございました!
【付録】
勉強会冒頭で利用した
会社紹介の資料
Septeni × Scala 勉強会 #3
〜 ドメイン駆動設計やってみた(2) 〜
2回目のテーマ:ドメイン駆動設計
チャットワーク株式会社
加藤潤一 さん
Play2 with DDD 〜何からはじ
めて何に気をつけるべきか〜
セプテーニオリジナル
原田侑亮
Scala : DDD × 弊社実践例
本勉強会について
本日のトピック
社内研修を兼ねつつ、セプテーニの知名度向上を目標としたもの
1回目のテーマ:Scala普及について
チャットワーク株式会社
加藤潤一 さん
Scala関数プログラミング初級
セプテーニオリジナル
杉谷保幸
Scalaに至るまでの物語
セプテーニオリジナル
寺坂郁也
新卒で初めて学ぶ言語が
Scalaで良かったこと/大変だったこと
※資料はconnpassにて共有されています
…
インターネット広告事業
自社サービスの企画・開発
海外の開発拠点
ソーシャルゲーム開発
広告プラットフォーム事業
マンガコンテンツ事業
セプテーニとは、ネット広告業を本業としつつ
1部署1会社としていろいろやっている会社です。
セプテーニグループ
セプテーニグループ
売上高 平均年齢
働きがいのある会社ランキング2015
書籍化・アニメ化も決定! オリジナルコミックアプリ
最強ネット広告運用支援システム
10代少女に人気・ファッションアプリ
・Playframework + Scala
・AngularJS + TypeScript
・DDD採用
・Unit Test 完備
・スクラム
・Android + Scala
・Swift
※検証中
・Playframework + Scala
・AngularJS + TypeScript
・DDD採用
・Unit Test 完備
・スクラム
・Sisioh(http://sisioh.org/)
…for
・弊社最後のLAMP構成
・レガシーコード改善ガイド活用
技術トピック
・スクラム
・Android + Kotolin
求人
セプテーニ・オリジナルでは
Scalaエンジニアを募集しています
社内カフェ(ランチ営業あり) スタンディング作業可の最新デスク
本日のテーマ:ドメイン駆動設計(2)
セプテーニオリジナル 杉谷保幸
GANMA!でDDDをやってみてから1年くらい経った
セプテーニオリジナル 原田侑亮
「DDD × 新人」が学んでみたレシピ共有
20:10 -
21:00 -

GANMA!でDDDをやってみてから1年くらい経った