ymemo

ハードルをすごく低くして書いていこう

手順から始めるオニオンアーキテクチャ

f:id:drowse314:20180908225803j:plain
Photo by Webvilla on Unsplash

LTで語るにはパッションと尺が足りず、勉強会で詳しくやるにはニッチすぎる技術的な話題が頭の中に燻っているので、たまにこちらに吐き出していきたいと思いました。

明確に誰かが読むことを想定していない自由研究のようなものですが、この文字の多さはちょっとひどいですね...

以下のような内容になります。

オニオンアーキテクチャ

解消できない技術的負債アンタッチャブルなレガシーコード で消耗してますか?
頑張って 立派なアーキテクチャ を採用したのに、 実践で問題 がありますか?

オニオンアーキテクチャ はその名の通り ソフトウェアアーキテクチャ の一つで、 クリーンアーキテクチャ のように、コード全体をどう整理するかという指針を示すものであり、 Flux のように、方法論ではなく設計思想が核にある類のものでもあります。

以下のような特徴があります:

  • インフラストラクチャの選択肢や、バックエンド/フロントエンドの別に関わらず適用できる
  • インターフェースを積極的に活用 し、静的型付け、および DI / IoC との相性がよい
  • ビジネスロジックの変化とインフラストラクチャの変化の分離を促進 する

歴史は長い 1 ので、 成果物に関する解説 は既にすばらしい or わかりやすいものがたくさんあります。

実践プロセスの問題

オニオンアーキテクチャは、一昔前にDDDが一瞬盛り上がった (?) 頃の話題という印象で、完成形がどうなっているか、という記事はそれなりに発見できます。
しかし、 実践プロセス に関して言及するものは容易には見つけられず、何から始めるか、どうサイクルを回していくか、という知見はあまり出回っていないのではないでしょうか 2

実際に仕事で使ってみると、オニオンアーキテクチャ初期開発から実践する過程にこそ大きな救いがある 、特に複雑なアプリケーション構築の際に役に立つ、ということを実感しました。
テスト駆動開発 は今やよく知られた、フィードバックループを活用して安全に開発を進めるための習慣ですが、オニオンアーキテクチャの実践プロセスには テスト以外の場面にも様々なフィードバックループを設定 する余地があり、アプリケーション構築プロセス全体をより安心・安全で質の高いものに引き上げてくれるポテンシャルがあります。

この記事では、これまでのオニオンアーキテクチャ実践で感じた利点を、Step by Stepで追う開発事例とともに言語化してみたいと思います。

プロセスとしてのオニオンアーキテクチャ

今回、 画像認識を雰囲気で使っていくSlack Bot の開発をサンプルとして扱っていきます。
このアプリケーション自体は複雑なものではないですが、画像認識やチャットボット連携は成長中の分野だけあって、I/Oインターフェースが熟れていなかったり、扱いにくい副作用があったりするので、純粋なビジネスロジックとの共存を考えるのに良いテーマだと思いました。

以下では、このアプリケーションの起案から実用化までステップを追いながら、オニオンアーキテクチャがどう開発をサポートしてくれるか見ていこうと思います。

尚、今回作ったBotソースコードは、全て以下に公開されています。

Step 1. 要件の詳細化

まずは、特にオニオンアーキテクチャと関係のない普通の活動として、やりたい事を、 ユースケース レベルで詳細化してみます。

オニオンアーキテクチャではコーディングの初期から、実際のユーザのユースケースを明確にイメージする必要があります。
まずはどんな表現でもよいので、形にしてみます。

事例

Image File Spoilerのイメージ
Image File Spoilerのイメージ

Slackに画像を共有すると、通信が遅くて画像が見れない人のために何が写っているか教えてくれる よくあるAIクソアプリ Botつくります。

とりあえずこれを、 Image File Spoiler と名付けてみましょう。 Botが参加しているchannelのみで有効になる想定で、画像のようなシーンの実現を目指します。

Step 2. インフラ設計・技術検証

普通の活動その2。
求められるユースケースがどんな技術を利用して実現できるか、丁寧に検証します。

オニオンアーキテクチャ自体は、 「どんなにインフラ設計をこねくり回しても、ソフトウェアの価値は人間が担保しなければ風化するか形骸化する 」という課題に対するものですが、一方どんなにビジネスロジックやUXをこねくり回しても、 技術的にムリがあるものは成立しないか、続きません

