1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

クリーンアーキテクチャは変更コストが高い! YAGNI原則を適用して軽量化する

1
Posted at

クリーンアーキテクチャを採用すると逆に変更コストが高くなってしまう原因

プログラムのコードを書く際にクリーンアーキテクチャを使えば、適切に関心ごとの分離ができ、変更に強くなると言われています。 クリーンアーキテクチャで最も重要なルールは、依存関係の方向を円の外側から内側へだけにすることです。

[Uncle Bob のホームページより](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)

実際のプロジェクトを見たほうがイメージしやすいと思います。 クリーンアーキテクチャに素直に従ったフォルダー構成は下記のようになると思います。

ドメイン的にはクラスは1つなのに、こんなにあるのは、ぱっと見ただけで煩雑だと感じるでしょう。 entities, interface-adapters, controllers, presenters, gateways, use-cases, ports という『分類』自体は間違ってはいません。 図に書いてある gateway や presenters などの標準的な単語をフォルダー名やクラス名などに使うことは良いことです。 しかし、クリーンアーキテクチャ的には gateway や presenters などは Interface Adapters 層 のサンプルなのでそれらの単語を使う必要はありません。

多くの分類があると、依存しなくなる代わりに、編集するときのコストが増大します。 クリーンアーキテクチャを提唱した Robert C. Martin(Uncle Bob)の本 The Clean Architecture のまえがきに「コストが高く、変更が難しく、数日や数週間で終わらないプロジェクト型の変更であってはいけない」と書いてありますが、クリーンアーキテクチャに素直に従ったらコストが高くなってしまったというのは皮肉なことです。 実際、私が以前引き継いだクリーンアーキテクチャを使ったプロジェクトは、ドメイン、API、controllers, query, service, builder, DTO, facade, インフラ, スキーマ, ドライバー(ラッパー)と分類が多く、多くの属性が冗長でした。 それだけでなく不整合まであるため、変更コストがかなり高いものでした。

なぜこのようなアーキテクチャになってしまったのでしょう。 おそらくアーキテクチャを考えた人がドメインの変更よりもフレームワークやインフラの変更を過大評価してしまったからだと思います。 バージョンアップするごとにドメインの内容は毎回変化しますが、フレームワークやインフラが変更されることは滅多にありません。 たとえば、ほとんどのファイル形式は上位互換です。 なのに、非依存にするために、依存性逆転の原則(と呼ばれる)冗長な『インターフェース』(原則ではない)を書くから、コストが高くなってしまうのだと思います。

💡 フレームワークやインフラの変更よりドメインの変更のほうが圧倒的に多い

フレームワークやインフラが変更されることが滅多にないのであれば、YAGNI原則を適用すべきです。YAGNI原則は、"You aren't gonna need it."(そんなの必要ないって)の略で、機能が実際に必要となるまでその機能を追加しないという原則です。特に 『偶発的複雑性』は持たせてはいけません。 『過度な抽象化』や『早すぎる抽象化』として警告されることもあります。 コストが高くなる原因(煩雑さ)を埋め込まないように KISS原則を適用すべきです。 KISS原則は、"Keep It Simple, Stupid."(シンプルにしろバカ)の略で、文字通りシンプルにし続けるという原則です。

一応、The Clean Architecture の本では、「境界を完全に構築しようとすると、コストが高くつくことを認識する必要がある。(略)これがYAGNIの哲学である」(第25章 p277)と書いてありますが、具体的な方法やクラス図までは書いてないようです。

ダウンロード

サンプル プロジェクト や検証に使ったコードはリポジトリ https://github.com/Takakiriy/Trials/tree/master/ddd_ca_todo_example からダウンロードできます。 実際に動く ToDo アプリケーション のプロジェクトです。 参考として、ドメイン駆動設計に従ったサンプルも入っています。

煩雑さの原因はインターフェースの使いすぎ。循環の回避と型を意識すれば十分

