😽

フロントエンドもドメインモデル貧血症にしない

2024/11/05に公開

Reactでコンポーネントやhooksを実装する際に、ドメインモデル貧血症にしない方法について記載しました。

ドメインモデル貧血症とは?

ドメイン駆動設計
ドメインモデルが持つべき情報がドメインモデルの外に書かれており、業務知識が漏れ出している状態を、「ドメインモデル貧血症」と呼ぶことがある。

上記のような記載があります。

ビジネスロジックの実装はフロントエンドにもあります。どのように実装するか悩ましい所です。
UIとロジックが密結合になるフロントエンドの実装でも、コンポーネントにロジックを流出をさせない事がベストだと考えています。

以下に会員向けの機能の実装して説明いたします。

仕様

有料会員, 無料会員, ログイン状態の概念がある実装です。バックエンドからのレスポンスとUIの一覧です。

loggedIn isPaidMember UI
true true 感謝メッセージ
true false 有料会員へのリンクを表示
false any ログインページへのリンク

ドメインモデル貧血症な実装

useMemberがバックエンドからデータを取得し、取得結果をコンポーネントへ渡す実装です。loggedIn,isPaidMemberなど値をそのままコンポーネントで使用しています。

課題

以下のような、仕様追加がある場合ネストが深くなりそうです。
有料会員の会員期限が迫った場合は、メッセージを表示する

UIとロジックが密結合になるとコンポーネントの分岐処理のネストが深くなります。どのような種類の状態があるか把握しづらくなります。仕様をそのまま表現できている実装が適切です。

type Props = {
  loggedIn: boolean;
  /**
   * 有料会員
   */
  isPaidMember: boolean;
};

const useMember = (): Props => {
  // バックエンドからデータを取得
  const res = useQuery();

  return {
    loggedIn: res.loggedIn,
    isPaidMember: res.isPaidMember,
  };
};

const Component: React.FC = () => {
  const member = useMember();

  return (
    <>
      {member.loggedIn ? (
        <>
          {member.isPaidMember ? (
            <>有料会員継続ありがとうございます。</>
          ) : (
            <a>有料会員になる</a>
          )}
        </>
      ) : (
        <a>ログインページへ</a>
      )}
    </>
  );
};

フロントのドメインを意識した実装

状態に名前をつける

loggedIn, isPaidMemberなどに対応した名称の__typeを定義しています。
loggedIn === true && isPaidMember === trueならば、PaidMemberのような__typeです。
レスポンスの結果から、存在する状態を明確化し人間が理解しやすい名称をつけています。

適切な粒度でコンポーネントの分割

状態に対応した粒度でコンポーネントを分割しました。おそらくは、Figmaのデザインと同等の粒度になるはずです。それに合わせて、コンポーネントのJSDocにFigmaへのリンクを追加しました。

/**
 * 有料会員
 */
type PaidMember = 'paidMember';

/**
 * 無料会員
 */
type FreeMember = 'freeMember';

/**
 * 会員
 */
type Member = PaidMember | FreeMember;

/**
 * 未ログイン
 */
type NotLoggedIn = 'notLoggedIn';

type Props = {
  __type: Member | NotLoggedIn;
};

const useMember = (): Props => {
  // バックエンドからデータを取得
  const res = useQuery();

  if (!res.loggedIn) {
    return {
      __type: 'notLoggedIn',
    };
  }

  if (res.isPaidMember) {
    return {
      __type: 'paidMember',
    };
  }

  return {
    __type: 'freeMember',
  };
};

/**
 * 有料会員のFigmaへのリンク
 */
const PaidMember: React.FC = () => <>有料会員継続ありがとうございます。</>;

/**
 * 無料会員のFigmaへのリンク
 */
const FreeMember: React.FC = () => <a>有料会員になる</a>;

/**
 * 未ログイン状態のFigmaへのリンク
 */
const NotLoggedIn: React.FC = () => <a>ログインページへ</a>;

/**
 * 仕様書へのリンク
 */
const Component: React.FC = () => {
  const member = useMember();

  if (member.__type === 'notLoggedIn') {
    return <NotLoggedIn />;
  }

  if (member.__type === 'paidMember') {
    return <PaidMember />;
  }

  return <FreeMember />;
};
chot Inc. tech blog

Discussion