事例

この記事の主題ではないため、簡単にまとめます。

Slackのくだりは実現可能なのか?
Botのサーバは?
  • 立てたくないので、AWSAPI Gateway + Lambdaにしておく
  • Slackへのレスポンスを同期的に返すのは厳しそうなので、API GatewayとLambdaの間にはLambdaを呼ぶだけのStep Functionsステートマシン実行を挟んでおく 3
画像の中身を言い当てるのは?
言語

言語も重要な技術要素の一つです。
今回は、 TypeScript を選択します。

オニオンアーキテクチャではDIとインターフェースを重用するため、 型の表現力が豊かな程、受けられる恩恵は大きい ように思います。
TypeScriptには、インターフェースはもちろん、null安全なコンパイルオプション、 Promise による副作用の明示等、効果的で安全なDIをやっていくための資質が十分に備わっています。

ここではなるべく、 使い捨てコード 等も使って、これらの技術的な仕組みが実際に成立するかどうかを検証し切ってしまいます 5

Step 3. ユースケースの設計・記述

いよいよコーディングに突入します。

ここでは、実際に ユースケースをソフトウェアに まで落とし込みます。
オニオンアーキテクチャでは、 インターフェースのみを用いてドメインモデルを記述 し、このモデルを使って実現したい ユースケースの場面 を表現します。
どんなモデリングをしたいかによってコードでの表現の仕方は自由ですが、 ...Table とか <何らかのライブラリ名>Service のようなインフラストラクチャに依存した概念を盛り込まないよう、気をつけます 6

概念設計とコードライティング (=インターフェース定義) を同時にやってもよいですが、ここで重要なのは インターフェースが「書けないな」と思ったら設計を直す ことです。これがオニオンアーキテクチャ実践の 最初のフィードバックループ になります。

事例

実践ではまず、どのようにユースケースの場面を表現するかイメージする必要があります。
名前を付けてみる のが出発点になるでしょう。今回は、 PublishImageFileSpoilerMessage 、「画像のネタバレメッセージを発行する」というユースケースをメインの活動として考えてみます。

ユースケース &#x60;PublishImageFileSpoiler&#x60; のイメージ
ユースケース `PublishImageFileSpoiler` のイメージ

コード以前の作業のやり方は自由ですが、ここでは PublishImageFileSpoilerMessage という場面を、テキストと図を使って表現してみました。
個人的には、 に、図のような関数型設計風のオブジェクト同士の関係性を手書きで図示してみるのが好きです。あらゆる枠線や矢印が自由に使えるし、モデルがなんだか身近に感じられるような気がします。(ちなみに、図中の波線が途中に差し挟まれた矢印は、副作用のある関係を示しています。)

図とともに、以下のようなインターフェース風のメモも書いたりしながら、コードのイメージを固めていきます。

<<PublishImageFileSpoilerMessage>>
    #fromNewImageFileEvent(event: NewImageFileEvent): Promise<void>

ImageFileSpoilerMessageSink
    #publishMessage(spec: PublishMessageSpec): Promise<void>

ImageRecognizer
    #findObjects(img: ImageFileRef): Promise<ObjectLabels>
...

ここでのシナリオは、以下のようなものです:

  • 新しい画像ファイルの発見は、 NewImageFileEventProviderNewImageFileEvent として提供する: イベントからは画像のバイナリを取得する方法の記載された ImageFileRef が得られる
  • ユースケース PublishImageFileSpoilerMessage では、 ImageFileRef から画像認識器 ImageRecognizer を用いて ObjectLabelsInImage を取得する
  • PublishImageFileSpoilerMessage は最後に、ラベルを用いて ImageFileSpoilerMessage を作成し、 ImageFileSpoilerMessageSink に投入する

インターフェースをコードに落とし込んだ結果、以下のようになりました (少し単純化しています) 。
図のモデルからは、少し修正を加えた形になっています。

// 画像関連
interface NewImageFileEventProvider {
    get(): Promise<NewImageFileEvent>;
}
interface NewImageFileEvent {
    getImageFileRef(): Promise<ImageFileRef>;
}
interface ImageFileRef {
    getContent(): Promise<NodeJS.ReadableStream>;
}