クリーンアーキテクチャの著者は『依存性逆転の原則』(DIP)が大好きなようで、本の至る所に登場します。 ただ、依存性逆転の原則は、逆転する(させる)ことが原則という意味ではありません。 「高レベルのモジュールは低レベルの実装の詳細に依存してはいけないこと」が原則にあるが、実装に依存しないように、原則の方向とは「逆方向の依存を追加する」という意味です。 原則ではなく追加することです。 原則って何なんでしょうね。 第22章の「境界を越える」では、「依存性のルールに違反するため、直接呼び出すことはできない」「円の内側にあるインターフェースを呼び出す」と書いてありますが、これは、局所的に見れば内側からさらに内側を呼び出す意味ですが、俯瞰して見れば、内側から外側を呼び出していることになります。 依存性逆転の原則とは、実質、双方向で『呼び出す』ことができる方法です。(ただし、片方は『実装』に依存できません。)

依存することは、TypeScript や Python などの import 文を書くことに相当します。 クリーンアーキテクチャのルールに従えば、外側のファイルに、import (内側の名前) を書きます。 メソッドを呼び出すことなども依存することになりますが、import しないと呼び出せないので、結局、依存関係は import で表わされることになります。

依存性逆転の原則の「高レベルのモジュールは低レベルの実装の詳細に依存してはいけない」はルールですが、実際に問題になるのは、循環 import をすると高い確率で双方向に実装依存になって定義ができないエラーになることです。 これは、ルールよりも強制力があります。

このエラーに対処する方法としてインターフェースを定義してインターフェースを呼び出すことは、対処法の1つです。 ただ、円の外側にあるクラスと、円の内側にあるインターフェースは、型を書くことに関しては冗長です。 たとえば、外側のクラスのメソッドが追加されたら、インターフェースにも同じようにメソッドを追加しなければなりません。 メソッドの引数の数が増えたら、インターフェースにも同じように引数の数を増やさなければなりません。 このように『二重管理』が発生してしまうことが煩雑さの原因です。 ちなみに、DTO(Data Transfer Object)や facade などの中間オブジェクトを作っても二重管理が発生します。 たとえば、新しく配属されて DTO/facade の概念を知らない人が DTO/facade だけに存在する自称便利な属性を作ってしまったりして簡単にカオス状態になります。

二重管理を発生させないようにするにはどうすればいいのでしょう。 それは TypeScript の import type 文や、Python の TYPE_CHECKING を使うことです。 一般的によく使われる import との違いは、インポートするファイルに書かれたコードのうち型だけをインポートするところです。 ここでいう型とは、データ変数の型だけでなく、クラスのインターフェース成分(メソッドの型など)も含みます。 つまり、インターフェースを書かなくても同様の効果が得られるのです。 実装に依存していないので、『依存性逆転の原則』というルールは満たしています。

非依存にするなら Java で考案されたインターフェースを書く習慣はやめるべきです。 インターフェースが考案される前は、関数に分けるだけでも十分に非依存になっていましたし、それから何十年とプログラミングが進化した今でもライブラリとの接続は直接的な関数(メソッド)呼び出しが多くあり、特定のバージョンに依存することなく、バージョンアップは頻繁に行われています。 インターフェースを書くのであれば、少なくとも複数の具象クラスが存在し、動的にそれらのインスタンスを切り替える動作を書くことになってからです。

💡 DIP として非依存にするならインターフェースを書く習慣はやめ、関数に分けるだけでできる型を import type する

インターフェースを取り除いた結果、フォルダー構成はこのようになります。 ports フォルダー とその中にあった 3つのファイルが無くなりました

インターフェースを無くしてしまったら依存してしまではないか!何のためにインターフェースがあるのか知らないのか!と思う方はまだいらっしゃると思うので、後ほど具体的に比較検証していきます。

最低限分けるべきもの

簡単なプログラムであれば、アーキテクチャに従って分類してフォルダーやファイルを分ける構成にしないで 1箇所にすべてのコードを書いても問題ありませんが、プログラムが大きくなっていくとそうもいかなくなります。 でま、まず何で分類するのが良いのでしょうか。 The Clean Architecture の本(CA本)の第22章には、ヘクサゴナル アーキテクチャ、DCI アーキテクチャ、BCE などのアーキテクチャは似ていて、いずれも『関心ごとの分離』という同じ目的を持っていて、次の特性を持っていると書いてあります。

  • フレームワーク非依存
  • テスト可能
  • UI 非依存(ユーザーインターフェース非依存)
  • データベース非依存
  • 外部エージェント非依存

これらは分かりやすい分離なので、最初に UI やデータベースの実装部分を分離するのは良い方針です。 具体的には、controllers-and-presenters/TodoWebAPI.ts, gateways/TodoMapStore.ts に分ける(非依存にする)ことは良いことだということです。

