ヨンソクのブログ
戻る
8 min read
Decoratorで織りなすコード

ある暑い日、新人開発者の悩み

ある暑い日、図形の国にて。

図形の国の開発者「三角(セモ)」は4歳の時に「パパ ママ」より先に「hello world」と叫んだ天才開発者である。
三角は成長して図形の国の中核開発者となり、一気呵成にコードを暗算のように書き上げる開発者として名を馳せた。

三角の唯一の仕事は「図形福祉部」所属として、図形の国の国民の面積を計算するロジックを開発することである。

厳格な三角にはしばらく後輩がいなかったが、「四角(ネモ)」という後輩が入ってきた。
新しく入った四角の課題は、三角のロジックを把握し拡張することであった。

四角は三角のコードを見て驚いた。

class 正多角形 {
  constructor(private 辺の数: number, private 辺の長さ: number) {
    if (辺の数 < 3) {
      throw new Error("図形の国の国民には最低3つの辺が必要です。");
    }
  }

  面積計算(): number {
    const 中心角 = this.中心角_求める();
    const 三角形の高さ = this.三角形の高さ_計算(中心角);
    const 三角形の面積 = this.三角形の面積_計算(三角形の高さ);
    const 多角形の面積 = this.多角形の面積_計算(三角形の面積);
    return 多角形の面積;
  }

  private 中心角_求める(): number {
    return (2 * Math.PI) / this.辺の数;
  }

  private 三角形の高さ_計算(中心角: number): number {
    const 半辺の長さ = this.辺の長さ / 2;
    return 半辺の長さ / Math.tan(中心角 / 2);
  }

  private 三角形の面積_計算(高さ: number): number {
    return (this.辺の長さ * 高さ) / 2;
  }

  private 多角形の面積_計算(三角形の面積: number): number {
    return this.辺の数 * 三角形の面積;
  }
}

// 使用例:
const 五角形 = new 正多角形(5, 10);
console.log("Area:", 五角形.面積計算());

何やら複数の過程を経て面積を計算するロジックのようだが、四角には一度で理解するのが難しかった。 そこで四角は過程を知るために console.log を入れてみることにした。

class 正多角形 {
  constructor(private 辺の数: number, private 辺の長さ: number) {
    if (辺の数 < 3) {
      throw new Error("図形の国の国民には最低3つの辺が必要です。");
    }
  }

  面積計算(): number {
    const 中心角 = this.中心角_求める();
    console.log("中心角:", 中心角);
    const 三角形の高さ = this.三角形の高さ_計算(中心角);
    console.log("三角形の高さ:", 三角形の高さ);
    // ...
  }
}

その瞬間、後ろから三角の視線が感じられた。厳格な三角は自分のコードに手を入れることを許さなかった。

四角はなんとか三角のコードに触れずに、過程を解明することにした。

アイデアを思い出してみることにした。まず以前読んだ記事が思い浮かんだ。

Proxy와 Reflect 어디다 쓰나? | 장용석 블로그

Proxy와 Reflect는 ES6에서 추가된 기능이다. Proxy는 객체의 기본 동작을 가로채는 기능을 제공하고, Reflect는 객체의 기본 동작을 대신하는 메서드를 제공한다. 예시를 통해 알아보자.

https://yongseok.me/blog/proxy_reflect_case
Proxy와 Reflect 어디다 쓰나? | 장용석 블로그

Proxyのようにオブジェクトをラップする方法を使えば、ロジックを途中に挟み込むことができそうだった。
同様の概念で、Classで使える方法はないだろうか?

かつてPython開発をしていた記憶が蘇った。
Pythonでは @ デコレータを使って関数をラップする方法があった。 関数を直接触らずに、関数の呼び出し前後にロジックを追加できたのである。

def logger(func):
  def wrapper(*args, **kwargs):
    print(f"関数 {func.__name__} が呼び出されました。")
    return func(*args, **kwargs)
  return wrapper

@logger
def func():
  print("hello world")

# 使用例:
func()
# 出力: 関数 func が呼び出されました。
# 出力: hello world

