エンジニア歴1年の僕がドメイン駆動設計(DDD)を参考にLaravelのプロジェクトをフルリニューアルした話

こんにちは!
はじめまして!
2020年7月からPIECE事業部でエンジニアをさせてもらっています。
野澤です。


今回、PIECEというサービスのリニューアルを担当させてもらったのでその時のことについて書きたいと思います!
まだ若輩者なので至らない点が多々あると思いますが


フルリニューアルってどんな事したんだろう〜?
Hajimariのエンジニアはどんな仕事をしてるんだろう〜?
って思った人はぜひ読んで見てください!

ドメイン駆動設計の説明も書いたのですがボリュームが多くなってしまいました…
ドメイン駆動設計について概要知りたいという方は是非読んでみてください。
クリーンアーキテクチャの説明やモデリングのやり方などは説明していません。
ご了承ください。

PIECEリファクタリングプロジェクトの概要

PIECEとはどのようなサービスなのか

PIECEとはスタートアップ向けマッチングサイト構築パッケージです。 特徴としては

  1. すでにマッチングサービスを運営するための機能が揃っているためスピード感があるビジネス展開が可能
  2. 弊社サービスのITプロパートナーズで培ったノウハウがあるためマッチングビジネスに寄り添った開発、保守が可能

です。 興味がある人はPIECEのホームページをぜひ見てみてください!


リニューアルの目的
  • 今後の保守、運用をしやすいようにソースコードリファクタリングする
  • テストについて考えられていないのでテストできるように整える
  • PIECEパッケージをもっと良くする
    の3点が主にありました。


リニューアル施策

ドメイン駆動設計適用

ドメイン駆動設計の考えを用いて層の責務をわけ、コントローラの肥大化を抑えるようにしました。
適切な責務わけをしたことにより、ビジネスロジックの再利用性も上がりました。
それとリポジトリという概念を用いてデータベース接続をORMに依存しないようにして、リポジトリのインタフェースを用意することでテストのしやすさも改善することができました。
今後はテストコードも書いていけるようにしていけたらいいなと思っています。

技術的負債を返す、既存のバグを改修

bladeを直接使わずに、フロント側のソースコードをサーバーサイドで作っていた部分があったのでそこの分離や既存で発覚していたバグの改修をしました。

DB制約の整備

本来NOT NULLのところがNULL許容されていたり、外部キー制約が設定されていなかったり整っていませんでした。
カラムに対して一つずつ精査をして正しい制約を設定しました。

型を明示的に指定する

PIECEではPHPのメソッドの引数、戻り値、プロパティの型を指定するようにしました。
型指定をするメリットは

  • 想定外の引数を受け取った瞬間にエラーになるため、不具合を検知するタイミングが早くなる
  • 関数の意図がわかりやすくなる
  • 型が保証されるので関数の引数チェックが簡単になる
    などがあります。
    安全なソースコードを書くためには方が必要だと思ったのと、僕自身HajimariでエンジニアになるまではJavaC#など型があったほうが安心感もあったので明示的に型を指定するようにしました。

コメントの強制

今まではメソッドコメントやクラスコメントが無かったのですがそれを書くようにしました。
それとコードの段落でのコメントを意識するようにしました。
理想を言うとソースコードを見ただけですぐコードを理解できればよいのですがそうはいかないときもあります。
そういうときにコメントがあれば早く理解でき、コードの改修がはかどります。

命名にこだわる

三者が見てすぐに分かるかどうかを重要視しました。
良い命名のメソッドはメソッドの中身を見なくてもどんな処理をしているかわかります。


実際にリニューアルをしてみて

今までいかにも自分がやってきた感を出しながら記事を書いていたのですが、実はそうではなく僕含め4人で対応していました。
見積もりやスケジューリング、タスクのチケットの割り振りなど経験がなく苦労しました。
ドメイン駆動設計も実務でやったことはほぼなく自分で本を読み、ネットで記事を読みPIECEに対してどのように適用したら良いのかを考え、フォルダ構成を考え色々試行錯誤しながら導入しました。
リニューアル中は上手くいかない部分がたくさんありましたがチームのみんなに協力してもらったり支えてもらったりで最終的にやり遂げることができました。
良い製品を作るためにはスケジューリング能力であったり、チームでのコミュニケーション能力だったり「技術」以外の要素も多分に必要になってくるということを実感させられました。


最後に

このPIECEプロジェクトのリニューアルに携わることになったのは入社して1週間後くらいのことでした。
まだエンジニア組織としては未熟でたくさんの課題があると思っていますが未熟な分、積極的に行動すれば改善できることがたくさんあり、挑戦できることもたくさんあります。
Hajimariの理念に共感でき、能動的に仕事をしていきたい!というエンジニアさん是非一緒に仕事をしましょう!
Hajimariではエンジニアの採用も行っているので興味がある人は見てみてください。
※下にドメイン駆動設計のちょっとした解説もあるので興味を持っていただいた方は見てみてください!

