⌨️

三項演算子の条件継ぎ足しのやめ時

2022/05/21に公開

はじめに

プログラミングをしていると、最初は特に問題なかったものが継ぎ足しで変更を加えていった結果、複雑な感じになってしまい「うーん…」と思うことが時々あります。

そのようなことを引き起こす原因の一つに、設計を見直して現在の設計をやめる「やめ時」の判断の遅れによるものがあります。

この記事では、そんなコードの継ぎ足し変更について仮想のおはなしをもとにして、現在の設計をやめる「やめ時」を考える題材として書いてみます。

今回扱う対象は「三項演算子」です。

おはなし

Aさんはとあるショッピングサイトを開発・運営する会社に務めるエンジニアです。

Aさんが開発するショピングサイトはReactを使用して実装されており、今回変更が入る主なコードは次のようになっています。<LogoImage />はWebサービスのロゴです。

export const App: React.FC = () => {
  return (
    <Header>
      <LogoContainer>
        <LogoImage />
      </LogoContainer>
      <MenusContainer>
        ...
      </MenusContainer>
    </Header>
  );
};

最初の変更(ダークモードの対応)

ある日、ブラウザの「ダークモード」に対応することになり、ロゴをユーザーの使用しているモードに合わせて切り替えることになりました。

Aさんはダークモードかどうかを取得できるuseBrowser()を実装し、通常のロゴを<LightLogoImage />、ダークモード用のロゴを<DarkLogoImage />として、次のように三項演算子による分岐で実装しました。

export const App: React.FC = () => {
  const { darkMode } = useBrowser();
  return (
    <Header>
      <LogoContainer>
        {darkMode ? <DarkLogoImage /> : <LightLogoImage />}
      </LogoContainer>
      <MenusContainer>
        ...
      </MenusContainer>
    </Header>
  );
};

変更がリリースされ、ダークモードに対応するようになりました。

2回目の変更(セール期間の対応)

ダークモード対応をリリースしてしばらく経ち、セール期間中にはそれがわかるロゴを表示することになりました。セール期間のロゴもダークモードが使われていた場合、専用のロゴに切り替える必要があります。

Aさんはセール期間かどうかを取得できるuseBusiness()を実装し、セール期間のロゴを<SaleLightLogoImage /><SaleDarkLogoImage />としてコンポーネント化し、次のようにコードを変更しました。

export const App: React.FC = () => {
  const { darkMode } = useBrowser();
  const { sale } = useBusiness();
  return (
    <Header>
      <LogoContainer>
        {darkMode
          ? (sale ? <SaleDarkLogoImage /> : <DarkLogoImage />)
          : (sale ? <SaleLightLogoImage /> : <LightLogoImage />)}
      </LogoContainer>
      <MenusContainer>
        ...
      </MenusContainer>
    </Header>
  );
};

変更がリリースされ、セール期間中に専用のロゴを表示するようになりました。

3回目の変更(プレミアム会員の対応)

セール期間対応のリリースからまたしばらく経ち、これまでの無料会員だけであったユーザーに有料のプレミアム会員枠を設けることになりました。それに伴い、ログインユーザーがプレミアム会員であった場合には専用のロゴを表示することになりました。もちろんダークモードやセール期間ではそれぞれの専用ロゴの対応も必要となります。

Aさんはログインユーザーがプレミアム会員かどうかを取得するuseLoginUser()を実装し、それによってそれぞれの条件のロゴを表示するようにコードに変更を加えました。

export const App: React.FC = () => {
  const { darkMode } = useBrowser();
  const { sale } = useBusiness();
  const { premiumMember } = useLoginUser();
  return (
    <Header>
      <LogoContainer>
        {darkMode
          ? (sale
            ? (premiumMember
              ? <SalePremiumDarkLogoImage />
              : <SaleDarkLogoImage />)
            : (premiumMember ? <PremiumDarkLogoImage /> : <DarkLogoImage />))
          : (sale
            ? (premiumMember
              ? <SalePremiumLightLogoImage />
              : <SaleLightLogoImage />)
            : (premiumMember ? <PremiumLightLogoImage /> : <LightLogoImage />))}
      </LogoContainer>
      <MenusContainer>
        ...
      </MenusContainer>
    </Header>
  );
};

Aさんはなんとなくコードが複雑化してきたと感じつつも変更はリリースされ、ログインユーザーがプレミアム会員の場合に専用ロゴを表示するようになりました。

問い

変更によって条件分岐が増えて段々と複雑化していきました。今後さらに何らかの条件が増えた場合、ますます複雑化していくことが考えられます。

今回のおはなしでの三項演算子の条件継ぎ足しのやめ時はいつだったのでしょう?

  1. セール期間の対応をいれるとき
  2. プレミアム会員の対応をいれるとき
  3. その他

また、その時どのような設計・実装にできるでしょうか?

付録

筆者の「やめ時」の例

筆者の場合、2回目の変更でセール期間の条件を追加するときに元の三項演算子に追加することをやめて、例えば次のように通常のロゴ<DefaultLogoImage />とセール期間のロゴ<SaleLogoImage />のように子コンポーネントに分割します。

export const App: React.FC = () => {
  const { sale } = useBusiness();
  return (
    <Header>
      <LogoContainer>
        {sale ? <SaleLogoImage /> : <DefaultLogoImage />}
      </LogoContainer>
      <MenusContainer>
        ...
      </MenusContainer>
    </Header>
  );
};

// 通常のロゴ
const DefaultLogoImage: React.FC = () => {
  const { darkMode } = useBrowser();
  return darkMode ? <DarkLogoImage /> : <LightLogoImage />;
};

// セール期間のロゴ
const SaleLogoImage: React.FC = () => {
  const { darkMode } = useBrowser();
  return darkMode ? <SaleDarkLogoImage /> : <SaleLightLogoImage />;
};

それぞれの子コンポーネントはダークモードを使うかどうかのみに関心を持ちます。

このようにしておいて、3回目の変更でプレミアム会員の対応が入った際には次のように変更します。

export const App: React.FC = () => {
  const { premiumMember } = useLoginUser();
  return (
    <Header>
      <LogoContainer>
        {premiumMember ? <PremiumLogoImage /> : <FreeLogoImage />}
      </LogoContainer>
      <MenusContainer>
        ...
      </MenusContainer>
    </Header>
  );
};

// 無料会員のロゴ
const FreeLogoImage: React.FC = () => {
  const { sale } = useBusiness();
  return sale ? <SaleLogoImage /> : <DefaultLogoImage />;
};

// プレミアム会員のロゴ
const PremiumLogoImage: React.FC = () => {
  const { sale } = useBusiness();
  return sale ? <SalePremiumLogoImage /> : <DefaultPremiumLogoImage />;
};

// プレミアム会員の通常のロゴ
const DefaultPremiumLogoImage: React.FC = () => {
  const { darkMode } = useBrowser();
  return darkMode ? <PremiumDarkLogoImage /> : <PremiumLightLogoImage />;
};

// プレイアム会員のセール期間のロゴ
const SalePremiumLogoImage: React.FC = () => {
  const { darkMode } = useBrowser();
  return darkMode
    ? <SalePremiumDarkLogoImage />
    : <SalePremiumLightLogoImage />;
};

無料会員のロゴ<FreeLogoImage />とプレミアム会員のロゴ<PremiumLogoImage />コンポーネントはセール期間かどうかだけに関心をもち、ダークモードかどうかは更に内部のコンポーネントの関心とします。

子コンポーネントに分割して三項演算子の条件は常に1つまでにし、たくさんのことに関心を持ちすぎないようにするという例でした。

Discussion