それなら、JavaScriptにもデコレータがあれば、三角のコードに触れずにロジックを追加できるはずだ。

そこで四角はデコレータを探してみることにした。

Decorator

GitHub - tc39/proposal-decorators: Decorators for ES6 classes

Decorators for ES6 classes. Contribute to tc39/proposal-decorators development by creating an account on GitHub.

https://github.com/tc39/proposal-decorators
GitHub - tc39/proposal-decorators: Decorators for ES6 classes

decoratorはまだECMAScript標準には含まれていないが、stage 3まで進行している。 すでにTypeScript(5.0以上)ではdecoratorを使用できるため、TypeScriptを使ってみることにした。

Documentation - Decorators

TypeScript Decorators overview

https://www.typescriptlang.org/ko/docs/handbook/decorators.html

デコレータを使って三角のコードに触れずにロジックを追加する

四角はドキュメントをより深く読み込む前に、まず簡単に作ってみることにした。

function log(value: any, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name);

  return function(this: any, ...args: any[]) {
    console.group(`[${methodName}]`);
    console.log('==> 入力:', args);
    const result = value.call(this, ...args);
    console.log("<== 出力:", result);
    console.groupEnd();
    return result;
  };
}

簡単に log デコレータを作成した。
このデコレータはメソッドデコレータであり、メソッドの呼び出し前後に console.group を通じてログを出力する。

では三角のコードにデコレータを適用してみよう。

class 正多角形 {
  constructor(private 辺の数: number, private 辺の長さ: number) {
    if (辺の数 < 3) {
      throw new Error("図形の国の国民には最低3つの辺が必要です。");
    }
  }

  @log // <= デコレータ適用
  面積計算(): number {
    const 中心角 = this.中心角_求める();
    const 三角形の高さ = this.三角形の高さ_計算(中心角);
    const 三角形の面積 = this.三角形の面積_計算(三角形の高さ);
    const 多角形の面積 = this.多角形の面積_計算(三角形の面積);
    return 多角形の面積;
  }

  @log
  中心角_求める(): number {
    return (2 * Math.PI) / this.辺の数;
  }

  @log
  private 三角形の高さ_計算(中心角: number): number {
    const 半辺の長さ = this.辺の長さ / 2;
    return 半辺の長さ / Math.tan(中心角 / 2);
  }

  @log
  private 三角形の面積_計算(高さ: number): number {
    return (this.辺の長さ * 高さ) / 2;
  }

  @log
  private 多角形の面積_計算(三角形の面積: number): number {
    return this.辺の数 * 三角形の面積;
  }
}

// 使用例:
const 五角形 = new 正多角形(5, 10);
console.log("Area:", 五角形.面積計算());

すべてのメソッドに @log デコレータを適用した。
そしてコードを実行してみた。

|[面積計算]
|==> 入力: []
  |[中心角_求める]
  |==> 入力: []
  |<== 出力: 1.25

  |[三角形の高さ_計算]
  |==> 入力: [1.25]
  |<== 出力: 6.88

  |[三角形の面積_計算]
  |==> 入力: [6.88]
  |<== 出力: 34.40

  |[多角形の面積_計算]
  |==> 入力: [34.40]
  |<== 出力: 172.00
|<== 出力: 172
Area: 172.00

おお!ロジックに手を加えることなく、四角が望んでいたログを出力できるようになった。

単純にメソッド内部に console.log を入れることでも解決できたが、三角の反発があった。
そのため遠回りして Decorator を使って問題を解決することになった。

三角の反発がなかったとしても、四角自身もコードに直接ログを入れることにはどこか違和感を感じていた。
その理由は何だろうか。じっくり考えてみた。

多角形面積ドメイン

既存のロジックは上図のように、多角形の面積計算に関するドメインを持っている。
内部メソッドはそれぞれの役割を持ち、これらを通じて多角形の面積を計算する。

ログを追加した様子

しかし、メソッド内部にログを追加すると、既存のドメインロジックとログが混在してしまう。
異なる目的のロジックが結合されることで、単なる「面積計算」ではなく「面積計算withLogging」という新たなメソッドになってしまう。

