コードのシンプルさで実現する、持続可能なプロダクト成長


ビジネスに限らず身の回りの様々な文脈において、「シンプルさ」はそれ自体が大きな価値とされています。シンプルな仕組みは多くの人にとって伝わりやすく、理解しやすく、扱いやすく、目標の達成に意識を集中することができます。つまり、シンプルさは推進力につながります。

Gaji-Labo では様々な事業をサポートし、プロジェクトの成果が最大化されるよう日々取り組んでいます。その営みのなかでも「シンプルさ」は大きな価値を生んでいます。

この記事では、数ある文脈の中から、プロジェクトを支える「プログラムコードのシンプルさ」にフォーカスして考えていきます。

プログラムコードにおけるシンプルさ

コードがシンプルであるということは、とても大切なことです。

シンプルなコードベースは可読性が高く、保守しやすく、テストがしやすく、実装者は余計な部分に意識を割かれることなく、今眼の前にあるタスクに集中することができます。実装者の思考もシンプルになるため速度もあがります。

しかし、プロジェクトが大きく成長するごとにコードベースはどんどん肥大化、そして複雑化していきます。複雑なコードは読み解くことがとても大変で、コードを介したメンバー間のコミュニケーションを難しくし、属人性の高さへと繋がります。小さな負債が積み重なり、根本解決が遠くなり、さらなる複雑さを生むことになるかもしれません。システム全体の把握は難しくなり、バグも生まれやすくなります。

そうならないために、コードベースはできうる限りシンプルに、健全な状態に保ちたいものです。わたしたちはリファクタリングというプロセスを繰り返し行うことで、こうした複雑さを除去していきます。

シンプルさを損なう複雑性を知る

ここまで「シンプル」という言葉を連呼してきましたが、この言葉はいささか抽象的で、もう少し具体化しなければイメージがわきません。しかし、「シンプルである」ということを直接的に言語化するのはなかなか難しいものです。そこで、逆説的に「複雑なコード」とはどのようなものかを足がかりにして考えていきます。

重複している(DRY原則)

よく知られているプログラミングの原則にDRY原則(Don’t Repeat Yourself)があります。重複は無駄なものであり、システムの複雑性を増すものとして除去し、必要に応じて抽象化・共通化されるべきだという考えです。

// 注文が出荷済みであり、配送先が設定されているかをチェックする条件が重複
if (order.status === "shipped" && order.deliveryAddress) {
  console.log("配送状況をお客様に通知します。");
}

if (order.status === "shipped" && order.deliveryAddress) {
  console.log("配送業者に出荷依頼を送信します。");
}

例えば、同じ処理がコピー&ペーストのように複数箇所に記述されていたら、それは重複です。関数などに共通化して、もしその処理に変更が加わった場合に、一箇所だけ手を加えれば済むように単純化します。

また例えば、同一の働きをする関数が複数存在している場合は、抽象化を施して統合することでシンプルになります。

しかし、ここで気をつけたいことがあります。

DRY原則における難しいポイントは、コードの見た目が重複しているからといって必ずしも抽象化・共通化が正しいわけではないという点です。それをすることでかえってメンテナビリティを損ねてしまう場合もあります。それは本質的にシンプルさと相反するものです。が、ここで詳しく語るには少し複雑すぎるので、一旦脇に置いておきましょう。

多くの機能や責務を持ちすぎている(単一責任の原則)

ひとつのコンポーネントに多くの機能を詰め込みすぎてしまったことはありませんか?十徳ナイフのように多機能なツールは一見魅力的にも思われます。

例えばユーザー情報を表示するシンプルなコンポーネントがあるとします。

export function UserInfo({
  name,
  email,
  avatarImageUrl,
}: UserInfoProps): JSX.Element {
  return (
    <div className="UserInfo">
      <img src={avatarImageUrl} alt="User Avatar" />
      <ul>
        <li>Name: {name}</li>
        <li>Email: {email}</li>
      </ul>
    </div>
  );
}

このコンポーネントに機能を追加します。 userId を受け取ってそれを元にユーザー情報を非同期で取得させたり、内部にステートを持たせて同じコンポーネントでユーザー情報を編集できるようにします。