// 画像認識
interface ImageRecognizer {
    findObjects(imageFileRef: ImageFileRef): Promise<ObjectLabelsInImage>;
}
interface ObjectLabelsInImage {
    labelsWithConfidence: ReadonlyMap<string, number>;
}

// メッセージ送信
interface ImageFileSpoilerMessage {
    mesage: string;
}
interface ImageFileSpoilerMessageSink {
    publish(message: ImageFileSpoilerMessage): Promise<void>;
}

// ユースケース全体
class PublishImageFileSpoilerMessage {
    public invole(): Promise<void> {
        // 上記の役割群を利用してユースケースを表現する (今はしなくてよい)
        return Promise.resolve();
    }
}

実際のコードの全体像は こちら から確認できます7

この段階でユースケースの中身の実装まで行う必要はありません。ここでは自由奔放に作った ユースケースのモデルが、ソースコードの世界のインターフェースにマッピングできるかどうか だけ検証できればよいのです。

オニオンアーキテクチャでは、ここで記述するものをインフラに依存しすぎない形で変更できるのが1つの利点で、DDD的なドメイン知識の表現や関数型設計など、やりたい放題できるのが楽しいところです。

Step 4. ユースケースのテストの記述

今の所、このアプリケーションは絵に描いた餅 (=インターフェースだけの存在) ですが、ここで テスト を書きます!
このStepでの 目的は2つあり、それぞれがフィードバックループを形成 します。

実施するのは一般的なTDDですが、これらのフィードバックループを適切に運用するため、以下の点に留意します。

テストの対象はユースケース

テストコードを記述する対象は、いわゆる "ユニットテスト" の対象となるようなオブジェクトではなく、ユースケースのインターフェースです。多くのアプリケーションでは、 ただ一つのオブジェクト に対するテストを記述することになります。

外部設定や外部依存関係、その他 アプリケーションコードだけではコントロールできない副作用を持つ全てのオブジェクトのモック を作成し、 唯一の実体を持つオブジェクトであるユースケースのテストを記述・実施 します。
モックの対象となるオブジェクトは通常、 単発のユースケースよりも生存期間が長く、いつも暗黙的にただそこにあってアプリケーションに特定の役割を提供するようなもの です。ファイルシステム、データベース、ネットワーク、外部サービスの類が関連するものは全てこれに該当します。個人的にはそれらを "ドメインの役割" と呼んで、その他のオブジェクトと区別しています。

ここでもやはり、 「テストが書けないな」と思ったら、Step 3. のモデリングを修正 します。 「テストケースが書けないな」も「モックが書けないな」も等しく修正対象 となります。これが全体から見て 2つ目のフィードバックループ です。

ユースケースのテスト駆動実装

テストの記述さえできれば、あとは Red-Green-Refactor のプロセスに従ってテストを進め、ユースケースの内部の実装をやり切るだけとなります。

TDDに慣れている開発者からすればお馴染みの感覚かもしれませんが、テストケースとの対話を通じて、ぼやけたイメージしかないユースケースの詳細仕様を固め、また実装していきます。これは 3つ目のフィードバックループ となります。

事例

まずは、テストで実際にユースケースを動かすにあたり必要となる、DI機構の整備を行います。

今回は、 InversifyJS を利用しました。

DI/IoCコンテナは生成関数を定義すると、テスト等での取り回しがしやすくなります。

import { Container } from "inversify";
export const buildServiceRegistry = (): Container => {
    const registry = new Container({ defaultScope: "Singleton" });
    // 様々な実装の注入
    ...
    return registry;
};

また、自動テスト機構の整備を行います。

今回は、AVA を利用しました。
クリーンで好きな選択肢ですが、オニオンアーキテクチャのテストは基本的に複雑にはならないので、シンプルな機能で足りるという意味でもAVAにしています。

ユースケースのテストに向けて、 "ドメインの役割" のモックを作成していきます。

インターフェースの契約を満たすもっとも単純な形にしますが、もしテストのためにSpyのような機能が必要であれば加えます。そういった機能が必要にも関わらず、テストで使えるモックが作れないのであれば、インターフェースを見直したほうがよいでしょう。