このような違和感が生じるのは、ロギングロジックが多角形の面積計算とは別の役割を持つ**横断的関心事(Cross-cutting Concern)**だからである。 縦方向に伸びるドメインロジックとは無関係に、適用されるべきロジックなのだ。
今回の場合はその対象が Class であり、実装のために Decorator を使用した。

織り込まれるロジック

ロギングは縦の線に触れることなく、横方向に織り込まれた。他のロジックも同様に横方向に織り込むことができる。
この過程は一種の**織り(weaving)**と言えるだろう。

織り weaving

構造的にも既存のロジックを侵さないため、認知的な負担も少ない。

このような方法論は AOP(Aspect Oriented Programming、アスペクト指向プログラミング) の実装方法の一つである。

アスペクト指向プログラミング

Aspect-Oriented Programming

Gregor Kiczales, John Lamping, Anurag Mendhekar, Chris Maeda, Cristina Videira Lopes, Jean-Marc Loingtier, John Irwin

https://www.cs.ubc.ca/~gregor/papers/kiczales-ECOOP1997-AOP.pdf

AOPについて検索すると、多くの場合 Spring のような例が出てくるだろう。
そのためアスペクト指向プログラミングはOOPに限定されたもののように見えるが、論文でも明記されているように独立した概念として捉えることができる。

ここで**アスペクト(Aspect)の核心は、「何を」するかと「どこに」**適用するかを分離することである。
一種の関心事の分離を通じて、結合度を下げ、再利用性を高めるのである。

似た目的を持つDependency Injectionが思い浮かぶかもしれない。
DIは主にオブジェクト間の関係と生成に焦点を当てるのに対し、AOPはオブジェクトの振る舞いに焦点を当てる。
オブジェクト間の関係の結合度よりも、振る舞いというアクションから付随的なものを分離して結合度を下げるのである。
混同する要素はないが、一緒に想起されうる概念なので参考にすると良いだろう。

デコレータとは?

**「Decorate」という単語は「装飾する」**という意味を持つ。
**「装飾」**するということは、既存のものを飾るという意味がある。 その意味で、プログラミングにおける「decorate」もまた、既存のものを飾ることを意味する。

既存のコードを修正せずに、機能を追加または変更することだと理解すればよい。

デコレータパターン

おそらくその由来はデザインパターンから来ているのではないだろうか。
デザインパターンの名著として知られる1994年刊行の**‘Design Patterns: Elements of Reusable Object-Oriented Software’において、Structural Patternsの一部としてDecorator Pattern**が紹介されている。

Structural Patterns(構造パターン)とは、クラスとオブジェクトがどのように構成されてより大きな構造を形成するかに関心を持つパターンである。
その中でデコレータパターンは、オブジェクトに動的に責任を追加するパターンとして紹介されている。

責任を追加する方法としては継承が思い浮かぶかもしれない。

例を挙げてみよう。
スターバックスでコーヒーを注文したとしよう。
普段はブラックコーヒーを注文して飲んでいたが、今日は特別にシロップを追加して飲みたい。

class アメリカーノ:
  def 説明(self):
    return "アメリカーノ"

私のコーヒー = アメリカーノ()
私のコーヒー.説明() # アメリカーノ

アメリカーノクラスを継承したシロップ入りアメリカーノクラスを作ることで、シロップ入りのアメリカーノを注文できるだろう。

class シロップ入りアメリカーノ(アメリカーノ):
  def 説明(self):
    return "シロップ入りアメリカーノ"


私のコーヒー = シロップ入りアメリカーノ()
私のコーヒー.説明() # シロップ入りアメリカーノ

しかし、メニューに シロップ入りアメリカーノ を追加するわけにはいかない。
我々はただ、受け取った アメリカーノ のインスタンスにシロップを追加したいだけなのだ。

継承は静的である。コンパイル時に決定され、実行時に変更できない。

こういう時にデコレータパターンを使うことができる。

class アメリカーノ:
  def 説明(self):
    return "アメリカーノ"