export function UserInfo({ userId }: UserInfoProps): JSX.Element {
  const [user, setUser] = useState<User>({ name: "", email: "", avatarImageUrl: "" });
  const [isEditing, setIsEditing] = useState<boolean>(false);

  useEffect(() => {
    (async () => {
      const data = await (await fetch(`/api/users/${userId}`)).json();
      setUser(data);
    })();
  }, [userId]);

  const handleEditClick = () => {
    setIsEditing(true);
  };

  return (
    <div className="UserInfo">
	    {isEditing ? ( ... ) : ( ... )}
	  </div>
  );
}

ひとつのコンポーネントが多くの役割を持つようになりました。これは、単一責任の原則(Single Responsibility Principal)に反するものです。上の例はまだ大して詰め込まれていませんが、実際のケースではもっと複雑化します。

責務が詰め込まれすぎた部品は、単純にコードが長くなるだけでなく構造も複雑になるため、メンテナビリティを欠くことになります。変更した際の影響範囲も把握しづらく、予期せぬ不具合を引き起こす可能性が高まります。

また、複数の機能がひとつの入れ物にパッケージングされている状態なので、個々の機能を再利用することができません。上の例で言えば、他のパーツでもユーザー情報を取得したくなった場合、既に機能があるにも関わらず新しく実装しなければなりません。このように重複コードが生み出されるようなことになれば、設計はさらに混沌としていくでしょう。

コンポーネントに多くの責務を負わせすぎたことに気づいたら、共通部分を分離したり、責務ごとに機能を抽出するなどして、可能な限りシンプルな構成にすることを心がけます。

現時点で必要のない機能がある(YAGNI原則)

関数やコンポーネントを作る時に、「こんなオプションが将来必要になるかもしれない」と言ってその時点では必要のない機能を実装してしまった経験はないでしょうか。なんて耳が痛いのでしょう!そしてそういった機能は結局使われないことが多いのです。

このような無駄を省くのがYAGNI原則(You Aren’t Gonna Need It)です。直訳すると「それはきっと要りません」のようになりますが、砕いて言い換えると「それが必要になるまで実装すべきでない」という原則をあらわしたものです。余分な機能は設計を複雑にします。プログラムは単純であればあるほど変更につよく、バグを生み出しにくくなります。

例えばこれはシンプルなタイトルコンポーネントです。

interface TitleProps {
  children: ReactNode;
  tag: "h1" | "h2" | "h3" | "h4" | "h5";
}

export function Title({ children, tag }: TitleProps): JSX.Element {
  const Component = tag;
  return <Component className="Title">{children}</Component>;
}

このコンポーネントに、「今は必要ないけどひょっとしたら将来的に必要になる可能性もなくはない機能」を追加します。

interface TitleProps {
  children: ReactNode;
  tag: "h1" | "h2" | "h3" | "h4" | "h5";
  color?: CSSProperties["color"];
  fontSize?: CSSProperties["fontSize"];
  isUppercase?: boolean;
  onClick?: () => void;
  margin?: CSSProperties["margin"];
  padding?: CSSProperties["padding"];
  textAlign?: CSSProperties["textAlign"];
  lineHeight?: CSSProperties["lineHeight"];
}

型宣言だけなのに、今は必要のない要素によって一気にコードの見通しが悪くなりました。

新規にパーツを作成する時は、その時要件として求められている機能だけを実装するよう心がけます。新たなオプションが必要になったその時に追加しても、遅すぎることはありません。

副作用が顕著である

関数が結果を返す以外に外部に影響を与える働きのことを「副作用」と呼びます。ごく簡単な例を以下に挙げます。

let count = 0;

function increment () {
  count += 1; // 副作用
  return count;
}

この increment 関数は結果の数値を返却する以外に、 count という外の世界にある変数を操作しています。これが副作用です。上の例は短く単純なのでまだ分かりやすいですが、数十行以上からなる関数の中に複数の副作用があるコードを見ると、複雑さに頭を抱えてしまいます。ニンゲンの認知能力の限界を迎えます。