presenters, gateways といった抽象的な単語がフォルダー名にあることが分かりにくさの要因としてありますが、ファイル名に Web API, database という明確な単語を使っているので、たとえフォルダー名の意味がよく分からなくてもファイルにコーディングする上で問題にはなりません。 CA本の UML には、クラスのボックスの右上に <I><DS> が書いてあるものがありますが、これらも分類基準としてフォルダー名に書くのはいいでしょう。 presenters, gateways, interface, data store といった抽象的な分類概念を知らない方でも、ファイル名から何となくフォルダー名の意味を知ることや調べるキッカケになることは良いことです。 逆に、ファイル名まで抽象的な単語しか使わないのは、良くありません。 ports フォルダー にあった 3つのファイルを無くしたことは、非効率的に調べることに時間が取られなくなるので、良いことです。

entity, use-case, indes は、比較的理解しやすい分類基準だと思うので問題ないでしょう。 entity はアプリケーションが対象とするドメインの言葉や概念(データ)、use-case はアプリケーションのユーザーが行う操作、index は起動時の処理です。

それらを踏まえると、3つのファイルが無くした現在のフォルダー構成よりもシンプルにする必要はないと思います。

インターフェースの有無で変更コストを比較してみる

YAGNI原則は未来のことを考えなくていいと言っているわけではありません。 将来必要になったときにどうすべきかを予想しておくほうが良いです。 その考えに基づいて、変更コストがどれだけ変わるのか比較してみました。

インターフェース アダプター の変更コスト

インターフェースを無くした代わりに型をインポートしたとしても、『具象クラスの名前という具体的なものに依存してしまう』という反論が考えらえます。 それがどれだけ変更コストが増えるのかを検証してみたいと思います。 具体的なものがあるといえたから問題だと判定するのは表面的すぎます。

サンプルにはデータベースの代わりに JavaScript のオブジェクトを使った TodoRecordStore と、Map オブジェクトを使った ToDoMapStore の 2種類があります。 そのうち実際に使われるのは片方だけです。 TodoMapStore を使うコードから TodoRecordStore を使うコードに変更するコストを比較してみましょう。(サンプルでは v2 から v1 に戻すことになります)

変更対象ファイル:

ユースケース層:
    ca-backend-lightweight-full/src/use-cases/TodoUseCase.ts
↓
インターフェース アダプター 層:
    変更前:
        ca-backend-lightweight-full/src/interface-adapters/gateways/TodoMapStore.ts
    変更後:
        ca-backend-lightweight-full/src/interface-adapters/gateways_v1/TodoRecordStore.ts

ユースケース層は『インターフェース アダプター 層の中の実装』に非依存という関係です。

手順

手順は、型を使っている場合でもインターフェースを使っている場合でも、同じです。

  1. ファイル名gateways/TodoMapStore.ts から gateways/TodoRecordStore.ts に変えます。 変えると、VSCode のリファクタリング機能で import 文も変えますかと聞かれるので、変えます。

  2. クラス名TodoMapStore から TodoRecordStore に変えます。 その際、クラス定義にあるクラス名 class TodoMapStore をクリックしてから VSCode のリファクタリング機能『Rename Symbol』で変えるようにしてください。 そうしないで、import 文 import { TodoMapStore } をクリックしてから変えると import { TodoMapStore as TodoRecordSrote } という局所的に変更したコードになってしまいます。

  3. ファイルの内容を変えます。 gateways_v1/TodoRecordStore.ts ファイルを gateways/TodoRecordStore.ts にコピーします。 その際、VSCode ではコピー先を削除してからコピーします。 そうしないと、VSCode では ___ copy ファイルができてしまいます。 また、フォルダーをコピーすると、コピー前だけに存在するファイルが残ってしまいます。

比較

手順に差はありませんが、手順を実施する前後のファイルの差分の量に違いがあります。 型を使っている場合に差分が多くなります。

実施後のクラス名の差分:

インターフェースあり インターフェースなし
円の中の層にあるクラス名 TodoGatewayPort TodoMapStore → TodoRecordStore
オブジェクトの構築 ❌ 差分あり ❌ 差分あり
円の中の層のコード ✅ 差分なし ❌ 差分あり
円の外の層のテスト ❌ 差分あり ❌ 差分あり

