Drupal のモジュール構成パターンあれこれ
今回は CMS の Drupal を使った開発に関する少しマニアックなお話です。
Drupal を使ったサイト制作において「カスタムモジュールをどのように分割するか」というのはそのサイトの中長期のメンテナンス性に影響する重要なポイントです。
一般に、 Drupal 上で A というひとつの機能を実現する方法はたくさんあり、そのモジュール構成の形もいくつもあります。 例えば、すべてを内包した大きなひとつのモジュールで実現する形もあれば、小さく小分けにした多くのモジュールで実現するやり方もあります。 極端なまでに粒度を細かくしていくと「 1 プラグイン = 1 モジュール」「 1 フック = 1 モジュール」というのも可能は可能ですが、単純に粒度を細かくすればメンテナンス性が上がるかというとそういうわけでもありません。
そんな Drupal のモジュール構成について、私がこれまでに見たり自分自身で使ったりしたことのあるパターン(のうち個人的によいと思うもの)を今回まとめてみました。 興味のある方はご参考にしてみてください。
a) 「汎用 - 固有」パターン
ひとつめは 「汎用 - 固有」パターン です。
これは、対象の機能を、複数のプロジェクトで使える「汎用的な部分を担うモジュール」と、「プロジェクト固有の部分を担うモジュール」に分けて実装するパターンです。 多くの場合、後者の「固有モジュール」が前者の「汎用モジュール」に依存する形になります。
このパターンを使うメリットは、「汎用的な部分を担うモジュール」を複数のプロジェクトで再利用できる点です。
一方のデメリットは、汎用性を持たせるために複雑性が増したり追加の実装が必要になったりすることです。
このパターンを採用する上での課題は、「汎用的なパターンの見極めと抽出」が難しいことです。 どこからどこまでが汎用的で、どこからどこまでが固有のものなのかというのは、 Drupal コアの構成の知識とサイトの構築・運用の経験をある程度持っていないことにはなかなか見極めが難しいと思います。
b) 「内部処理 - プレゼンテーション」パターン
ふたつめは 「内部処理 - プレゼンテーション」パターン です。
これは、ドメインロジックや集計といった「内部処理を担うモジュール」と、ユーザーにコンテンツを表示する「プレゼンテーションを担うモジュール」に分けるパターンです。
レイヤーアーキテクチャの原則に則って「プレゼンテーションは内部処理に依存するが、内部処理はプレゼンテーションに依存しない」形に切り分けるとスッキリします。 プレゼンテーションのパターン数(≒画面数)が多い場合は、ひとつの「内部処理」モジュールに対して複数の「プレゼンテーション」モジュールを作る形にするとよいでしょう。
Views や Rules といった規模の大きなプロジェクトでは、これに似たパターンでモジュールが分割されています。
このパターンを使うメリットは、内部処理とプレゼンテーションがモジュールとして分かれるので、プレゼンテーションの部分だけの追加・変更が比較的安全に(安心して)できることです。 また、内部処理部分は継続的に使用するがプレゼンテーション部分は開発やメンテナンスといったかぎられたフェーズでのみ利用するといった場合には、プレゼンテーションモジュールはふだん無効化しておくことで省メモリ化とパフォーマンス向上が図れます。
デメリットは特に無いように思いますが、内部処理とプレゼンテーションの間の境界があいまいな場合には、リファクタリングをするときにかえって手間が増えてしまったりするので注意が必要です。
また、プレゼンテーションの部分をあまりに小さな粒度で分割していると、「プレゼンテーション層の共通処理」の置き場所に困ったりすることもあります。
c) 「ドメインロジック - ユーティリティ」パターン
続いては 「ドメインロジック - ユーティリティ」パターン です。
これは、サイトの要件を直接満たすための「高レベルの処理を担うモジュール」と、部品として使用される「比較的低レベルの処理を担うモジュール」とを分割するパターンです。
ドメインロジックは、例えば、単一のユースケースや見積もりの一項目として挙げられるような粒度でまとめて作り、サイト内で広く共通で利用される部品はユーティリティモジュール(あるいはヘルパーモジュール)として切り出す、という考え方です。 ユーテリティモジュールの具体例としては、例えば、 JSON シリアライズ / デシリアライズ処理を行うモジュール、ファイルシステムを取り扱うモジュール、エンティティのフィールドの処理を行うモジュール、フォームフィールドを生成するモジュール、などが考えられます。
このパターンを使うメリットは、ユーティリティ部分を一元化することでモジュールの複雑な依存関係やコードの重複を防げることです。 ユーティリティの部分を汎用的な作りにするように心がければ、パターン a のようにプロジェクトをまたいで再利用できるパーツにすることもできます。
デメリットは特にありませんが、どの部分をユーティリティとして切り出すのか、切り出したユーティリティをどの粒度で分けるのか、といったところにはある程度の経験とセンスが求められる気がします。
d) 「エンティティ定義 - エンティティ利用」パターン
次のパターンは 「エンティティ定義 - エンティティ利用」パターン です。
これは、 Drupal の「エンティティ」を「定義するモジュール」と「利用するモジュール」とに分けて実装するパターンです。
カスタムエンティティを定義して利用する場合は、定義と利用のロジックの両方をひとつのモジュールに納めてしまう形がパッと思い付く構成ではありますが、別のモジュールに切り分ける形も検討するとよいでしょう。 特に、カスタムエンティティに固有のフィールドが付いていたり必要なバリデーション処理がたくさんあったりするような場合には、モジュールのサイズがすぐに大きくなってしまうので、早いうちから分割するオプションを考慮しておくのがよいかと思います。
このパターンを使うメリットは、もちろん、カスタムエンティティのモジュールの肥大化をある程度抑えられることです。 エンティティを定義するモジュールになんでもかんでも詰め込んでしまうと、どんなにきれいに設計したつもりでも複雑でわかりづらいモジュールになってしまうので、肥大化が予想される場合には早期に分割を検討するとよいでしょう。
特に、サイトが進化していったときに、エンティティの定義はあまり変わらなくて利用方法だけがコロコロ変わっていくような場合には、「定義」側は触らずに「利用」側だけを変更していけばよく、変更の範囲が明確に限定できるため安心して追加開発を行うことができます。
e) 「プラグインタイプ定義 - プラグイン実装」パターン
「プラグインタイプ定義 - プラグイン実装」パターン です。
これは、プラグインタイプを「提供する側」と、そのプラグインタイプのプラグインを「実装する側」とを分けるパターンです。
d のエンティティのパターンと似ていますが、変更の少ない「プラグインタイプの定義」と追加・変更が比較的多い「プラグインの実装」とを分ける形になります。
このパターンを使うメリットは、そのままですが、変更の少ない部分と多い部分とをきれいに分けられることです。 そして、モジュールの依存関係ツリーがそのままプラグインの定義と実装の関係(クラスとインスタンスのような関係)を反映する形になるので、実装した人以外の人が見てわかりやすい作りになります。 プラグインを増やしていくときにも、シンプルでわかりやすい形を維持したままきれいに横展開することができます。
特定のプロジェクトでプラグインタイプを定義する機会は一般にあまり多くないので、実際にこのパターンの利用すべき場面に遭遇することはあまりありませんが、こういう形があるということは押さえておくとよいかと思います。
f) 「フック定義 - フック実装」パターン
「フック定義 - フック実装」パターン です。
これは、フックを「定義するモジュール」と「フック実装を提供するモジュール」とを分割して実装するパターンです。
e のプラグインがフックに変わったパターンです。
コントリビュートモジュールなどであれば、フック定義と実装が分かれている形は一般的ですが、カスタムモジュールを書くときにこのあたりのオプションをあまり意識せずにコードを書いている Drupal 開発者の方も多いのではないかと思います。
私が個人的に使っていて気持ちがいいと感じるのは、 Rules モジュールのルールイベントだけをカスタムモジュールで作って、ルールコンフィグの実装は Rules その他のモジュールが提供するコンディションとアクションを使って組み立てる形です。 この場合だと、 Rules イベントを定義するモジュールが「フックを定義するモジュール」にあたり、ルールコンフィグを収めた Features モジュールなどが「フック実装を提供するモジュール」になります。
g) 「パーツ - オーケストレータ」パターン
「パーツ - オーケストレータ」パターン です。
これは、特定のユースケースに必要な諸機能を小さなパーツに分解しておいて、統合用のメインモジュールでそれらをひとつに組み合わせて実装するパターンです。
依存関係としては、メインのモジュール(オーケストレータモジュール)が多数のサブモジュールに依存する構造となります。
a や c のパターンと似ていますが、切り出し単位を必ずしも「汎用的な部分」「ユーティリティとして使い回せる部分」に限定する必要がない点がこのパターンの特徴です。
メリットは、この構成を意識して使うことで、「依存の少ない安定したパーツ」と「他に多く依存する不安定なパーツ」との間に明確な線引きができることです。
デメリットは、説得力の低い独りよがりな切り分けをしてしまうと、可読性やメンテナンス性が下がってしまうことです。
また、サイトの進化にあわせて変更を加えていくときには、意識して小さな再構成を繰り返さないとオーケストレータ部分が肥大化して複雑怪奇になりがちなので注意が必要です。
h) 「外部ライブラリ管理」パターン
「外部ライブラリ管理」パターン です。
これは、 Drupal のモジュール以外の外部のライブラリ、例えば、 Composer や npm のパッケージ、フロントエンドのライブラリを管理するためのモジュールを切り分けるパターンです。
例えば、グラフ生成用のフロントエンドライブラリや、特定のファイル形式を取り扱うためのライブラリ、ストリームフィルタを定義するライブラリを Drupal からかんたんに扱えるようにするラッパー的、アダプタ的モジュールを提供するイメージです。
私は個人的には、テーマレイヤーは狭義の「見た目」だけを扱う形で薄く保ちたいので、 UI/UX に影響するフロントエンドの諸ライブラリは「 1 ライブラリ = 1 モジュール」の形で切り出す形をよく使います。
i) 「対象のカスタムモジュール」パターン
「対象のカスタムモジュール」パターン です。
これは、 alter フックや theme 上書きなどで対象とするモジュールの単位でカスタムモジュールを切り分けるパターンです。
このパターンを使うときには命名規則も重要で、例えば、対象のモジュールの名前をカスタムモジュールのマシン名に含めるようにしておく形にするのがシンプルでわかりやすいです。
例えば、プロジェクトで使うカスタムモジュールのマシンメインの共通プレフィックスが hayato
とすると、ログインフォームなどユーザアカウント周りに対するカスタマイズは hayato_user
モジュールに、タームの挙動を変更するモジュールは hayato_taxonomy
モジュールに、 Views のカスタムハンドラを定義するモジュールは hayato_views
モジュールに格納するようなイメージです。
対象のモジュールの名前が長い場合、 Drupal エンジニアの 9 割以上が知っているような定番モジュールなら、省略形を使ってもよいでしょう。
例えば、私は views_bulk_operations
といった名前は高確率で打ち間違う自信があるので、状況が許せば vbo
などの省略形を使いたい派です。
このパターンのメリットは、コアやカスタムモジュールの挙動に対するカスタマイズがたくさん発生したときに、どのモジュールが何をやっているのかがわかりやすいところです。 「コア / コントリビュートモジュールの挙動がどこかで変更されているみたいだけど、実際にどこで変更されているのかがわからない」というのは、他の人が作ったドキュメントの少ないサイトを取り扱うときに Drupal エンジニアが最も苦しむシチュエーションのひとつですが、このパターンの分割ルールと命名ルールを採用しておくと、他のエンジニアが後で触ったときのストレスをある程度抑えることができます。
ただし、変更対象のモジュールの粒度が要件やドメインロジックの粒度と一致することは稀なので、このパターンを使用する場合はドメインロジック単位での切り分けについては諦める必要があります。 これがこのパターンを選ぶ際の大きなデメリットであり課題です。
j) 「ページ別」パターン
「ページ別」パターン です。
これは他のパターンに比べるとシンプルで、単純にページ単位でカスタムモジュールを切り分けるパターンです。
ひとまとまりのページがひとつのモジュールで実装される形になるので、利用者側視点で見たときにわかりやすいのが特徴です。
わかりやすさを重視してここでは「ページ」パターンと呼んでいますが、昨今のヘッドレス需要・トレンドを考えると、「エンドポイント」パターンと呼んだ方がより適切かもしれません。
メリットは、もちろん、ページとモジュールの対応関係がシンプルでわかりやすいことです。 特定のページに対するパーミッションとコントローラ(ページコールバック)を小さな粒度でまとめておくことで、利用者視点でのコードの見通しをよくすることができます。
デメリットは、内部処理やドメインロジックが後から複雑になってきたときに、共通部分を切り出すコストがかかることです。 あまり細かい粒度で分割してしまうと、後からサイトマップ・ページ構成の変更が発生したときに苦しむ可能性が高くなります。
個人的には、このページ別パターンはどらかというと「べからずパターン」「アンチパターン」という印象です。
・・・以上です。
最後に、今回取り上げたパターンを一覧にまとめておきます。
- a) 「汎用 - 固有」パターン
- b) 「内部処理 - プレゼンテーション」パターン
- c) 「ドメインロジック - ユーティリティ」パターン
- d) 「エンティティ定義 - エンティティ利用」パターン
- e) 「プラグインタイプ定義 - プラグイン実装」パターン
- f) 「フック定義 - フック実装」パターン
- g) 「パーツ - オーケストレータ」パターン
- h) 「外部ライブラリ管理」パターン
- i) 「対象のカスタムモジュール」パターン
- j) 「ページ別」パターン
このあたりのモジュールの構成パターンのノウハウについては英語圏も含めあまり公に語られているのを見かけない気がするので、よい情報源(書籍・記事)を知っている方はぜひ教えてください。