副作用は複雑さであり、不透明性です。実行した時に何が起きるのかが外から見てわからないブラックボックスです。中身のコードを読んではじめてどのような影響を外の世界に与えるか知ることができるのです。つまり、プログラムの働きを理解しようとする人に複雑難解なコードの解読を強いることになります。

副作用をシステムから完全に排除することはできません。しかし、処理を分割したり、副作用をもつ関数を一部に局所化するなどして整理することはできます。または、明確な命名規則やコメントなどでリーダビリティやメンテナビリティを保つことができるかもしれません。

コードをシェイプアップするために

いくつかの例を挙げましたが、コードを複雑にしてしまう要素はこれだけではなく、無数のパターンが存在すると思います。それらを網羅的にひとつずつ検証していくことは、あまり現実的ではありませんね。

コードがシンプルであるかどうかを検証するプロセスもまた、シンプルでありたいものです。そこで、そのための判断基準を抽象化して考えてみるとしましょう。どのような時にコードが複雑であると感じるでしょうか?

  1. コードが読みづらいと感じる
  2. テストが書きづらいと感じる
  3. 変更しづらいと感じる
  4. 「自分だったらこう書く」がイメージできる

これらのチェックに引っかかったコードは原則に則って検証し、それを足がかりにしてリファクタリングを進めていきます。

1. コードが読みづらいと感じる

コードを読んでいてわかりづらいと感じたり、認知負荷が高かったり、求めている情報になかなかたどり着けなかったりした場合、そのコードは改善の余地があるはずです。違和感を感じた箇所に対して、なぜこのコードは読みづらいのか、どうしたらよりシンプルに書けるのかを考えていきます。

2. テストが書きづらいと感じる

テストが書きづらいと感じるコードでは、責務が集中しすぎていたり、部品同士が密に結合してしまっていたりすることがあります。そんなときは、適切なサイズに機能を分割したり、整理をするなどしてシンプルさを取り戻します。

3. 変更しづらいと感じる

既存の機能に変更や拡張を加えるストーリーを想像しましょう。それは簡単に実現できそうでしょうか?もしコードベースに起因する理由で変更が難しそうだと感じた場合、それは設計改善の重要な手がかりになるはずです。

3. 「自分だったらこう書く」

コードのある部分を見て「自分だったらこう書く」という具体的なイメージがよぎったならば、それは改善案として使えるかもしれません。

ただし、このチェックには注意が必要です。そのアイデアは個人的な趣向に寄ってしまっている可能性も大いにあるからです。その書き方がより良い書き方だと言うことができる客観的な理由を説明できるのならば、それはきっと良い提案になるでしょう。それを検証するときには上で挙げたプログラミング原則の知識が役立つはずです。

おわりに

プロジェクトの成長に伴ってプログラムコードは確実に複雑化します。これは避けられません。

プロダクトが価値を届け続けられるように、わたしたちはコードのリーダビリティ、テスタビリティ、メンテナビリティを維持し、可能な限りシンプルに保ち続けます。そのために改善サイクルをまわしつづけます。

このようにして、コードのシンプリシティはプロダクトを持続的に成長させていくのです。

今回はプログラムコードのシンプルさにフォーカスしましたが、プロジェクトの進行やコミュニケーションなど、シンプルさが鍵となる文脈は他にもあると思います。その話はまたいずれ別の機会に。

Gaji-Labo フロントエンドエンジニア向けご案内資料

Gaji-Labo は新規事業やサービス開発に取り組む、事業会社・スタートアップへの支援を行っています。

弊社では、Next.js を用いた Web アプリケーションのフロントエンド開発をリードするフロントエンドエンジニアを募集しています!さまざまなプロダクトやチームに関わりながら、一緒に成長を体験しませんか?

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください!

求人応募してみる!


投稿者 Oikawa Hisashi

フロントエンドエンジニア。モダンなJavaScript開発に関心があります。 デザインからバックエンドまで網羅的にこなすマルチデザイナーとして長く活動してきた経験を活かして、これから関わる様々なものをデザインしていきたいです。チームもコミュニケーションもデザインするもの。ライフワークはピアノと水泳。