// 単純なケース
const createNewImageFileEventProviderMock = (
    event: NewImageFileEvent
): NewImageFileEventProvider => {
    return {
        get: () => Promise.resolve(event)
    };
};

// Spyのような機能を持つパターン
// * 後から `sink` を検査し、
// * `publish` された `ImageFileSpoilerMessage` を追跡する
const createImageFileSpoilerMessageSinkMock = (
    sink: ImageFileSpoilerMessage[]
): ImageFileSpoilerMessageSink => {
    return {
        publish: message => {
            sink.push(message);
            return Promise.resolve();
        }
    };
};

あらゆる "ドメインの役割" をモック化したところ、ユースケースのテストには 画像から検出したラベルのパターンに対してどんなメッセージを発行するか というロジックだけが残りました。

import { Container } from "inversify";
import { test } from "ava";

// ユースケースのテスト実行関数
const invokeUsecase = (
    registry: Container,  // DIコンテナ
    recognizerResult: ObjectLabelsInImage
): Promise<void> => {
    registry.bind<ImageRecognizer>(ROLES.ImageRecognizer)
        .toConstantValue(mocks.createImageRecognizerMock(recognizerResult));
    return registry
        .get<PublishImageFileSpoilerMessage>(ROLES.PublishImageFileSpoilerMessage)
        .invoke();
};

// 典型的なテストケース
// * ※ AVAのTest Contextには、
// * 毎回モックと共に初期化されたDIコンテナが差し込まれるよう小細工をしています
test(">=90%のラベル1つを検知", t => {
    const labels: [string, number][] = [["dog", 1.0]];
    const recognizerResult: ObjectLabelsInImage = { /* labelsを用いて結果を構成 */ };

    return invokeUsecase(t.context.registry, recognizerResult).then(() => {
        t.is(t.context.messagesSent.length, 1);
        t.deepEqual(t.context.messagesSent[0].formalExpr, "certainly(dog)");
    });
});

一応いくつかの "ドメインの役割" の中をデータが流れては行きますが、テストが検証するのは結果的に、何ら副作用を持たないメッセージ構築のルールのみとなりました。

ユースケーステストで検証されるのは普通、アーキテクチャに依存しない部分のうち、実務上もっとも面倒で、もっともソフトウェアの挙動にとって重要なものになります。これはいわゆる "ビジネスロジック" と呼ばれるものですが、外部依存関係を全て排除し、大部分の知識を契約のインターフェースに委ねた後に見えてくるのは、案外このように単純な宣言的定義だけだったりするかも知れません。

最後に、テストが通るよう、モックのままの "ドメインの役割" を使ってユースケースを実装します。

ユースケースに必要な役割を全てInversifyJSの機構によって注入し、これらを用いて仕様を表現します。
最終的には、おおよそ以下のような形になりました。完全なコードは こちら になります。

// 新しい画像ファイルへの参照を取得し、
const newImageFileEvent = await this.newImageFileEventProvider.get();
newImageFileEvent.getImageFileRef()
    .then(imageFileRef =>
        // 画像認識器からラベルを取得し、
        this.imageRecognizer.findObjects(imageFileRef)
    ).then(objectLabelsInImage =>
        // ラベルの情報からメッセージを作成し、
        this.composeImageFileSpoilerMessage(newImageFileEvent, objectLabelsInImage)
    ).then(message =>
        // 投稿する
        this.imageFileSpoilerMessageSink.publish(message);
    );

ここまでで、外部依存関係を含まず、どんな環境でも実行できるテストが用意できました。これは、ビルド時間を除けばわずか数秒で、ビジネスロジックの包括的な検証を行えるものです。
次のStepではこれを、アプリケーションとして動かしていきます。

モックを使ったテストでは、不安があるでしょうか?

オニオンアーキテクチャでは、ユニット・コンポーネントレベルの動作が (ユースケースとの間の) インターフェースによる契約関係によって保証されています。
ここで検証するのは、契約に従って記述されたユースケースが正しく動作するか、あるいは契約そのものの整合性であって、個々のユニット (ドメインの役割) が契約に従っているかどうかではない という点が重要です。

個々の "ドメインの役割" が契約を守って動作するかどうかは、実際のインフラストラクチャと接続された状態で検証しなければ意味がありません。外部サービスやDBをテストしようとするなかれ、とはTDDの金言ですが、オニオンアーキテクチャではこの警告に対して、具体的な指針を提供します。

