🚀

ウェブフロントに見る clean architecture の一例

2023/01/03に公開

最初に: clean architecture は誤解されている

『Clean Architecture 達人に学ぶソフトウェアの構造と設計』(以下『Clean Architecture』)を読んだことがありますか?例の同心円の図しか知らないという人も多いでしょう。

例の同心円図
画像出典: Robert C. Martin 「The Clean Architecture

さて、ここでクイズです。「Clean architecture とは、 controllersuse casesentities というクラスを作って繋げるアーキテクチャのことだ、○か×か」。どっちでしょうか?

→ → → 正解は×です。 あの同心円は、あくまで clean architecture の一例として『Clean Acrhitecture』で紹介されたものです。

そう、clean architecture とはアーキテクチャの分類のことです。 「このソフトウェアのアーキテクチャは clean architecture である」のような感じです。

今回は clean architecture とはどういうアーキテクチャなのか、実際の react アプリのアーキテクチャをもとに紹介します。

本記事の対象読者

  • 「同心円に沿って entities とか作ればいいんでしょ?」と 誤解している人
  • clean architecture を読んだけど実際のアーキテクチャのイメージができていない人

clean architecture の例: ストア層とコンポーネント層が分かれているアーキテクチャ

同心円では全くありませんが 以下のアーキテクチャは clean architecture の一つです。

ストア層とコンポーネント層がわかれたアーキテクチャ

実際のコードがこちらです

import useSWR from 'swr';
const useSomeStore = () => {
  const { data, error, isLoading } = useSWR('/api/articles');
  return {
    error,
    data,
    isLoading,
  }
}
import { useSomeStore } from './useSomeStore';
export const Container = () => {
  const { data } = useSomeStore();
  return <Component items={data.items || []} />;
};

このアーキテクチャは、useSomeStore の型がインターフェイスとなってストアとコンポーネントが分かれています。また、インターフェイスはストア層にあり、コンポーネント層がストア層を使う形になっています。よく分からないと思いますがこれから解説していきます。

ストア層とコンポーネント層がインターフェイスで分かれている

typeof useSomeStore がレイヤー間のアーキテクチャになっています。コンポーネント層からすると useSomeStore の実装の中身はわからず、useSomeStore も呼び出されたらなんらかの処理をして errordataisLoading を渡す以上のことができません。このように、コンポーネント層とストア層は互いの詳細について知らず、typeof useSomeStore だけに依存してやり取りをしているのがミソです。

また、インターフェイスの定義はストアが持っています。これによってストアはコンポーネントを一切知らなくてよくなっています。もしコンポーネント層にインターフェイスがあると clean architecture でなくなるので注意してください。

このアーキテクチャのメリット

ストア層とコンポーネント層をわけるアーキテクチャには以下のメリットがあります。

  • ストア層の改修がコンポーネント層に影響しなくなる
  • コンポーネント側の変更もストア層に影響しない

ストア層の改修がコンポーネント層に影響しなくなる

たとえば useSomeStore を改修してキャッシュ機能をつけてみます。

const useSomeStore = () => {
  const { data, error, isLoading } = useSWR('/api/articles');

   // フェッチが完了するまで前回のデータを表示する
  const [cache, setCache] = useState(() => {
    try {
      return JSON.parse(localStorage.getItem('cache'));
    } catch {
      return undefined;
    }
  });
  useEffect(() => {
    if (isLoading || error) return;
    localStorage.setItem('cache', JSON.stringify(data));
  }, [isLoading, error, data]);

  return {
    error,
    data: isLoading ? cache : data,
    isLoading,
  }
}

この機能改修をしても、Container コンポーネントのコードは一切直す必要がありません。これは useSomeStore の型 (() => { data: Article, ... })、すなわちインターフェイスが変わっていないからです。

コンポーネントからはストアの実装の詳細は見えていません。インターフェイスにのみ依存しています。そのため、インターフェイスさえ変わらなければストアをいくら改修したとしてもコンポーネントには影響しません。

ストア層の改修がコンポーネント層に影響しなければ、例えば SWR をやめて Recoil やただの Context API に変えることも簡単にできます。

const articlesQuery = selector({
  key: 'Articles',
  get: async ({get}) => {
    const response = await fetch('/api/articles');
    if (response.error) {
      return { error: response.error };
    }
    return { data: response.json() };
  },
});

const useSomeStore = () => {
  const { data, error } = useRecoilValue(articleQuery);
  return {
    error,
    data,
  }
}

SWR を Recoil に変えるという大変な作業も、ちゃんとストアとコンポーネントが分かれていれば狭い影響範囲で実現できます

(isLoading がなくなってますが、これは僕が recoil に詳しくないだけです…たぶん recoil でもインターフェイスを変えずに実現できます)

コンポーネント側の変更もストア層に影響しない

コンポーネントの変更がストアに影響しないのはほぼ自明だと思うので割愛します。いくら Container の DOM 構造をいじっても useSomeStore のインターフェイスを変える必要がないことを思えば自明ですよね。

Conclusion

身近な React アプリも clean architecture になっているという紹介でした。ストア層とコンポーネント層という考えを持っておくと実装がしやすくなります。ぜひ clean architecture を取り入れてみてください。

最後にもうひとつ。今回紹介したアーキテクチャもあくまで一例です。 実際に自分でアーキテクチャを考えるときに clean architecture にできているかどうかを判断するには、やはり本を一度しっかりと読む必要があります。これを機に読んでみましょう。

https://amzn.asia/d/9P5TpRn

(アフィリンクではなくただのリンクです)

Discussion