🌪️

サーバーサイドTypeScriptを関数で書く理由

2024/10/04に公開
3

この記事は?

著者はNode環境でのサーバーサイドTypeScriptを専門としているエンジニアです。この記事では、サーバーサイドTypeScriptをClassではなく関数的に書く 理由について書きます。

比較対象

テスタブルなコードを書くための手法に、DI(Dependency Injection) があります。ClassでDIをする場合と比べ、関数でDIをする場合にどのような感じになるか?をみてみましょう。

Repository/ Application Service/ Use Caseの3つの層で見てみましょう。

関数で書くと?

注釈) 関数を引数に受け取ったり、関数を返す関数のことを高階関数(HOF) という。
・Repository層

const createRepository1 = () => {
    return {
        someHOfunc: (arg1, arg2, ..., argN) => {},
        someHOfunc2: (arg1, arg2, ..., argN) => {}
        // ...と他にも色々なHOCFuncが作られる
};       
 
// repository2
// repository3
// repository4...と他にも色々なrepositoryが作られる

・Application Service層

const createService1 = (arg1, arg2, ..., argN) => {
    return {
        someHOfunc: (arg1, arg2, ..., argN) => {},
        someHOfunc2: (arg1, arg2, ..., argN) => {}
        // ...と他にも色々なHOCFuncが作られる
    };
};

// service2
// service3
// service4...と他にも色々なserviceが作られる

・UseCase層

const executeUseCase = (service1: ReturnType<typeof createService1>) => {
    service1.someHOfunc(arg1, arg2, ..., argN);
    service1.someHOfunc2(arg1, arg2, ..., argN);
    // 他のメソッドを必要に応じて呼び出す
};

const service1 = createService1(arg1, arg2, arg3, ...argN);
executeUseCase(service1);

このように、TypeScriptを関数で書くと高階関数を用いて依存関係を解決できることがわかります。特に何もライブラリーなども必要ないので、比較的軽量であると言えます。

Classで書くと?

Classで書くと上記のサービス層は以下のようになります。


class Service {
   
constructor(
    @Inject(DependencyA) private depA: DependencyA,
    @Inject(DependencyB) private depB: DependencyB
) {}

    someMethod(arg1, arg2, ...argN) {}
    someMethod2(arg1, arg2, ...argN) {}
}

classで書く場合は、constructorで引数をセットすることでそのService Class内で使えるようになります。ただし、上記で注入しようとしているdepA, depBがさらに他のClassと依存関係を持っている場合、それらの依存関係も解決する必要があるので複雑さが増します。そこで解決策として登場するのがいわゆるDIコンテナーで、TypeScriptの場合 tsryngeなどを使用することができます。
ただしDIコンテナーも万能ではなく、開発に応じてどんどんDIコンテナーが管理する依存関係は肥大化していき、設定を間違えると実行時エラーとなる点には留意が必要です。

Classを使っているが故の複雑さ

DIで開発する場合、Classには継承があるためClass同士での依存関係が生じるため、引数を渡すにはconstructorで受け取る必要があり、それらの依存関係も解決される必要があります。また、それを管理するツールであるDIコンテナーにもデメリットがあります。こういった諸種の制約をクリアーにしてくれる関数でのサーバーサイドTypeScriptを本記事では紹介しました。

著者はサーバーサイドの開発では小規模〜大規模のプロジェクトまでClassを使って開発をすることが多かったのですが、ことサーバーサイドTypeScriptの開発においては経験を経るにつれ上記のような制約を感じることが増え、関数型で書く恩恵を重視して感じるようになりました。

Discussion

dog_cat_foxdog_cat_fox

Class の形と同等に書くなら service1 を関数の引数として下記のようにはならないでしょうか。
こうすれば DI の利点であるモックの容易さを関数にも持たせられるかと思います。

const executeUseCase = (service1: Service1) => {
    service1.someHOFunc(arg1, arg2, ..., argN);
    service1.someHOFunc2(arg1, arg2, ..., argN);
    // 他のメソッドを必要に応じて呼び出す
};
muratakmuratak
const executeUseCase = (service1: ReturnType<typeof createService1>) => {
    service1.someHOfunc(arg1, arg2, ..., argN);
    service1.someHOfunc2(arg1, arg2, ..., argN);
    // 他のメソッドを必要に応じて呼び出す
};

としておいて、

const service1 = createService1(arg1, arg2, ..., argN);
executeUseCase(service1);

とすることで実現可能ですね。その方がusecaseのテストもしやすく良さそうです!

ojisanojisan

こちら取り入れさせていただき本文に反映させていただきました🙇‍♀️