もちろん、よほどの余裕があるか、よほど複雑な役割の実装があるのであれば、個別の実装ごとにテストする (e.g. Dockerでテスト用DBを準備する前提でのユニットテスト) のも良いでしょう。しかし、ユースケースとの契約はユースケースの発展に伴って変更される場合があることを、心に留めておく必要があります。

Step 5. アプリケーションの作成とドメインの役割の実装

テストが終わったら、いよいよユースケースを起動/実行するための アプリケーションを、技術要件に従って実装 していきます。これに伴い、モックのみの状態だった "ドメインの役割" を、実際のインフラストラクチャ上で動くように記述 していきます。

このStepは、利用するインフラに対する知識や検証の成果を使って 最適な処理 を記述する 最も純粋に技術的で泥臭いフェーズ ですが、ユースケースとの契約さえ破らなければ、実際には公開インターフェースを守って雑にコードを書くだけ、という作業になります。各 "ドメインの役割" に求められる入出力の契約は型によって守られているので、 コードのクオリティは気にしすぎない で進めていきます。

Step 5.では明確にフィードバックループがある訳ではないですが、ユースケースがアプリケーション統合しにくい形だったりした場合に修正することがあるかも知れません。
ちなみに、実際の外部サービスやインフラとの接続のため、設定注入機構やロガーを用意するのもこの段階になります。

事例

PaaS等で動かすアプリケーションの場合、先にローカルマシンで動かせる形のアプリケーションを作って動くようにすると便利です。アプリケーションの実際の動作を安全に検証できるだけでなく、「インフラ構成や設定のせいかと思ったら実装コードがまずかった」という類の無駄なつまずきの予防になります。
オニオンアーキテクチャでは、簡単な役割の実装やアプリケーションのエントリーポイントを差し替えるだけで、こういったものを素早く制作できるのも利点の一つのように思います。

ということで、まずは、 ターミナルの対話入力で画像ファイルの絶対パスを読み取り、実実装を使って結果を発行するCLIアプリケーション を作ってみます。

以下のオブジェクトを実装しました:

エントリーポイントの処理の流れは、外部環境に接しているアプリケーションインターフェース (ここではCLI) に合わせ、 適切な "ドメインの役割" をアダプタとして用意 して注入 、あとはユースケースを起動するだけ、という形になります。

以下のような実装になりました。完全なコードは こちら になります。

// DIコンテナのルートを取得
const serviceRegistry = buildServiceRegistry();

// コマンド入力待機型のアダプタ(= `NewImageFileEventProvider` のCLI適応系)を作成して注入
serviceRegistry
    .bind<NewImageFileEventProvider(ROLES.NewImageFileEventProvider)
    .to(CliNewImageFileEventProvider);

// 同様に、 `ImageFileSpoilerMessageSink` を適応・注入
const messageLogger: ImageFileSpoilerMessageSink = {
    publish: message => { console.log(message); return Promise.resolve(); }
};
serviceRegistry
    .bind<ImageFileSpoilerMessageSink>(ROLES.ImageFileSpoilerMessageSink)
    .toConstantValue(messageLogger);

// ユースケースを取得し、起動する
const usecase = serviceRegistry
    .get<PublishImageFileSpoilerMessage>(ROLES.PublishImageFileSpoilerMessage);
usecase.serve();

動作させてみましょう。

CLIアプリケーションの動作の様子
CLIアプリケーションの動作の様子

結果出力以外のログが少し鬱陶しいですが、動きましたね!

このアプリケーションでは、CLI実装を NewImageFileEventProvider実装クラス に委ねています。
CLIはプレゼンテーションの一種と言えますが、オニオンアーキテクチャでは プレゼンテーションはインフラストラクチャの一部 であり、基本的に "ドメインの役割" の一部として実装されます。オニオンアーキテクチャはこの点が曖昧になると、ただの 手の混んだレイヤードアーキテクチャ になりがちなので注意しておきます 8


CLIアプリケーションが想定通り動作することを確認できたら、 AWS Lambda上で動作するアプリケーション を作成します。
プロダクション用の役割の実装を行いますが、既にユースケースの中心部の実装は終わっているので、 インフラストラクチャアダプタを作って注入 するだけ、という形になります。

