Patterns of Enterprise Application Architecture から学ぶ - Chapter 4
はじめに
こんにちは。ソフトウェアエンジニアのttokutakeと申します。
これはPatterns of Enterprise Application Architecureという本を読んでみて、自分が理解した内容を要約して書き起こしていくシリーズの4回目の記事です。
全8回のシリーズとなる予定です。
間違っている部分などがありましたら、ご指摘いただけますと幸いです。
注意事項
対象読者は主にWebアプリケーションのエンジニアです。
本の内容をそのまま記事に記載しているわけではありません。
内容のすべてを記載すると情報量が多いように感じたので、省略している部分がそれなりにあります。
自分自身の見解を述べている箇所もあります。
Chapter 4. Web Presentation
今更な話をすると、この本の発売は2002年です。Webフレームワークが成熟していたり、Webフロントエンドの進化が激しい近年においては、この章の内容は今まで以上に古く感じられる部分が多いと思います。
2002年当時、Webアプリケーションが徐々に台頭してきていて、その構成は大きく分けて2つのスタイルが主流でした。スクリプトスタイルとサーバーページスタイルです。
スクリプトスタイルの代表格が CGI(Common Gateway Interface) スクリプトです。CGIとは、動的にWebページを生成するためにWebサーバー( Apache など)と外部プログラム( Perl など)が連携するための仕組みです。外部プログラムはHTTPレスポンスを標準出力に出力します。
外部プログラムとして Node.js を使ったCGIスクリプトのコードを紹介します。Apacheなどを含むすべての設定を確認したい場合は こちら をご参照ください。
#!/usr/bin/node
import process from "node:process";
const listItems = Object.keys(process.env).map((key) => {
const value = process.env[key];
return `<li>${key}: ${value}</li>`;
});
const response = `
Content-type: text/html
<html>
<head>
<title>CGI Example</title>
</head>
<body>
<div>Hello, CGI!</div>
<ul>
${listItems.join('')}
</ul>
</body>
</html>
`;
process.stdout.write(response.trim());
このファイルをApacheの設定に合わせて配置してあげて、Webブラウザでアクセスすると以下のように表示されます。
もう一つのスタイルであるサーバーページスタイルの代表格は PHP です。PHPは、HTMLをほとんどそのまま記述しつつ、動的に内容を変更する箇所に直接スクリプトを書き、実際にリクエストが来たタイミングでスクリプトを実行して動的にWebページを生成します。PHPではありませんが、テンプレートエンジンを用いたコード例をこのあと紹介します。
スクリプトスタイルは柔軟にリクエストを処理できて、サーバーページスタイルはレスポンスをわかりやすく整形できます。それらの良いところをどちらも享受できるのがModel View Controllerパターンです。
簡単なコード例を紹介していきます。すべてのコードを参照したい場合は こちら をご確認ください。これ以降のコード例はすべて TypeScript と Deno を用いています。
まずはModelです。簡潔にするために、DBは使わず固定値を返すようにしています。
// Model
export class Crew {
constructor(
public name: string,
public bounty: bigint,
) {}
static findAll(): Crew[] {
const crews = [
new Crew("Luffy", BigInt(1_500_000_000)),
new Crew("Zoro", BigInt(320_000_000)),
];
return crews;
}
}
続いてViewです。 Mustache というテンプレート形式で書かれています。
// View
<html>
<head>
<title>Model View Controller</title>
</head>
<body>
<ul>
{{#each crews}}
<li>{{this.name}} {{this.bounty}}</li>
{{/each}}
</ul>
</body>
</html>
ControllerではModelとViewを用いて、クライアントにHTMLを返しています。Mustache形式で書かれたViewを Handlebars というライブラリーで読み込んで利用しています。
// Controller
import { dirname, fromFileUrl, HandlebarsJS } from "../deps.ts";
import { Crew } from "./model.ts";
const __dirname = dirname(fromFileUrl(import.meta.url));
const indexTemplate = await Deno.readTextFile(
`${__dirname}/index.html.mustache`,
);
const indexView = HandlebarsJS.compile(indexTemplate);
export class Controller {
static index(_request: Request): Response {
const crews = Crew.findAll();
const html = indexView({ crews });
return new Response(html, {
status: 200,
headers: { "content-type": "text/html; charset=utf-8" },
});
}
}
最後にWebサーバーを起動するコードです。単純にControllerを利用してHTTPリクエストのハンドリングをするように設定しています。
import { serve } from "./deps.ts";
import { Controller } from "./src/controller.ts";
const port = 8080;
const handler = (request: Request) => {
return Controller.index(request);
};
console.log(`HTTP webserver running. Access it at: http://localhost:8080/`);
await serve(handler, { port });
WebブラウザでWebサーバーにアクセスすると以下のように表示されます。
現在のWebアプリケーション開発においては、Webフレームワークの利用が当たり前になっていたり、テンプレートエンジンが充実しているので、この辺はもはや普通なことと思えるかもしれません。
Model View Controllerを使う最も重要な理由は、モデルをWebのプレゼンテーションから分離できることです。この分離のおかげで、プレゼンテーションの追加や修正、そしてモデルのテストが楽になります。
Viewパターン
Viewにおいては3つのパターンが存在します。
Transform View
Template View
Two Step View
大まかな選択肢として、まずはTransform ViewかTemplate Viewがあります。それぞれどちらを選択したとしても、さらにその中でTwo Step Viewを利用するかしないかの選択肢があります。
Template Viewは、HTMLのような成果物と同様のデータ形式で記述しつつも、どこが動的に変更されるかを示すマーカーを埋め込む方法です。すでにコード例でも登場している Mustache や ERB のようなテンプレートエンジンは、Webに限らずいろいろな場面で利用できます。
Transform Viewは、HTMLのような成果物とは別のデータ形式で記述して、それを変換して成果物を生成するスタイルです。この本では XSLT がよく使われると紹介されていました。自分はこの本を読むまで存在を知らないくらいにはなじみがなく、調べてみてもあまり自分が利用する場面を想像できませんでしたので、別の例を紹介したいと思います。
例えば Haml や Jbuilder などがTransform Viewに該当するようなものと思われます。 React などもTransform Viewのようなものと考えられなくはなさそうですが、より高度なことをしているのでTransform Viewという枠には収まらないかもしれません。
Reactを使った簡単な例を紹介します。すべてのコードを参照したい場合は こちら をご確認ください。
まずはReactを使ってメインのコンポーネントを記述し、それをDOMにマウントしているコードです。(設定を極力簡単にするために) JSX で記述していないため、ちょっとわかりづらいことになっていますがご容赦ください。
const List = () => {
const crews = [
{ name: "Luffy", bounty: 1_500_000_000 },
{ name: "Zoro", bounty: 320_000_000 },
];
return React.createElement(
"ul",
null,
crews.map(({ name, bounty }, index) =>
React.createElement("li", { key: index }, `${name} ${bounty}`)
),
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(React.createElement(List));
Controllerでは上記のコードを `script` タグにごそっと埋め込んで、それをクライアントに返しています。
import { dirname, fromFileUrl } from "../deps.ts";
const __dirname = dirname(fromFileUrl(import.meta.url));
const script = await Deno.readTextFile(
`${__dirname}/index.js`,
);
export class Controller {
static index(_request: Request): Response {
const html = `
<html>
<head>
<title>Transform View</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
</head>
<body>
<div id="root"></div>
<script>${script}</script>
</body>
</html>
`;
return new Response(html, {
status: 200,
headers: { "content-type": "text/html; charset=utf-8" },
});
}
}
WebブラウザでアクセスするとModel View Controllerのコード例と同様のページが表示されます。
共通のViewを使うようなアプリケーションであるならTwo Step Viewを利用すると良いです。 Ruby on Rails の レイアウト という概念や、厳密には違うかもしれませんが、 Atomic Design のTemplateのように様々なデザイン手法で当たり前のように登場するパターンだと思います。
Mustacheによる簡単な例をご紹介します。すべてのコードを参照したい場合は こちら をご覧ください。
Model View Controllerのコード例と違って、共通で利用できるレイアウトが登場しています。レイアウトには `html` タグや `body` タグが記述されています。
// Layout
<html>
<head>
<title>Two Step View</title>
</head>
<body>
{{> @partial-block }}
</body>
</html>
各ページのViewはレイアウトを共用することで、それぞれのページに必要なタグを書くだけで済みます。
// View
{{#> layout }}
<ul>
{{#each crews}}
<li>{{this.name}} {{this.bounty}}</li>
{{/each}}
</ul>
{{/layout}}
Controllerパターン
Controllerには2つのパターンがあります。
基本的な実装はページごとにオブジェクトを分けるPage Controllerパターンで実装をします。ページごとよりもアクションごとに分けるような実装も好まれます。また、RailsなどのようにリソースごとにControllerを実装することも多いと思います。このあと登場するパターンと同時にコード例を紹介します。
Controllerには2つの責務があります。
HTTPリクエストをハンドリングすること
HTTPリクエストを元に何をするか決定すること
この1つ目の責務であるHTTPリクエストのハンドリングを一手に担うのがFront Controllerパターンです。パスを変更するときに、Webサーバー側の設定を変更する必要がなくなるという利点があります。Railsでは ルーティング という概念があり、これも大抵の人は当たり前のように利用していると思われます。
Page ControllerとFront Controllerのコード例を一挙に紹介します。すべてのコードを参照したい場合は こちら をご確認ください。
Modelは相変わらずDBを使わない適当なものです。
// Model
export class Crew {
constructor(
public id: number,
public name: string,
public bounty: bigint,
) {}
static find(id: number): Crew {
const crews = this.findAll();
const crew = crews.find((crew) => crew.id == id);
if (crew == undefined) {
throw new Error("Crew Not Found");
}
return crew;
}
static findAll(): Crew[] {
const crews = [
new Crew(0, "Luffy", BigInt(1_500_000_000)),
new Crew(1, "Zoro", BigInt(320_000_000)),
];
return crews;
}
}
一覧のページを表示するContorollerです。Viewは大したことないので割愛します。
import { dirname, fromFileUrl, HandlebarsJS } from "../deps.ts";
import { Crew } from "./model.ts";
const __dirname = dirname(fromFileUrl(import.meta.url));
const indexTemplate = await Deno.readTextFile(
`${__dirname}/index.html.mustache`,
);
const indexView = HandlebarsJS.compile(indexTemplate);
export class IndexController {
static page(_request: Request): Response {
const crews = Crew.findAll();
const html = indexView({ crews });
return new Response(html, {
status: 200,
headers: { "content-type": "text/html; charset=utf-8" },
});
}
}
詳細のページを表示するControllerです。一覧ページと違って個々の `crew` の情報が載っているHTMLを返します。こちらもViewは大したことないので割愛します。
import { dirname, fromFileUrl, HandlebarsJS } from "../deps.ts";
import { Crew } from "./model.ts";
const __dirname = dirname(fromFileUrl(import.meta.url));
const crewTemplate = await Deno.readTextFile(
`${__dirname}/crew.html.mustache`,
);
const crewView = HandlebarsJS.compile(crewTemplate);
export class CrewController {
static page(request: Request): Response {
const url = new URL(request.url);
const idString = url.searchParams.get("id") || '';
const crew = Crew.find(parseInt(idString));
const html = crewView({ crew });
return new Response(html, {
status: 200,
headers: { "content-type": "text/html; charset=utf-8" },
});
}
}
最後にルーティングを担うFront Controllerです。リクエストされたURLパスによって利用するControllerを変えています。
import { IndexController } from "./index_controller.ts";
import { CrewController } from "./crew_controller.ts";
export class FrontController {
static handler(request: Request) {
const url = new URL(request.url);
switch (true) {
case url.pathname === "/crew":
return CrewController.page(request);
default:
return IndexController.page(request);
}
}
}
Webブラウザでそれぞれのページにアクセスすると以下のように表示されます。
さいごに
今回はChapter 4. Web Presentationについての紹介をしました。
説明が不足していたり、わかりにくいようなところがありましたら、お気軽にご連絡いただければと思います。
改めてになりますが、今回のChapterの内容は今から考えると古かったり、もしくは当たり前となっているようなものが多かったと思います。
実際にWebアプリケーションを実装するときには、ここで登場しているパターンの内容を気にするよりも、まずはWebフレームワークやライブラリーの流儀に従うのが良いと思います。
WebフレームワークやWebフロントエンドの進化は激しいので、パターンなどの最新の動向は今後も勉強を続けていく必要がありそうです。大変ですけど。
次回はChapter 5. Concurrencyを紹介します。どうぞよろしくお願いします。