class コーヒーデコレータ:
  def __init__(self, コーヒー):
    self.コーヒー = コーヒー

  def 説明(self):
    return self.コーヒー.説明()

class シロップ追加デコレータ(コーヒーデコレータ):
  def 説明(self):
    return self.コーヒー.説明() + "にシロップ追加"

class ホイップクリーム追加デコレータ(コーヒーデコレータ):
  def 説明(self):
    return self.コーヒー.説明() + "にホイップクリーム追加"

私のコーヒー = アメリカーノ()
私のコーヒー = シロップ追加デコレータ(私のコーヒー) # 動的にシロップを追加

print(私のコーヒー.説明()) # アメリカーノにシロップ追加

友達のコーヒー = ホイップクリーム追加デコレータ(シロップ追加デコレータ(アメリカーノ()))

元のコーヒーと同じインターフェースを持ちながら、動的に責任を追加できる。

少し難しい言い方だが、補足すると**「デコレータはオブジェクトを再帰的に構成することで、開放的な追加責任を許容する構造パターン」と書籍で説明されている。
一種の
Wrapper**とも見ることができるだろう。

デコレータとデコレータパターン

デコレータデコレータパターンは、言語レベルの構文とデザインパターンであり、直接的に対等な概念として見るのは難しい。

What is the difference between Python decorators and the decorator pattern?

What is the difference between &#x201C;Python decorators&#x201D; and the &#x201C;decorator pattern&#x201D;?&#xA;&#xA;When should I use Python decorators, and when should I use the decorator pattern?&#xA;&#xA;I&#x27;m looking for examples of Python

https://stackoverflow.com/questions/8328824/what-is-the-difference-between-python-decorators-and-the-decorator-pattern
What is the difference between Python decorators and the decorator pattern?

書籍で説明されたデコレータパターンが克服しようとした問題を考えると、主に静的型付け言語で発生する問題を動的なタイミングで解決できるようにするパターンだと見ることができる。

他の言語でのデコレータのような要素を見てみよう。

Python

まずPythonのデコレータについて見ていこう。
デコレータに関する言及の中で、PEP 318に記載された内容が最も気に入っている。一緒に少し読んでみよう。

PEP 318 – Decorators for Functions and Methods | peps.python.org

The current method for transforming functions and methods (for instance, declaring them as a class or static method) is awkward and can lead to code that is difficult to understand. Ideally, these transformations should be made at the same point in the...

https://peps.python.org/pep-0318/
PEP 318 – Decorators for Functions and Methods | peps.python.org

これは言語レベルでの新しい構文に関する提案であるが、コードを書く実装レベルにおいても参考になる概念的な内容を含んでいる点が気に入った。
ここで「構文」というキーワードを「コード」あるいは「実装」に置き換えて読んでみても良いだろう。

良い設計は領域を超えて貫通する概念だと考える。

脱線してしまったが、PEPに言及されている内容を参考にすると、デコレータは関数やメソッドを変形するツールであり、宣言に近い場所に配置することでコードの可読性を高めることが目的である。
コードを読む人に対して、より宣言的にコードを表現することで凝集度を高め、結合度を下げるのである。

それでは、我々のJavaScriptデコレータはどのような目的を持っているのだろうか?

JavaScript

GitHub - tc39/proposal-decorators: Decorators for ES6 classes

Decorators for ES6 classes. Contribute to tc39/proposal-decorators development by creating an account on GitHub.

https://github.com/tc39/proposal-decorators?tab=readme-ov-file
GitHub - tc39/proposal-decorators: Decorators for ES6 classes

TC39 Proposalで見られるように、JavaScriptでは従来、関数に対してデコレータパターンを適用することは簡単にラッピングして使用できた。
ドキュメント内の例のように、このように書くことができた。

const foo = bar(baz(qux(() => /* 素晴らしいことをしよう */)))

しかし、クラスやオブジェクトに対してはこれを適用するのが困難であった。これを解決するためにDecorator構文が提案された。

では、デコレータはJavaScriptにおいて本質的にどのような意味を持つのだろうか?

まず、簡単なメソッドデコレータを例に見てみよう。