www.wantedly.com




ドメイン駆動編〜

ドメイン駆動設計を勉強するのに利用した本

www.shoeisha.co.jp ドメイン駆動設計について記事を調べたりすると抽象的な話が多い中、この本は具体的な実装例がたくさんありどのように実装していけばいいのかを知ることができる本です! 層の責務や概念はわかったけど具体的にどう書けばいいんだ!っていう人におすすめです。


gihyo.jp この本はプログラムを書くときに常に意識しておきたいことについてのことがたくさん書いてあり、かつドメインモデルの考え方や画面遷移やweb APIについてにも触れられており、
本当に現場で役に立つことが書いてありました。
ドメイン駆動設計に問わずおすすめの本です。


www.shoeisha.co.jp この本も具体的な実装例が書いてありとても参考にしました。


gihyo.jp 雑誌なのですがそこに書いてあるドメイン駆動設計がとても参考にしました。 「集約」という言葉について「そういうことだったのか!」となった本です。 ページ数は少ないですが体系的にまとめられておりその少ないページにドメイン駆動設計の情報がかなり凝縮されていて腑に落ちる部分がたくさんありとても良かったです。

ドメイン駆動設計とは

僕自身うまく言葉にできなかったのでWikipediaで調べてみたのですが
【ソフトウェアの設計手法であり、「複雑なドメインの設計は、モデルベースで行うべき」であり、また「大半のソフトウェアプロジェクトでは、システムを実装するための特定の技術ではなく、ドメインそのものとドメインのロジックに焦点を置くべき」であるとする】 と書いてありました…
抽象的すぎて分かりづらいですよね…
僕もまだ勉強の途中なので理解不足な面は多大にありますが僕自身の感覚だと「ビジネスロジックの開発の効率やビジネスロジックの再利用性を最大限上げることができる設計手法、業務(ビジネス)を中心にそえて開発を進めていくための設計手法」となります。
機能を開発するときも、改修するときもビジネスロジックに注力します。
そこでSQLAPIを意識させないようにします。
そのようにすることで業務の発展に合わせてソフトウェアを成長させることに集中することができます。


層の責務について

この記事を読んで概念がなんとなくわかるようになっていただけたら嬉しいです。 具体的な実装、コードについては別途また他の記事で書いたりしようと思います。

プレゼンテーション層

MVCのコントローラがこの層に属していると考えます。
この層は下記アプリケーション層に依存し、ユースケースを実現します。
その他http系の処理やCookie、セッション関連の処理が責務となります。
実装例.

class UserController
{
    /**
     * コンストラクタ
     *
     * @param \Application\User\UserApplicationService $service
     */
    public function __construct(UserApplicationService $service)
    {
        $this->service = $service;
    }

    /**
     * ユーザー情報参照
     *
     * @param string $id
     * @return \Illuminate\View\View
     */
    public function show(string $id): View
    {
        $user = $this->service->show($id);

        return view('user.show')->with([
            'user' => $user
        ]);
    }

}



アプリケーション層

アプリケーション層はトランザクション管理やドメイン層、リポジトリを使ったユースケースの実現が責務です。
ここにビジネスロジックは書きません。
ビジネスロジックはmodelかDomainServiceにまとめたあと、そのメソッドを呼ぶようにします。
ビジネスロジックを組み立ててユースケースを実現します。
それとドメイン駆動特有のルールではないのですがPIECEではこの層で直接ORMを利用してDB接続に関する処理を書いてはいけないという決まりにしました。
DB接続に関してはただ保存するだけ、ただ更新するだけであればリポジトリのupdateOrInsertメソッドを直接呼び出して良い、そこにビジネスロジックが入ってきた場合(データの加工をしたあとにデータ保存など)はDomainServiceを通じてDB接続をするようにします。
これらはApplicationServiceクラスに記述します。
実装例.

class UserApplicationService
{
    /**
     * ユーザー情報参照
     *
     * @param string $id
     * @param integer $companyId
     * @return \Domain\Models\User\User
     */
    public function show(string $id): User
    {
        $userRepository = new UserRepository();
        $user = $userRepository->get($id);

        return $user;
    }
}
ドメイン
ドメイン(model)

基本ビジネスロジックはここに書くことになります。
本来ドメイン駆動設計においてドメインのモデルとMVCのモデルは意味合いが違うのですが、今回PIECEでは同一のものととして考えることにしました。
ですのでドメイン=モデル=Entityといった感じで捉えるようにしています。
実装例.

class User extends Model
{
    /**
     * フルネーム取得
     *
     * @return void
     */
    public function getFullNameAttribute(): string
    {
        return $this->last_name. " " .$this->first_name;
    }
    