入力アダプタ は、AWS Lambda関数に入力されたSlackのイベントペイロードに応じてオンデマンドで作る必要があります。こちらは、InversifyJSの Container#resolve (依存関係の注入が定義されたクラスをその場で解決する機能) を用いて ファクトリ を取得、生成してみました。

最終的なAWS Lambda関数ハンドラは、以下のようになりました。完全なコードは こちら になります。

export const handler: AWSLambda.Handler = async (event, _context, callback) => {
    // DIコンテナのルートを取得
    const serviceRegistry = buildServiceRegistry();

    return serviceRegistry
        .resolve(SlackImageFileEventProviderFactory).getProvider(event)
        .then(({ provider, slackNewFileEvent }) => {

            // AWS Lambdaハンドラ型のアダプタ
            // (= `NewImageFileEventProvider` のLambda起動イベント(+Slack Event)適応系)を作成して注入
            serviceRegistry
                .bind<NewImageFileEventProvider>(ROLES.NewImageFileEventProvider)
                .toConstantValue(provider);

            // 同様に、 `ImageFileSpoilerMessageSink` を適応・注入
            const imageFileSpoilerMessageSink = serviceRegistry
                .resolve(SlackImageFileCommentatorFactory)
                .createSlackImageFileCommentator(slackNewFileEvent.event.file_id);
            serviceRegistry
                .bind<ImageFileSpoilerMessageSink>(ROLES.ImageFileSpoilerMessageSink)
                .toConstantValue(imageFileSpoilerMessageSink);

            // ユースケースを取得し、起動する
            const usecase = serviceRegistry.get<PublishImageFileSpoilerMessage>(
                ROLES.PublishImageFileSpoilerMessage
            );
            return usecase.invoke();
        })
        .then(
            () => callback(null),
            err => callback(null, err)
        );
};

動かしてみると…

AWS Lambdaを用いたSlack統合アプリケーションが動いている様子 鳥 竹林 バス 鳥の絵

問題なさそうですね! 9

完成!そして今後の変更にむけて

今回のアプリケーションの実装はこれで完了ですが、オニオンアーキテクチャでは、以後のコードの変更の際も、同じようなStepを辿ってコードの健康状態を維持していきます。

ユースケースビジネスロジックに変更があった場合は、基本的に今回のStep全体をもう一度なぞる形になります。
変更内容に応じて、ユースケース、モデルインターフェースとテストの修正を行います。静的型付けの言語で実装を行った場合、インターフェースで表現された "ドメインの役割" 等の契約が変更されると必要な変更点がコンパイルエラーとしてリストアップされるので、これに従って修正を実施できます。
また、場合によっては "ドメインの役割" を新たに追加する必要がある場合もあるかもしれません。

あるいは、インフラストラクチャに対する要求が変化した場合、まずは技術検証フェーズを経て、変更後の構成やデータ移行の構成を固める必要があるでしょう。
その後、 "ドメインの役割" の実装を新しいインフラストラクチャの特性に合うよう書き換えるか、または丸ごと新しい実装に差し替えることができます。
インターフェースによって交わされた、ユースケースとの小さな契約を守りさえすれば、実装の提供の仕方には自由があります 10

ところで、いずれの場合でも、開発者にはアプリケーションのユースケース、および "ドメインの役割" の契約に対するそれなりの理解が求められます。オニオンアーキテクチャでは、ソフトウェアに それを理解しなければ容易には変更できない制約 を付加します。
これが望ましい性質かどうかは、プロジェクトの種類や規模、存続期間に依りますが、少なくとも品質に対する要求が高いソフトウェアでは、目的の達成に貢献してくれる可能性が高いと思います。

考えられる論点

実践上の補足をいくつか、追記します。

時間はかからないのか?

実装フェーズのみを "やらない場合" 11 と比較すると、もちろん若干の追加時間を要し、体感では最大1.5倍程になるように思います。
ただ、個々のStepで目的として設定された検証を確実に行い、前のStepに対するフィードバックが機能すれば、全体の手戻りはかなり少なくなるはずです。
もともと、数週間〜数ヶ月で捨ててしまうようなソフトウェアの開発には向かないので、対象のプロジェクトや実践のスコープは適切なものを選ぶ必要があります。