function logged(value, { kind, name }) {
  if (kind === "method") {
    return function (...args) {
      console.log(`starting ${name} with arguments ${args.join(", ")}`);
      const ret = value.call(this, ...args);
      console.log(`ending ${name}`);
      return ret;
    };
  }
}

class C {
  @logged
  m(arg) {}
}

new C().m(1);
// starting m with arguments 1
// ending m

まず対象であるClassから分解してみよう。JavaScript(ES6)ではメソッドは以下のようにも表現できるだろう。

function C() {}

C.prototype.m = function(arg) {};

new C().m(1);

であれば、先ほど見た関数でのデコレータパターンの実装と同様に、メソッドにもデコレータパターンを適用できるはずである。
デコレータをdesugarすると、おおよそ以下のように表現できるだろう。

class C {
  m(arg) {}
}

C.prototype.m = logged(C.prototype.m, {
  kind: "method",
  name: "m",
  static: false,
  private: false,
}) ?? C.prototype.m;

現状

The TC39 Process

No description available

https://tc39.es/process-document/

現在、JavaScriptデコレータはStage 3の段階に入っている。Stage 3はほぼ最終仕様に近い段階と言える。
しかし、まだ正式な構文として取り入れられていないため、BabelやTypeScriptのトランスパイラに依存して使用する必要がある。

Babelの場合は以下のようなプラグインが提供されている。 https://babeljs.io/docs/babel-plugin-proposal-decorators

TypeScriptの場合はどうだろうか?

TypeScriptはかなり以前からデコレータをサポートしていた。馴染みは薄いかもしれないが聞いたことがあるであろうオプションとして、tsconfigで "experimentalDecorators": trueオプションを通じてデコレータを使用できた。

differences-with-experimental-legacy-decorators

Today we&#8217;re excited to announce the release of TypeScript 5.0! This release brings many new features, while aiming to make TypeScript smaller, simpler, and faster. We&#8217;ve implemented the new decorators standard, added functionality to better support ESM projects in Node and bundlers, provided new ways for library authors to control generic inference, expanded our JSDoc [&hellip;]

https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#differences-with-experimental-legacy-decorators
differences-with-experimental-legacy-decorators

このオプションはStage 2段階のデコレータを使用可能にするオプションである。
TypeScript 5.0からはStage 3段階のデコレータが使用可能になったため、experimentalDecoratorsオプションはレガシー用として残ることになった。

Announcing TypeScript 5.0 - TypeScript

Today we&#8217;re excited to announce the release of TypeScript 5.0! This release brings many new features, while aiming to make TypeScript smaller, simpler, and faster. We&#8217;ve implemented the new decorators standard, added functionality to better support ESM projects in Node and bundlers, provided new ways for library authors to control generic inference, expanded our JSDoc [&hellip;]

https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#decorators
Announcing TypeScript 5.0 - TypeScript

正式な構文として取り入れられれば、ターゲットが最新の場合はエンジンで直接実行できるようになるだろう。
V8側でも以下のような作業が進行中のようである。

Implement the decorators proposal

