Feature-Sliced DesignをPublic APIなしで運用する

2024/05/26に公開

概要

Feature-Sliced Designについて、現実問題としてPublic APIがネックになることがあり、本記事はより簡易的な方式を提案したものになります。

Public APIの欠点

Public APIではeslintによって全てのモジュールは外部に公開できなくなります。各segmentフォルダ下にindex.tsxのようなファイル(barrelファイル)を作成することでモジュールの公開ができるようになります。
このやり方は安全ではあるものの、開発時のパフォーマンスが悪くなりがちです。
https://feature-sliced.design/docs/reference/public-api#about-re-exports
特にVite使用時はその影響が大きいです
さらにRemixだとbarrelファイルとの相性が微妙です。コンパイラがclient bundleなのかserver bundleなのかを識別するのが難しくなります。

原則モジュールは公開

お行儀が悪いことを言うと大体のモジュールは公開で良いのではないでしょうか。隠したいモジュールがあれば、それを特別なフォルダに置けば良いのでは? これはフェイルセーフの観点から言うとまずいですが、10人くらいまでの少人数のプロジェクトであればそこまで悪くない判断だと思います。要は運用でカバーするパターンです。
私の今の設定ではsegmentsにprotectedというものを置いて、レイヤー内部だけにしか公開しないようにしています。

ESLintを自分たちで管理する

私見ですが、Feature-Sliced Designの思想についてはおそらく多くの人が肯定的です。ですが、細部を見ていくと納得できる人は少なくなりそうです。layersやsegmentsの命名に疑問を持ったり、パフォーマンスの影響等の問題、segmentsまで分ける必要があるのか等々。なので、そのあたりについては自分たちでESLintを管理してみてはいかがでしょうか?管理する手間はかかりますが、自分たちの現実に即したルールをきちんと制定できるようになります。
雑な例で申し訳ないですが、下記に私のRemixプロジェクト設定を追加しました。基本的には、import/no-restricted-pathsによってimportを邪魔するイメージです。sharedは例外的にslicedがないので、ルールを変えています。また、widgetsよりもlayoutsと呼んだほうがイメージがつきやすいのでそこも変えています。Remixなのでpagesはなく、routesでの管理になります。

設定ファイル
eslint-rules/fsd.cjs
const FS_LAYERS = [
  "app",
  "pages",
  "routes",
  "layouts",
  "features",
  "entities",
  "shared",
];

const FS_LAYERS_WITHOUT_APP = FS_LAYERS.filter((val) => val !== "app");
const FS_LAYERS_WITHOUT_SHARED = FS_LAYERS.filter((val) => val !== "shared");

const getUpperLayers = (layer) =>
  `*(${FS_LAYERS.slice(0, FS_LAYERS.indexOf(layer)).join("|")})`;

const getLowerLayers = (layer) =>
  `*(${FS_LAYERS.slice(FS_LAYERS.indexOf(layer) + 1).join("|")})`;

const upperLayers = FS_LAYERS_WITHOUT_APP.map((layer) => {
  return {
    from: `**/${getUpperLayers(layer)}/**/*`,
    message: "上位レイヤーのモジュールをインポートすることはできません。",
    target: `**/${layer}/**/*`,
  };
});

const noImport = FS_LAYERS_WITHOUT_SHARED.map((layer) => {
  return {
    from: `**/${getLowerLayers(layer)}/**/no-import/**/*`,
    message:
      "下位レイヤーであってもno-import内のモジュールをインポートすることはできません。",
    target: `**/${layer}/**/*`,
  };
});

const inLayer = FS_LAYERS_WITHOUT_SHARED.map((layer) => {
  return {
    from: `**/${layer}/**/*`,
    message:
      "sharedレイヤー以外はレイヤー内のモジュールをインポートすることはできません。",
    target: `**/${layer}/**/*`,
    except: [`**/${layer}/**/*.test.ts`, `**/${layer}/**/*.spec.ts`],
  };
});

module.exports = {
  "import/no-restricted-paths": [
    "error",
    {
      zones: [...upperLayers, ...noImport, ...inLayer],
    },
  ],
};
.eslintrc.cjs
const fsd = require("./eslint-rules/fsd.cjs");
...
  plugins: [
    "import",
  ],
  rules: {
    ...fsd,
  }

終わりに

今回初めてFieature-Sliced Designを触ってみましたが、アーキテクチャとして見通しの良さを感じました。いっぽうでその素晴らしい思想を既存のプロジェクトとうまくすり合わせていくことが重要だと感じました。なにかの参考になりましたら幸いです。

Discussion