これはウォータフォールではないのか?

巨大な開発プロジェクト全体でこれらのStepを進めていくようであれば、そうなってしまう可能性はあります。一方、そういったスコープの切り方はそもそも適切ではありません。
あくまでユースケース (ログインするだけ、会員登録するだけ、商品をカートに入れるだけ、等) の単位で、局所的に整合性のあるモデルを作って開発に役立てるのがオニオンアーキテクチャの核となります。
その後、モデルをどのくらい多くのユースケースで横断して利用できるものにするかは、マイクロサービス分割等と同じように、別途判断を要するものです。

まとめ

ここまでで、オニオンアーキテクチャを実際に始める際の手順を通して、この方法がどのように安心・安全で高品質な開発に貢献してくれるか見てきました。

人による部分はあると思いますが、ソフトウェア開発の中で単純作業や流れ作業が続くような場合、個人的にはとてもやる気が削がれます。
オニオンアーキテクチャでは、プロジェクトを始めるとあらゆるステップに設計とフィードバックループが含まれ、開発中は常に、ソフトウェアの向上に貢献する知的な作業に時間を費やすことができます。
冒頭でも述べましたが、オニオンアーキテクチャは上手く使えばこういった意味でも、完成形だけでなくプロセスで大いに価値を発揮できるアーキテクチャだと思います。

実際の開発では何よりもスピードを求められることの多い時代ですが、自分の「理想的な手順や形」のイメージがあると、日々の細かい作業の最適化やアーキテクチャの行き詰まりの問題に直面しても視界が保て、生産的でいられる助けになるように思います。
それは自分にとって今の所、オニオンアーキテクチャや関数型です。


  1. 自分が オニオンアーキテクチャの電波 を初めてちゃんと感じ取ったのは、所謂「DDD実践本」だったかと思います。調べる限りでは、はじめに提唱したのは Jeffrey Palermo氏 (2008) という方なのでしょうか (? “自分は名前を付けただけだ” とのことではあるが)。ちなみに、DDDと関わりが深いのは確かですが、少なくともDDDの文化的側面を抜きにしても実践できるアーキテクチャです。

  2. これは XXXアーキテクチャ 全般に言えることかもしれませんね… とはいえこの記事のように、書いてみると際限なく長くなってしまうのであまりやられないのかもしれません..

  3. あまり適切な使い方ではないですが、 こちら をちくちく設定するより楽だったので…

  4. TensorFlow.js のNode.jsバインディングを使いたいと思っていたのですが、記事を書き始めた頃はまだ開発中でした…

  5. 少し手順が重いようにも思えますが、もし「お試しコード」やプロトタイピングを作成する場合はこの段階の活動でプロジェクトを終え、以降のStepに進む必要もないと考えます。

  6. 技術検証のことはいったん全て忘れます。おおよそのインフラ構成が頭の片隅にありさえすれば、ここでどれほど自由に設計をしても、アプリケーションが破綻することはないように思います。またインフラは蔑ろにされる訳ではなく、後に外部依存関係の分離をレビューするStepや、集中的な実装を行うStepがあります。

  7. 次のStepで導入するDIためのコードが既に少し混入しています。

  8. とはいえ、例えばWebアプリケーションフレームワークのようなものに接続する場合、全てのメソッドを役割としてユースケースに注入するのはあまり現実的ではないでしょう。ユースケースの実装は “ポータブル” なので、ユースケースを “サービス” のようにしてメソッドハンドラ内で使い回すことができます。どのような “サービス” 分割、メソッド分割にするかは自由ですが、この場合も、CLI等の別のプレゼンテーション方式では “ドメインの役割” として注入されるべきものがユースケースの外に漏れ出ないよう、注意しておく必要があります。

  9. デプロイやその他、インフラストラクチャを整える作業がここで発生するはずですが、この部分はオニオンアーキテクチャのスコープ外なので省略します。

  10. と、言葉で記述しても説得力に欠けますね… 元々やりたかったTensorFlow.jsの組み込みや、Amazon Rekognitionの画像サイズ制限に対応するための修正を、別途実施してまとめてみたいところ。

  11. 厳密に、それはどんな場合?と言われると難しいのですが..