The decorators proposal (https://github.com/tc39/proposal-decorators) reached Stage 3 at the March 2022 TC39.

https://issues.chromium.org/issues/42202709

https://chromium-review.googlesource.com/q/hashtag:%22decorators%22

Gerrit Code Review

https://chromium-review.googlesource.com/q/hashtag:%22decorators%22

メタデータ

デコレータは自身がデコレートする対象に対して最善を尽くして責任を果たす。
しかし、デコレータをより効果的に活用するためには、対象となるクラスやメソッドに関する追加情報が必要になる場合がある。
例えば、デコレータを通じてクラスを装飾しつつ、他のデコレータの存在を確認したり、 クラス全体に関する情報を活用したい場合にメタデータが有用となる。

現在のJavaScriptのStage 3デコレータでは、自身が装飾する対象に関する情報に限定的にしかアクセスできない。
デコレータは基本的に、自身が装飾するメソッド、フィールド、またはクラスに関する情報を含むcontextオブジェクトを受け取る。
このcontextはデコレータが装飾する対象に関する情報のみを提供し、クラス自体や他のフィールドに関する情報にはアクセスできない。

Stage 2 vs Stage 3 メタデータアクセス方式

過去のStage 2デコレータでは、targetを通じてクラスプロトタイプ全体にアクセスすることが可能であった。
これにより、クラスレベルでメタデータを保存し、それを活用する方式でデコレータを構成することができた。
例えば、特定のクラスをデコレータで装飾する際、そのクラス自体をキーとするWeakMapを使用して関連メタデータを保存・参照することが可能であった。

const metadataMap = new WeakMap();

function myDecorator(target, key, descriptor) {
  // クラスプロトタイプにアクセス可能
  const metadata = metadataMap.get(target) || {};
  metadata[key] = { someMetadata: 'value' };
  metadataMap.set(target, metadata);

  return descriptor;
}

class MyClass {
  @myDecorator
  myMethod() {}
}

上記の例では、targetを使用してクラスのプロトタイプにアクセスし、 そのクラスに関するメタデータをmetadataMapに保存して再利用することができた。

Stage 3では以下のように変更された。

function someDecorator(originalMethod:any, context: ClassMethodDecoratorContext){
  const methodName = String(context.name);
  // ...
}

targetが廃止され、デコレータが装飾する対象に関する情報のみを持つようになった。

このようになった理由はproposalで確認できる。

デコレータがクラス自体に変更を加えると、クラスの振る舞いの予測が困難になる。
これを避けるため、デコレータはデコレートする対象に関する情報のみを持つようになった。

では、デコレータがクラス自体に関する情報を取得する必要がある場合はどうすればよいのだろうか?

理屈だけで理解するのでは不十分である。具体的なケースとともに見ていこう。
デコレータが他の情報を知る必要がある場合とはどのような場合だろうか?

依存性注入(Dependency Injection)とメタデータ

メタデータは依存性注入(Dependency Injection)パターンを実装する上でも重要な役割を果たす。
依存性注入とは、クラスが他のクラスに依存する際、クラス内部で依存性を生成するのではなく外部から注入を受ける方式である。
JavaScript領域で依存性注入を実装するためにデコレータを使用するケースを見てみよう。

まず思い浮かべられるのはNestJS依存性注入である。

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).

https://docs.nestjs.com/providers#dependency-injection
Documentation | NestJS - A progressive Node.js framework

ドキュメントにも記載されているように、Nestは依存性注入を基盤として構築されたフレームワークである。
以下のようなケースで使用される。
簡単な例として、NestJSを始めた際に最初に目にするサンプルコードを見てみよう。

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}

// app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}


// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

構造を見てみよう。

このコードだけを見ると、AppControllerAppServiceを使用していることは分かるが、
AppServiceがどのように注入されて使用されているかは分からない。

我々が把握できる手がかりとしては、注入される対象であるAppService@Injectable()デコレータを使用していることと、
AppModuleprovidersAppServiceを登録していることである。

@Injectable()@Controllerデコレータや、@Moduleデコレータが魔法を生み出しているのだろうか?

export function Injectable(options?: InjectableOptions): ClassDecorator {
  return (target: object) => {
    Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
  };
}

export function Module(metadata: ModuleMetadata): ClassDecorator {
  const propsKeys = Object.keys(metadata);
  validateModuleKeys(propsKeys);

  return (target: Function) => {
    for (const property in metadata) {
      if (metadata.hasOwnProperty(property)) {
        Reflect.defineMetadata(property, (metadata as any)[property], target);
      }
    }
  };
}

実際にコードを見てみると、わずか数行にすぎない。魔法のようなことは起きておらず、何かを設定しているだけである。
確かにNestJSを実行すると、どこかでAppServiceを生成しAppControllerに注入しているはずである。
しかし実質的な結合の仕組みは、表面からは把握できなかった。ただモジュールで登録する際にAppServiceを登録しただけである。

表面的には直接注入していないということは、「情報」だけを通じて誰かが注入しているということである。
ここで我々は@Injectableデコレータを通じて注入可能であるという「情報」を表示し、@Moduleデコレータを通じてこの「情報」を伝達した。
これによりNestJSは内部的にAppServiceを生成しAppControllerに注入したのである。

