はじめに

本書の内容

本書は@pospomeがサーバサイドのアプリケーションアーキテクチャに関してまとめたものです。

著者

pospome。サーバサイドエンジニア。DDD、実装パターンのようなアプリケーションアーキテクチャが専門。最近は認証認可に興味がある。Twitterは@pospome。ブログは「pospomeのプログラミング日記」。共にアプリケーションアーキテクチャ、Go言語、GCPを中心にアウトプットしている。キャラクターは羊ではなくポメラニアン。

誤字脱字報告フォーム

誤字脱字、不自然な日本語、図やサンプルコードの参照ミスを見つけた場合は以下のフォームから報告していただけると嬉しいです。

誤字脱字報告フォームはこちら

免責事項

本書に記載された内容は、情報の提供のみを目的としています。したがって、本書を用いた開発、製作、運用は、必ずご自身の責任と判断によって行ってください。これらの情報による開発、製作、運用の結果について、著者はいかなる責任も負いません。

第1章 Webアプリケーションにおけるレイヤ構造パターン集

1.1 はじめに

最近のWebアプリケーション開発ではMVCのような一般的なレイヤ構造だけではなく、レイヤードアーキテクチャ、クリーンアーキテクチャのような複雑なレイヤ構造を採用する機会が増えてきました。レイヤ構造はコードの品質に大きな影響を与えます。そのためエンジニアはそれぞれのレイヤがどのようなもので、どのようなメリット、デメリットを持っているのかを把握し、使い分ける必要があります。本章ではレイヤ構造の中でも有名なものをパターン別に紹介します。

1.2 レイヤとは?

そもそもレイヤとは何でしょうか?レイヤ構造について説明する前にレイヤの定義について説明します。

筆者は"レイヤ=パッケージ(モジュール)"という認識を持っています。どのようなパッケージかというと、Webアプリケーションにおいて普遍的な責務を持つパッケージです。例えば、MVCパターンであれば、モデル、ビュー、コントローラーという3つのパッケージがレイヤとして存在します。ECサイトであれ、オークションサイトであれ、ドメインロジックを持つというモデルの責務が変わることはありません。そのため、常に使う側と使われる側の関係性が決まっている(レイヤ間は単方向依存である)のが特徴です。

一方普遍的ではないパッケージも存在します。それがWebアプリケーションの仕様に対するパッケージです。例えばECサイトであれば、買い物かごパッケージが存在するかもしれませんが、オークションサイトには買い物かごパッケージが存在するとは限りません。Webアプリケーションの仕様によってどのようなパッケージが存在し、どのような依存関係になるのかが異なります。

1.3 レイヤ構造のメリットとデメリット

レイヤ構造を利用することで次のメリットを得ることができます。

修正による影響範囲を予測することができる

レイヤ構造はレイヤ間の依存方向が単方向依存です。例えば、MVCではコントローラーはモデルに依存しますが、モデルはコントローラーに依存しません。そのためコントローラーを修正した場合はモデルに影響はありませんが、モデルを修正した場合はコントローラーに影響があります。このように修正箇所の影響範囲を予測することができます。単方向依存が保証されていない場合、パッケージ同士が修正の影響を受けあってしまい、影響範囲を予測することができません。

どこにどのようなコードがあるのかが分かりやすい

レイヤはWebアプリケーションにおいて普遍的な責務を持つパッケージです。そのためシステムを修正する際にどのレイヤのコードを修正すればいいのかが明確になりますし、レイヤ間の依存方向に沿って自然とコードを置く場所が決まります。実装者のスキルや好みによってコードを置く場所がブレることが少なくなります。

コードのキャッチアップのイニシャルコストが下がる

レイヤは普遍的な責務を持つパッケージなので、自分が担当するサービスや部署が変わったとしてもレイヤ構造が同じであれば、コードをキャッチアップするためのイニシャルコストを削減することができます。

パッケージ同士の依存を適切に保つことができる

レイヤは普遍的な責務を持つパッケージなので、それらの依存関係は常に適切です。レイヤを利用せず自由にパッケージを定義すると、それらの依存関係は実装者のスキルに依存したものとなります。誰もが適切にパッケージ同士の依存関係を定義できるわけではありません。レイヤ構造では最低限の秩序を守ることができます。

一方で当然ながら次のデメリットも存在します。

学習コストが高い

レイヤ構造に対する知識がない場合、レイヤそれぞれの責務を覚える必要があります。レイヤも1種類ではありません。プロジェクトごとに異なるレイヤ構造を採用している場合、それなりにキャッチアップコストがかかってしまいます。それらを覚えるまでは作業スピードも落ちてしまうでしょう。

コード量が多くなる傾向にある