    // Userクラスが持つデータに対する業務ロジックを実装
}



ドメインサービス

ドメイン(model)で書くべきではないビジネスロジックを書くときにここに書きます。
modelにビジネスロジックを書くのに違和感があるとき(クラスのプロパティに対するロジックではない)や複数のドメインが関わってくるロジックのときなどに記述します。
例えば「ログイン可能かどうか」というメソッドはドメインに置くべきではないと考えます。
1ユーザーが自分がログイン可能かどうか自分自身で確認していくのは違和感を感じます。。
ですので「ログイン可能かどうか」というメソッドはドメインサービスに置くのが良いと思います。
実装例.

class UserDomainService
{
    /**
     * ユーザー重複チェック
     *
     * @param User $user
     * @return void
     */
    public function exists(User $user) : User
    {
        $userRepository = new UserRepository();
        $user = userRepository->get($user->id);

        return empty($user->id) === false;

    }
}



リポジトリ

データのやり取りの際は必ずこのリポジトリが呼ばれます。 ここのメソッドはDBの接続に関するものだったら内部でORMを利用します。
DBへの接続など意識することなく、データの入出力を実現します。
ここはDBへの接続だけではなく、例えばNoSQLを利用するようにしたりすることもできます。
ドメイン駆動設計においてリポジトリのインタフェースをドメイン層、実装クラスをインフラストラクチャ層と分類したりするのですが、フォルダの構成上の事を考えてリポジトリドメイン層の要素とみなしています。
実装例.

class UserRepository
{
    /**
     * データ取得
     *
     * @param string $primaryKey
     * @return void
     */
    public function get(string $id): User
    {
        $user = User::find($id);
        return $user ?? new User();
    }

    /**
     * データ更新or追加
     *
     * @param User $user
     * @return void
     */
    public function updateOrInsert(User $user): void
    {
        $user->save();
    }
}




依存関係について

PIECEではプレゼンテーション層がアプリケーション層に依存し、アプリケーション層がドメイン層に依存する形にしました。
もっとわかりやすく言い換えるとControlllerがApplicationServiceのメソッドを呼びます。


フォルダ構成について

ドメイン

package
│   ├── Domain
│   │   └── Models
│   │       ├── Company
│   │       ├── User

ドメイン層に関しては上記のようにModelsの下にmodelの名前のフォルダを作り、その中にドメイン(model)、ドメインサービス、リポジトリインタフェース、リポジトリを格納しています。
フォルダ構成についてはいろいろな考え方があると思いますがPIECEでは各要素を一つのフォルダに加えることで、ソースコードを見つけやすくなると考えてそのような構成にしました。
例えばUserに関する機能を作るときはUserの下のドメインドメインサービスを見れば再開発しなくても済むかもしれないと予想することが用意になります。

アプリケーション層

packages
│   ├── Application
│   │       ├── Company
│   │       ├── User

アプリケーション層に関しては上記のように構成しています。
CompanyやUserというフォルダにはApplicationServiceとDTOのファイルが格納されています。
DTOとは【Data Transfer Object】のことで、関連するデータを一つにまとめた、データを運ぶだけのオブジェクトです。
PIECEではApplicationServiceのメソッドではドメイン(model)かDTOインスタンスをControllerに返すようにしました。

ドメイン駆動設計の考え方を用いてリファクタリングした感想

ドメイン駆動設計は短期間でのリファクタリングや新規開発などスピード感を求められる場面ではあまり向いていないと思いました。
実際にドメイン駆動設計の考え方を取り入れたときにネックだったなと思ったのは

  • 責務わけをしたことによりファイルの数が多くなり対応に時間がかかった。
  • ドメイン駆動設計にどこまで乗っ取るのか決めるのに時間がかかった。(「entitiy」、「valueobject」、「仕様」、「集約」、「ファクトリー」などドメイン駆動設計で重要な概念があるがスケジュールのことや今後のことを考えて使用しなかった)
  • チームでドメイン駆動の概念の理解やドメイン駆動設計に基づくルールに則ってちゃんとコードに起こせるようになるまでのコストが高いと感じた。
  • 集約の概念が難しい。「リポジトリでのやり取りの基本単位が集約」、「不変条件を維持する単位」など頭でわかっていてもモデリングを時間をかけてやらないと上手くいかない部分が多々出てきた。

という4点です。
プロジェクトの規模にもよりますが、リファクタリングをするために時間をたくさんかけることができる状態であり、
入念にドメインエキスパートの人に話を聞いてモデリングして実装に起こしていくというふうにすすめていかないとレベルの高いドメイン駆動設計は難しいと思いました。
ただし軽量ドメイン駆動設計でも多分にコードのみやすさであったりビジネスロジックの再利用性は上がったので良かったと思っています。