ここで重要なのは、「情報」の媒介としてメタデータが使用されたということである。

Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);

メタデータは一種の「情報の情報」と見なすことができる。

ここではReflect metadataが使用されているが、これはStage 3以前に使用されていた方式である。

Just a moment...

No description available

https://www.npmjs.com/package/reflect-metadata

メタデータもデコレータとは別に、独自のproposalが進行中である。
現在Stage 3として進行中であり、TypeScriptでは5.2バージョンからStage 3に合わせてメタデータをサポートしている。

Documentation - TypeScript 5.2

Decorator Metadata

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#decorator-metadata

GitHub - tc39/proposal-decorator-metadata

Contribute to tc39/proposal-decorator-metadata development by creating an account on GitHub.

https://github.com/tc39/proposal-decorator-metadata
GitHub - tc39/proposal-decorator-metadata

Stage 3 メタデータ

Stage 3ではどのように変わったのか、依存性注入の例を改めて作ってみよう。

まずInjectableデコレータを作ってみよう。

(Symbol as any).metadata ??= Symbol('Symbol.metadata');

const METADATA_KEY = Symbol('DI:METADATA');

function Injectable() {
  return function (_: unknown, context: ClassDecoratorContext) {
    const metadata = (context.metadata[METADATA_KEY] as any) ?? { injectable: true };
    metadata.injectable = true;
    context.metadata[METADATA_KEY] = metadata;
  };
}

Injectableデコレータは、該当クラスのメタデータにinjectableキーを追加し、trueに設定する。
注入可能なクラスであることを表示するものである。

次にInjectデコレータを作ってみよう。

function Inject(dependencyName: string) {
  return function (_: unknown, context: ClassFieldDecoratorContext) {
    const metadata = (context.metadata[METADATA_KEY] as any) ?? { dependencies: {} };
    metadata.dependencies = metadata.dependencies ?? {};
    metadata.dependencies[context.name] = dependencyName;
    context.metadata[METADATA_KEY] = metadata;
  };
}

InjectデコレータはdependencyNameを受け取り、該当クラスのメタデータにdependenciesキーを追加し、そのクラスのフィールド名をキーとしてdependencyNameを保存する。
これにより、そのクラスがどのような依存性を持っているかを表示できる。

それぞれの表示子が準備できたので、中央で管理するContainerクラスを作ってみよう。

class Container {
  private services: Map<string, any>;

  constructor() {
    this.services = new Map();
  }

  register(name: string, ServiceClass: any): void {
    console.log(`Registering service ${name}`);
    this.services.set(name, ServiceClass);
  }

  resolve(name: string): any {
    console.log(`Resolving service ${name}`);
    const ServiceClass = this.services.get(name);
    if (!ServiceClass) {
      throw new Error(`Service ${name} not found`);
    }

    const metadata = (ServiceClass as any)[Symbol.metadata]?.[METADATA_KEY];
    if (!metadata || !metadata.injectable) {
      throw new Error(`Service ${name} is not injectable`);
    }

    const instance = new ServiceClass();

    if (metadata.dependencies) {
      for (const [propertyKey, dependencyName] of Object.entries(metadata.dependencies)) {
        instance[propertyKey] = this.resolve(dependencyName as string);
      }
    }

    return instance;
  }
}

Containerクラスはregisterメソッドを通じてサービスを登録し、resolveメソッドを通じてサービスを取得する。
nameで入力されたサービスをresolveする過程で、メタデータを通じて該当サービスの依存性情報を取得し、再帰的に依存性を解決する。

// ...
const instance = new ServiceClass(); // ServiceClassのインスタンスを生成

if (metadata.dependencies) { // 依存性がある場合
  for (const [propertyKey, dependencyName] of Object.entries(metadata.dependencies)) {
    instance[propertyKey] = this.resolve(dependencyName as string); // 依存性を解決
  }
}
return instance;

実際に使用する部分を作ってみよう。

@Injectable()
class ProductService {
  constructor() {
    console.log('ProductService created');
  }