総合比較:

インターフェースあり インターフェースなし
全体のファイルの量 ❌ 多め 普通
変更されるファイルの量 ✅ 少い ❌ 多め
クラス名の抽象度 ❌ 抽象的で分かりにくい ✅ 具体的で分かりやすい
変更コスト 同じ 同じ

インターフェースがある場合に差分が少ない理由は、中の層に書かれているクラス名が抽象的な TodoGatewayPort であるからです。 クラス名が変わらないから良しとするか、変わったから良しとするかの違いですが、どんなデータベースを使っているかを要求仕様(ユースケース)で認識しているのであれば、変わったから良しだと思います。

変更コストの差はそれくらいしかありません。インターフェースが無いから依存して変更コストが膨大になることはありません

インターフェースを使っている場合:

index.ts:
非依存にインターフェースを使っている場合

(テストの差分は省略)

型を使っている場合:

index.ts:
非依存に型を使っている場合 index.ts

TodoUseCase.ts:
非依存に型を使っている場合 TodoUseCase.ts

(テストの差分は省略)

インフラの変更コスト

サンプルにはすでに TodoMapStore と TodoRecordStore がありますが、その内容を作るコスト(インフラの変更コスト、使うライブラリの変更のコスト)はどれだけかかったのでしょうか。

変更対象ファイル:

インターフェース アダプター 層:
    変更前:
        ca-backend-lightweight-full/src/interface-adapters/gateways/TodoMapStore.ts
    ↓
    変更後:
        ca-backend-lightweight-full/src/interface-adapters/gateways/TodoRecordSrote.ts

移植前後

このように、クラスの参照とメソッド呼び出しを編集するコストがあります。

少し蛇足ですが、import 文に差がありません。 通常は差があります。 オブジェクト(Record)とMapは、両方とも TypeScript で import 不要な特殊なクラスなので import 文が書いて無いからです。

このような変更コストを下げるために、オブジェクトと Map を抽象化した第3のインターフェース(ライブラリのラッパー)を挟むことは誰しも思いつくことだと思いますが、それがあると インターフェース アダプター 層 のメソッドを追加するコストはどれだけ減るでしょうか。

ユーザーにとっては、一般に知られているライブラリの使い方の知識が使えなくなるのでコストは逆に増えます。 オブジェクトから、独自ラッパーへのインターフェースの変更コストがインフラを使うコードを書くたびに毎回かかることになるからです。 インフラの変更は滅多に発生しませんが、ドメイン エンティティ の変更はよく発生するので、総合的に見れば変更コストは高くなります。 AI にコーディングさせる場合、独自ラッパーを使うコードを得るには手間がかかります。 そのことからも総合的に見れば変更コストが高くなることが分かるでしょう。

また、抽象化したインターフェースがライブラリを普遍的に抽象化している保証はありません。 たとえば、RDB から Key-Value 型(Value はJSON)のデータベースに変えたとき、とりあえず作っておいた RDB 型の抽象化インターフェースを引き続き使うと、便利な JSON へのシリアライズ関数が使えません。 設定(コンテキスト)の変更+メソッド呼び出しでできたことが、1つのメソッド呼び出しでできるように便利になった場合、古い抽象化インターフェースを引き続き使うと、便利になったものが使えません。 このようなズレは意外と多いので、とりあえずラッパーを入れておくことは単に二重管理になるだけです。

それらを無視して、まだラッパーを作ったほうがインフラの変更が必要になった場合に、上記の差分で表されるような編集を行うコストが高すぎる、と思うかもしれません。 しかし、すべてのファイルではなく、gateway(interface-adapters 層)の中にあるファイルだけしか対象ではありません。 interface-adapters 層 のフォルダーが無くても、特定のインフラを指定している import 文を検索して見つかったファイルだけしか対象ではありません。 interface-adapters 層 にコードを分けただけで TodoMapStore クラスのメソッドの構成(型)ができ、それは TodoRecordStore クラスのメソッドの構成(型)は変わりません。 なぜなら、Todo クラスを入力または出力していることから予想できると思いますが、型(暗黙のインターフェース)はドメイン(円の中)が定義したものであり、インフラ(円の外)に依存しないからです。 ライブラリを普遍的に抽象化している保証は不要です。