レイヤ構造の階層が多ければ多いほど、全体のコード量は増えてしまう傾向にあります。実装対象のWebアプリケーションに対して不適切なレイヤ構造を導入してしまうと、レイヤ構造のメリットを享受できず、単にコード量を増やしてしまうことになります。

今回紹介したレイヤ構造のメリット、デメリットは一般的なものです。実際はレイヤ構造によってメリット、デメリットが異なります。

1.4 コントローラーパターン

最初に紹介するのはコントローラーパターンです。コントローラーパターンはMVCパターンのコントローラーに全てのロジックを実装するシンプルなパターンです。レイヤがコントローラーのみなので、一般的にはレイヤ構造とは認識されないと思います。

1.4.1 パッケージ構成

パッケージ構成例はリスト1.1です。

リスト1.1:

./
├── controller
│   └── user.go
└── main.go
└── template
    └── user.html

コントローラーパターンはコントローラーレイヤしか存在しないので、パッケージもcontrollerのみです。基本的にcontrollerパッケージ直下にファイルやパッケージを置きます。サーバの出力がHTMLの場合は、templateパッケージのようなHTMLのテンプレートファイルを置くパッケージが存在します。

1.4.2 レイヤの責務

コントローラーレイヤ

コントローラーパターンのレイヤはコントローラーレイヤのみです。そのためHTTPリクエストのバリデーション、DBトランザクション、ドメインロジックの実装など、コントローラーレイヤに全てのロジックを実装することになります。リスト1.2がコントローラーレイヤの実装例です。

リスト1.2:

package controller

import (
  "fmt"
  "net/http"
)

func Handler(w http.ResponseWriter, r *http.Request) {
  //バリデーション
  n := r.FormValue("name")
  if n == "" {
    fmt.Fprint(w, "name error")
    return
  }

  //DBアクセス
  conn := db.NewConnection()
  conn.BeginTransaction()

  u := &User{
    Name: n,
  }

  if err := db.Insert(conn, u); err != nil {
    db.Rollback()
    fmt.Fprint(w, "db error")
    return
  }
  conn.Commit()

  fmt.Fprint(w, "success")
}

type User struct {
  Name string
}

1.4.3 メリット

コントローラーパターンはコントローラーレイヤに全てのロジックを実装するので、レイヤ構造としてはシンプルになります。学習コストはかからないでしょう。レイヤ同士の依存関係に悩むこともなく、すぐにコードを書くことができます。

1.4.4 デメリット

全てのロジックをコントローラーレイヤに実装するという性質上、実装対象の仕様が複雑でコード量が多くなるとコードの見通しが悪くなってしまいます。リスト1.3のように関数ベースで処理を切り出すことで、ある程度見通しの良いコードにすることが可能ですが限界があります。

リスト1.3:

package controller

import (
  "fmt"
  "net/http"
)

func Handler(w http.ResponseWriter, r *http.Request) {
  //入力値チェック
  n := r.FormValue("name")
  if n == "" {
    fmt.Fprint(w, "name error")
    return
  }

  //DBアクセスを関数に切り出す
  if err := InsertUser(n); err != nil {
    fmt.Fprint(w, "name error")
  }


  fmt.Fprint(w, "success")
}

//DBアクセス部分のみ関数に切り出す。
func InsertUser(name string) error {
  conn := db.NewConnection()
  conn.BeginTransaction()

  u := &User{
    Name: n,
  }

  if err := db.Insert(conn, u) {
    return err
  }

  conn.Commit()

  return nil
}

type User struct {
  Name string
}

Go言語の場合はパッケージ単位で変数、関数へのアクセスを制御することになります。そのため、1つのレイヤに全てのロジックを実装してしまうと、実質全ての変数、関数へのアクセスが可能になり、特定のコードを修正することに対する他のコードへの影響が大きくなる可能性が高くなります。コード量が多くなる場合はコントローラーパターンは避けた方がよいでしょう。

1.4.5 使い所

レイヤ構造はシンプルですが、実装するコード量が多くなるとコードの見通しが悪くなるので、小規模な実装に向いています。筆者は次のユースケースでこのパターンを採用することが多いです。

  • 開発作業に必要なデータを開発用DBに登録する。
  • 本番環境のサービス運用で必要なデータを本番DBに登録する。

これらはいずれもサービスを利用するエンドユーザー向けの実装ではなく、社内でのサービス運用に利用する実装です。社内でのサービス運用では信頼性のあるデータがすでに存在していて、それをWebUIからDBに登録するだけという要件が多いので、コントローラーパターンで十分実装可能なことが多いでしょう。おそらく読者の方もこのようなユースケースでコントローラーパターンを採用した経験があるでしょう。実装するコード量が多くなるとコードの見通しが悪くなるというはっきりとしたデメリットがあるので、他のレイヤ構造よりも採用すべきユースケースが限定されるレイヤ構造だと思います。

試し読みはここまでです。
この続きは、製品版でお楽しみください。