  getName() {
    return 'Product Service';
  }
}

@Injectable()
class PriceService {
  constructor() {
    console.log('PriceService created');
  }
  getPrice() {
    return 100;
  }
}

@Injectable()
class ShopService {
  @Inject('productService')
  productService!: ProductService;

  @Inject('priceService')
  priceService!: PriceService;

  listProductsWithPrices() {
    return `${this.productService.getName()} costs ${this.priceService.getPrice()}`;
  }
}

const container = new Container();

container.register('productService', ProductService);
container.register('priceService', PriceService);
container.register('shopService', ShopService);

const shopService = container.resolve('shopService');

console.log(shopService instanceof ShopService); // true
console.log(shopService.productService instanceof ProductService); // true
console.log(shopService.priceService instanceof PriceService); // true
console.log(shopService.listProductsWithPrices()); // Product Service costs 100

3つのクラスを作成し、すべてContainerに登録した後、ShopServiceをresolveして使用してみた。
この過程で直接的に依存性を注入していないにもかかわらず、Containerを通じて依存性の注入を受けて使用することができた。

このように構造を作ると、ShopServiceはProductServiceとPriceServiceに依存しているが、どのように生成され注入されるかといった具体的な実装には依存しなくなる。
結合度を下げたのである。

我々はこのように結合度を下げる手段として、中間に第三者であるContainerと、情報をやり取りする媒介としてのメタデータを使用した。
「情報の情報」という観点から、メタデータが見事にその役割を果たしてくれた。

魔法のようなものは存在しない。
質量・エネルギー保存則は我々のコードにも同様に適用されると考える。(真剣に物理的な意味ではない。誤解なきよう)
我々が結合度を下げるために何かを切り離す行為は、まるで強い力で結合している陽子・中性子を引き離すことに似ている。
核分裂の過程でウラン235に中性子を衝突させて原子核を分裂させる。その過程で質量欠損により結合エネルギーが放出され、エネルギーに変換される。
中性子を衝突させる過程は、我々が時間とコストを投資し、様々な手法(上記のメタデータのような)を用いて結合度を下げることに相当する。

結合度を下げるために一定のエネルギー(コスト)が必要であるが、その結果として放出されて我々に返ってくるエネルギーはより大きいはずである。

Nuclear Fission Diagram.
出典:https://www.automated-teaching-machines.com/Intermediate-Science/310-Nuclear-Fission-and-Fusion.php


P.S ただし、過ぎたるは猶及ばざるが如し。規模に見合わない過度な結合度の分離や、目的と行為の主客が転倒した結合度の分離は逆効果を生みかねない。
制御されない(核)分裂が歴史的にどのような結果をもたらしたか、考えてみよう。

Reference

https://ko.javascript.info/class#ref-285 https://www.cs.ubc.ca/~gregor/papers/kiczales-ECOOP1997-AOP.pdf https://en.wikipedia.org/wiki/Aspect-oriented_programming

https://peps.python.org/pep-0318/ https://github.com/tc39/proposal-decorators?tab=readme-ov-file

https://docs.google.com/document/d/1GMp938qlmJlGkBZp6AerL-ewL1MWUDU8QzHBiNvs3MM/edit https://springframework.guru/gang-of-four-design-patterns/

https://groups.google.com/g/v8-reviews/c/inK1X-XQQPg https://chromium-review.googlesource.com/q/hashtag:%22decorators%22+(status:open%20OR%20status:merged) https://chromium-review.googlesource.com/c/v8/v8/+/5837979 https://chromium-review.googlesource.com/c/v8/v8/+/5837979/4/src/interpreter/bytecode-generator.cc#3154

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#decorator-metadata https://www.typescriptlang.org/docs/handbook/decorators.html https://www.typescriptlang.org/tsconfig/#experimentalDecorators https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#decorators https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#differences-with-experimental-legacy-decorators

https://2ality.com/2022/10/javascript-decorators.html https://medium.com/javascript-scene/javascript-factory-functions-vs-constructor-functions-vs-classes-2f22ceddf33e