💡 インフラの変更コストを過大評価せず、インターフェースを作って普遍的に抽象化したつもりにならない

インターフェースあり インターフェースなし
ライブラリの知識 ❌ 使えない ✅ 使える
ライブラリ変更時 ラッパーの編集 インターフェース アダプター の編集
ライブラリの抽象化 ❌ 必要 ✅ 不要
変更の量 ✅ 少ない ❌ 多め
変更の難しさ ❌ メタレベルが異なる高度な編集が必要な場合がある ✅ 容易。パターンで編集でき検証もしやすい
メソッドの構成(型) ドメインが定義 ドメインが定義

データ構造の変更コスト

仕様が更新され、ToDo タスクの更新日が配列になったとします(サンプルでは最大要素数=2)。 更新日は記録するものですから、この変更をコーディングする対象として真っ先に思いつくのは、データベースのスキーマ(の更新)でしょうが、ToDo タスクの更新日を配列にする変更は、正しくはドメインのエンティティの変更です。 なぜなら、特定の種類のデータベースに固有の情報ではないからです。 config ファイルの構成の変更も同様にエンティティの変更ですが、Repository の中に書いた方が分かりやすいでしょう。

ということで ドメイン エンティティ で定義されている Todo クラスの _updatedAt プロパティの型を配列に変更しました。

ドメイン エンティティ 層:
    変更前:
        ddd-backend-lightweight-full/src/domain/entities_v1/Todo.ts
    ↓
    変更後:
        ddd-backend-lightweight-full/src/domain/entities/Todo.ts

配列化

ドメイン エンティティ 層 の変更は、クリーンアーキテクチャの円の中の変更ですが、円の外は円の中に依存しているので、円の外も変更が必要になります。 しかし、さまざまな都合ですぐに外が中の変更に対応できないこともあるでしょう。 たとえば、暫定的にデータベースだけ更新して画面は変えない、もしくは、社内評価用の画面だけ更新してデータベースは変えない、といったようなケースです。

ということで、皮肉なことに、依存していないはずの円の外の都合を円の中が対応しなければならないのですが、具体的には上記のように、従来と同じ仕様の updatedAt getter と、新しい仕様の updatedAtList getter を作ります(関数型プログラミングの方針に近い)。 こうすることで、プロパティの構成は、更新前(v1)の構成と更新後(v2)の構成をマージしたものにします。 よくある上位互換の構成です。 v1だけのインターフェースと v2だけのインターフェースをコーディングする必要はありません二重管理が必要になって変更コストが増えるだけです

しばらくして、すべて新しい仕様に対応したら、旧 updatedAt の廃止と、updatedAtListupdatedAt に名前を変えるリファクタリングを実施します。 サンプルは『そのリファクタリングができる状況ですが、する前の状態』です。

すでにお気づきかもしれませんが、v1, v2 と言っているものは、セマンティック バージョニング でいえば v1.0 → v1.1 のようなマイナーバージョンアップであり、リファクタリングは v1.1 → v2.0 になるメジャーバージョンアップです。 メジャー バージョンアップ は時々破壊的バージョンアップとも言われますが、実際は整理されるので良い変更です。

v1.0/v1.1互換なしインターフェースあり インターフェースなし上位互換(v1.1のみ定義)
v1.0ユーザー 使える 使える
v1.1ユーザー 使える 使える
差分の確認方法 インターフェースの差分 Git diff
二重管理 ❌ 発生 なし

テスト

インターフェースが無くてもドメインに対する ユニット テスト はできます。 ドメインにあるものをテストするときは DI (Dependency Injection、依存注入) という大袈裟なものを使わなくても直接オブジェクトを渡せば良いからです。 インターフェースを定義すれば DI(依存注入)ができて便利だと言う人がいますが、構造的部分型(structural typing) がある言語(TypeScript はある)ではインターフェースが無くても注入できます。 DI ライブラリの都合のためにインターフェースを定義するのは偶発的複雑性が増していますし、DI で注入するコードは間接的でわかりにくいです。 アプリケーション本体や結合テストでも、Composition Root のコードを書くのはそれほど難しくないですし、コードがあることによって構造の見通しが良くなります。

💡 DI ライブラリを使うより、Composition Root のコードを書いて、オブジェクトの構造の見通しを良くする

