機能依存性注入


機能について話しましょう!私たちは皆、右の関数が好きですか?まあ、少なくとも私は、私は毎日何十もの書く!私は最後の1 , 5年の私の日仕事のTypesScriptプログラマーでした、そして、私が仕事を見た若干の実行と他のものを共有したいです..まあ吸う.
今日はバックエンドアプリケーションのグローバル設定の問題についてお話します.

グローバル設定


通常、グローバル設定は通常のオブジェクトとして表されます.グローバル設定と言うと、ポート、ホスト、またシステム接続のような定数を意味することができます.
type Config = {
  // configuration constants
  port: number;
  host: string;
  // so called "dependencies"
  db: { getUserByUserName: (userName: string) => Promise<User> };
  logger: { info: (s: string) => void };
  // state variables
  currentUserName: Option<string>;
};

const config: Config = {
  port: process.env.PORT ?? 3000,
  host: process.env.HOST ?? "localhost",
  db: { getUserByUserName: () => Promise.resolve({ name: "some user" }) },
  logger: { info: console.log },
  currentUserName: O.some("some user"),
};
グローバルな依存関係を消費する最も一般的で不幸な方法は、次のコードスニペットの行に沿った何かです.
const fetchUserNaive = (): Promise<Option<User>> => {
  const { db, logger, currentUserName } = config;
  return pipe(
    currentUserName,
    O.fold(
      () => Promise.resolve(O.none),
      (userName) => {
        logger.info(`fetching user with userName: ${userName}`);
        return db.getUserByUserName(userName).then(O.some);
      }
    )
  );
};

const hasAuthNaive = (): boolean => O.isSome(config.currentUserName);
なぜこれはあなたが尋ねるかもしれないので、悪いです.井戸
  • 関数の純粋性
  • 純粋な関数(それは入力パラメータに依存していて、副作用がないだけの関数)は、理由を説明して、構成するのがより簡単です.グローバルな状態を読み書きする機能は本質的に不純なものです.京大理
  • プログラム状態を予測できません:
  • グローバル設定オブジェクトが純粋でない言語でアプリケーション全体で使用されている場合、オブジェクトが変異されていないことは保証されません.これは、プログラムの振る舞いが、グローバルな設定取得から突然変化することを意味します.
  • テストのモックインは難しい
  • 異なるテストのために異なる模擬実装を提供することは、難しい/エラー傾向があります.
  • コンカレンシーの問題
  • NodeJSなどの単一スレッド環境では問題はありませんが、グローバルな状態を共有する他のどこでも、何らかの種類のロックが必要です.
  • コード理解
  • 多くの変更可能なグローバル変数に依存するコードの振る舞いは理解しにくい.
    幸いにもこれからの改善は比較的簡単です.あなたに紹介します.

    関数パラメータ


    したがって、この関数は、関数のパラメータの外側にある値に依存しないことになります.これは、関数が持つ依存関係の全てを列挙することを強制します.このスタイルに適応すると、前のコードは次のようになります.
    const fetchUserParams = (cfg: Config): Promise<Option<User>> => {
      const { db, logger, currentUserName } = cfg;
      return pipe(
        currentUserName,
        O.fold(
          () => Promise.resolve(O.none),
          (userName) => {
            logger.info(`fetching user with userName: ${userName}`);
            return db.getUserByUserName(userName).then(O.some);
          }
        )
      );
    };
    
    const hasAuthParams: (currentUserName: Option<string>) => boolean = O.isSome;
    
    ああ、これはとても良いです.でもどうして…….さて、我々は以前からの問題のリストに取得の改善の種類を参照してみましょう.
  • 関数の純粋性
  • この関数の純度はdb.getUserByUserName です.私たちは、それがデータベースを照らして、したがって、不潔であると仮定することができます.しかし、私たちは関数パラマールの値によって引き起こされた不純物を除去しました.
  • グローバル共有状態は、プログラム状態を予測不能にします.
  • 我々はもはやグローバル共有状態を使用していません.
  • テストのモックインは難しい
  • 突然容易になった.
  • コンカレンシーの問題
  • もはや問題.
  • コード理解
  • 関数の依存性をタイプシグネチャ(この場合のパラメータ)から見ることができます.

    依存関係インジェクション?


    かなり、それはバックエンド開発のための依存性注入や制御の反転の本当に基本的なアプリケーションです.特に機能的な言語やスタイルでは、通常、依存性注入については触れません.しかし、考えは同じです.実際、このようなプログラミングのパターンはずっと前から注目されている.我々は、我々の機能への構成を渡すことで抽象的でありえますReader データ型.
    const fetchUserReader: Reader<Config, Promise<Option<User>>> = pipe(
      R.ask<Config>(),
      R.map(({ currentUserName, db, logger }) =>
        pipe(
          currentUserName,
          O.fold(
            () => Promise.resolve(O.none),
            (userName) => {
              logger.info(`fetching user with userName: ${userName}`);
              return db.getUserByUserName(userName).then(O.some);
            }
          )
        )
      )
    );
    
    // This is a bit silly, I would probably leave this to the version without the reader
    const hasAuthReader: Reader<Pick<Config, "currentUserName">, boolean> = pipe(
      R.ask<Pick<Config, "currentUserName">>(),
      R.map(({ currentUserName }) => O.isSome(currentUserName))
    );
    
    あなたが考えることができる詳細に行くことなくReader<Config, string> 次の関数の抽象化として(config: Config) => string . それで、それはちょうどDisquiseの機能です!
    例題はとても簡単ですが、原則として、ここではいくつかの理由がありますReader データ型.
  • 関数パラメータを抽象化することは、すべての関数に設定オブジェクトをパラメーターとして渡す問題を避けるために有用です.
  • これは、コードを読みやすくすることができます.前の点と手に手のようになります.(Readabilityは読者の目にあります————
  • 個人的にはReader それはいくつかの人々を怖がらせることができますし、コストを正当化するために入力スクリプトのコンテキストでは、Enfinanceの利点をもたらすことができないので、クライアントのプロジェクトでモナド(複雑さ、よく再び読者の目にある).

    脚注:

  • The Option and Reader この投稿で使用したデータ型はfp-ts .
  • あなたが見たいならばReader Haskellで使用され、他の同様の言語はここに導入されたブログの投稿です ReaderT パターン.