ddd-backend-normal/src/index.ts:

const repo = new TodoMapStore();
const domainService = new TodoDomainService(repo);
const useCase = new TodoUseCase(repo, domainService);
const todoPresentation = new TodoWebAPI(useCase);

ddd-backend-lightweight-full/src/application/use-cases/tests/TodoUseCase.test.ts:

beforeEach(() => {
    const db = new TodoMapStore();
    const domainService = new TodoDomainService(db);
    useCase = new TodoUseCase(db, domainService);
DI ライブラリ 手動DI
アプリのオブジェクト ✅ DI ライブラリで自動構築 ❌ 手動で Composition Root を書く, 容易
構成の表示 ❌ DI ツールによる コードを開く
ユニット テスト ❌ 注入メソッド呼び出し ❌ 生成コードを書く, ✅ 容易
DI用インターフェース ❌ 言語により必須 ✅ 不要

💡 ユニット テスト は、主な正常系のケースから外れる仕様に対して 1〜2種類の値を通すテストを作ります。 主な正常系のケースは結合テストで通るので、主な正常系のケースの ユニット テスト は冗長であり変更コストが増えます。 開発するために作った主な正常系の ユニット テスト は捨てるほうが変更コストが減ります。

効果

最初に示した通り、ファイルの数は減りました。 また、抽象的なクラス名も無くなりました。

また、インターフェースを無くしたことで、コードのブラウジング(定義へジャンプ)もシンプルになりました。

ddd-backend-lightweight/src/presentation/TodoWebAPI.ts:

await this.useCase.getAll()
  ⬇️
export class TodoUseCase {
    async getAll(): Promise<Todo[]> {
        return this.database.findAll();  // データベース
}}
  ⬇️
export class TodoMapStore {
    async findAll(): Promise<Todo[]> {
        return Object.values(this.store).sort(
}}}

ddd-backend-normal/src/presentation/TodoWebAPI.ts:

await this.useCase.getAll()
  ⬇️
export class TodoUseCase {
    async getAll(): Promise<Todo[]> {
        return this.repo.findAll();    // repo とは? Git リポジトリ?
}}
  ⬇️
export interface TodoRepository {
    findAll(): Promise<Todo[]>;    // ここでいつものように Ctrl + クリック すると、下記のような多くの候補が出てしまいます
                                   // 右クリックして Go to Implementations を選んでください
}
  ⬇️
export class TodoMapStore implements TodoRepository {
    async findAll(): Promise<Todo[]> {
        return Object.values(this.store).sort
}}

Go to Implementations を選ばないと多くの候補から選ばされて、大変なことになります。

まとめ

コストが高く変更が難しいクリーンアーキテクチャにならないために注意することは以下の点です。

  • フレームワークやインフラの変更よりドメインの変更のほうが圧倒的に多い
  • インフラの変更コストを過大評価せず、インターフェースを作って普遍的に抽象化したつもりにならない
  • DIP として非依存にするならインターフェースを書く習慣はやめ、関数に分けるだけでできる型を import type する
  • DI ライブラリを使うより、Composition Root のコードを書いて、オブジェクトの構造の見通しを良くする

YAGNI原則を適用して軽量化したときとしないときの差は以下のとおりです。

normal CA/DDD light weight CA/DDD
DIP, 分離境界 インターフェース
import import 文のみ import type も使う
クラス名の抽象度 ❌ 抽象的で分かりにくい ✅ 具体的で分かりやすい
全体のファイルの量 ❌ 多め 普通
変更されるファイルの量 ✅ 少い ❌ 多め
変更コスト 同じ 同じ
ライブラリの知識 ❌ 使えない ✅ 使える
変更の難しさ ❌ メタレベルが異なる高度な編集が必要な場合がある ✅ 容易。パターンで編集でき検証もしやすい
二重管理 ❌ 発生 ✅ なし
アプリのオブジェクト ✅ DI ライブラリで自動構築 ❌ 手動で Composition Root を書く, 容易
構成の表示 ❌ DI ツールによる コードを開く
ユニット テスト ❌ 注入メソッド呼び出し ❌ 生成コードを書く, ✅ 容易
DI用インターフェース ❌ 言語により必須 ✅ 不要
コードのブラウジング ❌ コードの理解が必要 ✅ シンプル

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?