🐹

複雑なメニュー項目の出し分けをスプレッドシートと日本語実装で整理した話

に公開

この記事はドワンゴ Advent Calendar 2025の15日目の記事です。

はじめに

こんにちは、株式会社ドワンゴでニコニコ生放送のフロントエンド開発を担当している misuken です。

11月に書いた記事で、超難解な仕様をスプレッドシートで整理し、日本語で実装する手法を紹介したところ、思いの外好評だったので、また別のアプローチで改善した事例を紹介しようと思います。

https://zenn.dev/misuken/articles/32807daad601d0
https://zenn.dev/misuken/articles/22be0dba9b6947

TL;DR

複雑な仕様をコンパクトにまとめるためのアプローチ。

  • 差の小さな部分を無理なく統合できないか模索
  • 情報の位置関係を揃えてパズルの要領で要所を推測
  • 早期リターンのような制約を追加して表現を効率化
  • 式と結果の関係でわかりやすさを向上
  • 日本語キーワード実装で仕様と実装の対応を明確化

結果: 仕様の効率的な言語化と、仕様と明確に連動した実装を実現

今回のお題

プレーヤーの横に表示されるコメントを操作するためのメニュー、通称「番組コメントメニュー」の複雑な仕様を整理して実装まで改善した話を書きます。

番組コメントメニューの表示例
番組コメントメニューの実際の表示例

問題の把握

仕様を整理する前に、まず何が問題なのかを明確にしておく必要があります。
このセクションでは、番組コメントメニューの複雑さを紐解き、従来の管理方法の限界を確認していきます。

問題を正確に把握することで、どのような方向性で整理すべきかが見えてきます。

番組コメントメニューの複雑さ

シンプルなメニューに見えますが、項目表示条件の複雑さを甘く見てはいけません。

誰(ロール)から見て、誰(コメントの所有者)の、どの状態(表示状態)の、どの種類(記名/匿名)のコメントかによって、表示されるメニュー項目が変わります。
条件の掛け合わせが多いうえ、特定の組み合わせだけのものもあるなど、これだけでヤバそうなことがわかります。

  • ロール
    • 放送者
    • 視聴者
    • モデレーター(放送者から指名された特別な権限を持つ視聴者)
  • 誰のコメントか
    • 放送者のコメント
    • 視聴者のコメント
    • モデレーターのコメント
    • 自身のコメント(視聴者の場合)
  • コメントの状態
    • 通常時(表示中)
    • ブロック後(放送者かモデレーターが配信からブロックしたユーザーのコメント)
    • 削除後(放送者が削除したコメント)
    • NG後(視聴者がNG登録したコメント)
  • 視聴者のコメントの種類
    • 記名コメント(コメントしたユーザーの名前を表示するコメント → なふだコメント)
    • 匿名コメント

仕様整理を始めるきっかけ

仕様整理を始めるきっかけになったのは、項目の変更依頼がFigmaの指示書で来たことでした。
元々仕様書のようなものは存在しておらず、デザイナーさんが表示仕様として考えられるパターンを並べていく形で運用されていました。

Figmaの指示書(放送者用メニュー)
Figmaの指示書(放送者用メニュー)

Figmaの指示書(視聴者用メニュー)
Figmaの指示書(視聴者用メニュー)

Figmaの指示書(モデレーター用メニュー)
Figmaの指示書(モデレーター用メニュー)

指示書が事実上の仕様であることの問題点

よくある指示書とは思うのですが、開発視点では色々と困りどころがあります。

  • 全パターン網羅できているかが不明
  • 間違っている部分や対称性の崩れがあっても気付きにくい
  • 実装方法に悩む

開発者が実装時に必要なのは、見た目の一覧だけではなく、網羅された項目ごとの表示条件です。

指示書に並べられたパターンの数だけ分岐を書いて実装することも可能ではありますが、条件の掛け合わせが増えた際、指数関数的に分岐が増加します。

仕様整理

ここからが本題です。複雑に絡み合った仕様を、どのように整理していったのか、順を追って説明していきます。

要求の書き起こし

まずは全体像を把握するため、Figmaの指示書から愚直に条件の組み合わせを洗い出し、企画さんやデザイナーさんにヒアリングしながら要求を書き起こすところから始めました。

ひとまず分量の多い放送者用と視聴者用のメニューを洗い出して、分量の少ないモデレーター用のメニューは後回しにしています。

初期の書き起こし表
放送者用・視聴者用メニューの初期の書き起こし表

差の小さい部分を統合

ここから不必要な情報量を減らしていきます。

視聴者コメントと、モデレーターコメントの表をよく見てみると、違いがあるのは「モデレーターに追加」と「モデレーターから削除」の赤枠で囲まれた2項目だけです。
それも、匿名コメントの部分はどちらも同じなので、記名コメントのときだけ視聴者コメントとモデレーターコメントに差があるということになります。

視聴者コメントとモデレーターコメントの差分
視聴者コメントとモデレーターコメントの差分(赤枠の2項目のみ)

たしかに、モデレーターが視聴者ベースであることを考えると、共通項が多いのも納得です。
これを整理すると、視聴者コメントは3種類あることがわかります。

  • 視聴者の記名コメント(モデレーターの記名コメントを除く)
  • モデレーターの記名コメント
  • 匿名コメント

視聴者コメントの1区画を2列から3列に拡張して統合すると、以下のようになります。
赤い背景が統合した部分です。また、これにあわせて3列とも同じ判定になるセルは結合でまとめました。

モデレーター記名コメントを統合した表
視聴者コメントを3列に拡張してモデレーター記名コメントを統合

これでモデレーターのコメントに関しては、最小限の差分で表現できるようになりました。

視聴者用メニューの統合

次に最初に縦に並べていた放送者用メニューと視聴者用メニューに関しても、共通点を探っていきます。

この2つのメニューを横に並べ、情報の位置関係が揃うように調整します。
すると、両メニューの通常時の青枠の部分が一致し、赤枠の部分は互い違いになりました。

放送者用と視聴者用メニューを横に並べた状態
放送者用と視聴者用メニューを横に並べ、ロールカラムを追加

パズルとして捉えると形がピッタリハマる状態ですが、赤枠の部分は放送者用メニューと視聴者用メニューで出し分けが必要なため、統合するにはもうひと工夫必要です。

次の画像のように、紫枠のところにロールというカラムを追加し、項目ごとに利用可能なロールを指定できるようにすると、統合しても出し分けの表現を保てます。

赤背景の部分が統合した部分です。これで視聴者用メニューの一部を放送者メニューの表に統合できました。

視聴者用メニューの一部を統合した表
視聴者用メニューの一部(通常時の項目)を放送者用メニューに統合

排他関係の列の連結

残りの視聴者用メニューの部分も整理していきたいところですが、残った "NG後" の列は、放送者用メニューに存在せず、形も一致しないので先ほどのように統合することはできません。

ロールとメニュー項目の列の対応からわかるように、互いに存在しない状態と言えます。

  • 放送者はNG登録という概念がないので、"NG後" の状態は存在しない
  • 視聴者はブロックや削除という概念がないので、"ブロック後" や "削除後" の状態は存在しない

この場面では、次の画像のように列をそのまま連結するだけで解決します。
なぜなら、放送者の場合は "NG後" の列に到達することがなく、視聴者の場合も "ブロック後" と "削除後" の列に到達することがないためです。

到達したら影響があるかもしれませんが、そのような状態が起こりえないため、到達しないロールからすれば関心がありません。

仮に放送者がNG登録できるようになった場合を想定しても、放送者と視聴者で "NG後" の列の表示パターンに違いは生まれません。つまり、いずれに到達するようになったとしても問題ないので、結果的に列を連結しても問題ないということになります。

念の為、その意図を注釈で補足し、現状完全に到達しないグレーのセルに関しては「存在しない」とし、理由をセルのメモに記載することにしました。

ブロック後・削除後・NG後の列を連結した表
状態別の列を連結して統合(到達不可能な部分は「存在しない」)

制約列を追加して完全統合

これで残る視聴者用メニューは自身のコメントだけになりました。

視聴者の自身のコメントとはどのようなものであるかを考えます。

通常は他者の視聴者コメントに対して、メニューを表示すると多くの項目が表示されますが、自身のコメントの場合は2つの項目しか表示されません。

これを整理するには、少し視点を変える必要があります。
「自身のコメントのときに特定の項目を表示しない」と捉えるのではなく、自身のコメントの❌️のところを反転させて「他者の視聴者コメントであれば表示する」と捉えます。

これを制約としてロールの隣に追加すると「他者の視聴者コメント」という制約を満たさなければ、項目を表示できない、いわば早期リターンのような役割になります。
実はロールの部分も早期リターンのように振る舞っているので、流れに沿った改善と言えます。

これで、視聴者用メニューと放送者用メニューの表は統合され、◯◯用メニューとして分ける必要がなくなりました。

視聴者用メニューを完全に統合した表
視聴者用メニューを完全に統合(「他者の視聴者コメント」制約を追加)

モデレーター用メニューへの対応

ここまで整理されると、後回しにしていたモデレーター用メニューの条件も簡単に追加できます。
モデレーターが利用可能な項目にロールを増やすだけです。また、ユーザー生放送番組でしか表示しない項目もあったため、制約に追加しました。

モデレーター用メニューも統合した表
全ロール(放送者・視聴者・モデレーター)のメニューを1つの表に統合

これで定義は95%完成といった感じですが、わかりやすさはまだ20%程度です。
正しく定義されているものの、毎回表から条件を読み取るのは大変ですし、この表の表現だけでは決して把握しやすいとは言えません。
そのうえこの状態から正確に実装するとなると、さてどうしようかなと考えてしまうでしょう。

しかしこの課題解決は簡単です。ここから残り5%の作業で、わかりやすさを一気に80%引き上げます。

最終調整

やることはとても簡単で、今作った表のメニュー項目ごとに、✅になる表示条件を簡潔な言葉で表現する列を追加するだけです。

一見そんなに変わってないように思えるかもしれませんが、表示条件を一つ一つ読んでいくと、✅の部分を最も効率的かつ論理的に表現できていることがわかります。

この表示条件をそのまま実装すれば良いとなれば、あとはもう楽勝です。

表示条件を追加した最終形の表
各メニュー項目の表示条件を簡潔な言葉で表現した最終形

何が起きたのか?

これは、最初に定義した表が「正規化された式」であり、 表示条件の一覧が「その式を解いた結果」だからです。

式のままだと計算しないと結果が得られませんが、一度計算して結果を記しておけば、その後の理解が容易になります。
それだけでなく、結果がおかしいと感じたら式か計算が間違っていることにも気付きやすくなるなど、チェック機構としても機能します。

我々が欲しているのは、計算された結果であり、その結果を得るために必要なものが、過不足なく整理された式ということになります。

実装

今回の記事でも、仕様をいかにわかりやすい実装に仕上げたかを紹介していきます。
表示条件の一覧を用意したことで、実装はとても楽なものです。今回も前回の記事のように、日本語を使いながら実装していきます。

条件オブジェクトの生成

番組コメントメニューはPropsを生成するContainerComponent側の出し分けで制御しています。

まず、スプレッドシートに現れる日本語のキーワードを条件オブジェクトに詰めていきます。
ここだけが実装のややこしい部分なので、判定を間違えないよう、一つずつ慎重に作ります。

  /** 選択中コメントの状態を格納したオブジェクトを返す */
  private get selectedCommentCondition() {
    const userComment = this.selectedUserComment;
    const 放送者であるか =
      this.stores.user.state.isBroadcaster &&
      (this.stores.program.providerTypeIsChannel || this.stores.program.providerTypeIsCommunity);
    const モデレーターであるか = this.stores.user.isPermittedModeratorCommentFilterEdit;
    const ブロック後であるか = userComment?.isBlocked;
    const 削除後であるか = userComment?.isDeleted;
    const NG後であるか = userComment?.isNg;
    const モ記コメであるか = this.selectedCommentIsModeratorSignedComment;

    // 出し分けの仕様
    // https://docs.google.com/spreadsheets/***
    return {
      ユ生番組であるか: this.stores.program.providerTypeIsCommunity,
      放送者であるか,
      /** NOTE: ここではモデレーターは視聴者と判定しない点に注意 */
      視聴者であるか: !放送者であるか && !モデレーターであるか,
      モデレーターであるか,
      /** "他視コメ" とは "他者の視聴者コメント" のこと、コードを見やすくするために省略しています */
      他視コメであるか: this.selectedCommentIsOtherUserComment,
      /** "モ記コメ" とは "モデレーター記名コメント" のことで、コードを見やすくするために省略しています */
      モ記コメであるか,
      /** "視記コメ" とは "視聴者記名コメント" のことで、コードを見やすくするために省略しています */
      // NOTE: 視聴者コメント以外(userComment が undefined)の場合は falsy な点に注意
      // NOTE: 視聴者コメントであっても、ここではモデレーター記名コメントは視記コメと判定しない点に注意
      視記コメであるか: userComment && !userComment.isAnonymous && !モ記コメであるか,
      表示中であるか: !(ブロック後であるか || 削除後であるか || NG後であるか),
      未ブロックであるか: !ブロック後であるか,
      削除後であるか,
      ユーザーIDNGであるか: !userComment?.isUserIdNg,
      コメント未NGであるか: !userComment?.isCommentNg,
    };
  }

Propsの生成

あとは、この条件オブジェクトを使い、メニュー項目ごとに表示条件をそのまま記述すれば完成です。

そのまま書くだけなので、コピペしてくれば間違えようもないですし、レビューも容易です。

以下の関数内では、! による反転間違いのうっかりミスを防ぐため、全てを真(true)の条件で書けるようにしています。
状態計算の複雑さは条件オブジェクトを作る責務に集約し、propsを組み立てる段階ではスプレッドシートとの整合性を重視しています。

このように、スプレッドシートの段階からキーワードを調整するなど、細かい工夫を重ねることで、責務の整理されたわかりやすい実装につながります。

  public get props(): Props | undefined {
    // 〜省略〜

    // NOTE: 判定が複雑なため、敢えて日本語で書いています
    const {
      ユ生番組であるか,
      放送者であるか,
      視聴者であるか,
      モデレーターであるか,
      他視コメであるか,
      モ記コメであるか,
      視記コメであるか,
      表示中であるか,
      未ブロックであるか,
      削除後であるか,
      ユーザーIDNGであるか,
      コメント未NGであるか,
    } = this.selectedCommentCondition;
    const canCopyText = ClipboardUtil.canCopyText();
    
    // IFは記述を簡潔にするためのヘルパー関数です
    // 略語を多用しているのは、1行に並べることでスプレッドシートとの対応関係をわかりやすくするためです
    // 実際のpropsの中身は省略しています

    return {
      // コメントをコピー
      commentCopyButton: IF(canCopyText) && { /*省略*/ },
      // ユーザーIDをコピー
      userIdCopyButton: IF(canCopyText && 他視コメであるか && 表示中であるか) && { /*省略*/ },
      // コメントが投稿された時点から視聴(常に表示される)
      programSeekToCommentPositionButton: IF(this.canSeekToCommentPosition) && { /*省略*/ },
      // コメントを削除
      commentDeleteButton: IF(放送者であるか && 他視コメであるか && 表示中であるか) && { /*省略*/ },
      // コメント削除を取り消す
      commentDeleteCancelButton: IF(放送者であるか && 他視コメであるか && 削除後であるか) && { /*省略*/ },
      // ユーザーを配信からブロック
      userBlockAddButton: IF((放送者であるか || モデレーターであるか) && 他視コメであるか && 未ブロックであるか) && { /*省略*/ },
      // コメントをNGに追加
      ngCommentRegisterButton: IF(視聴者であるか && 他視コメであるか && コメント未NGであるか) && { /*省略*/ },
      // ユーザーIDをNGに追加
      ngUserIdRegisterButton: IF(視聴者であるか && 他視コメであるか && ユーザーIDNGであるか) && { /*省略*/ },
      // モデレーターに追加
      userProgramModeratorPrivilegeAddButton: IF(
        放送者であるか && 他視コメであるか && ユ生番組であるか && 表示中であるか && 視記コメであるか,
      ) && { /*省略*/ },
      // モデレーターから削除
      userProgramModeratorPrivilegeDeleteButton: IF(
        放送者であるか && 他視コメであるか && ユ生番組であるか && モ記コメであるか,
      ) && { /*省略*/ },
      // 荒らし通報
      commentAllegationButton: IF(他視コメであるか) && {},
    };
  }

// 〜省略〜

/**
 * boolean を `true | undefined` 型にして返すヘルパー。
 * NOTE: `&&` 演算子で条件が false の場合に undefined を返すことで、props の undefined を許容する型に対応させています。
 */
function IF(bool: boolean | null | undefined): true | undefined {
  return bool || undefined;
}

余談。。。まさかの展開

このように仕様を整理してみると、自然と複雑さの正体が浮かび上がってくることがあります。

今回も記事を書きながら改めて見返してみたところ、番組コメントメニューの複雑さは、次の画像の赤枠で囲った4箇所が❌であることが原因とわかりました。

  • ブロック、削除、NGにしたらユーザーのIDをコピーの項目を表示しない
    • 非表示にしたユーザーのIDをコピーしたいシーンはなさそう?
    • あっても良さそう?
  • ブロック後に削除系の項目を表示しない
    • ブロックしたユーザーのコメントをさらに削除する意味はなさそう?
  • ブロック後にモデレーター追加の項目を表示しない
    • ブロックしたユーザーをモデレーターに追加したいシーンはなさそう
  • 削除後にモデレーター追加の項目を表示しない
    • 削除したコメントのユーザーをモデレーターに追加したいシーンはなさそう

どれも項目の表示を最適化した結果で、表示してはいけないものではないけど、表示しないほうがユーザーさんの認知負荷を減らせそうなものです。

複雑さの原因となる4箇所の特別条件
赤枠の4箇所が❌になっていることが複雑さの原因

この4箇所が通常時の列と同じであれば、定義はかなりシンプルになるのは当然ですが、そうも行かないので少し視点を変えてみます。

  • シンプルな定義にしたいけど、特別な条件が4箇所ある
  • シンプルな定義 + 4つの特別条件 という形に分けて考えてみる

シンプルな定義はそれはそれで完結させて、4つの特別な条件を別枠で足せば意図も明確になりそうです。

早速試してみるとこのとおり。

  • 視聴者コメントは純粋にシンプルな定義で表現
  • 状態が変化する操作項目は初期と事後の状態で分けて表現
  • 特別条件を除外条件として追加

シンプルになりすぎて不安になるくらいですが、これで正しく表現できています。

純粋な定義と除外条件を分離した表
純粋な定義と特別な条件(除外条件)を分けて表現した最終形

各項目の表示条件の列もより簡素になり、除外条件の列が分かれていることで何を最適化したかの内容も明確になりました。

結果的にわかったことは「混ぜるな危険!純粋な定義と最適化は分けて扱いましょう」ということでした。

まとめ

今回も複雑な仕様をスプレッドシートで整理し、仕様と連動した日本語で実装するアプローチを紹介しました。

余談のところでまさかの展開になりましたが、ちゃんと整理したからこそ浮かび上がってきた答えだったと言えるでしょう。

実際、仕様を正確に表したことで、元々対称性のおかしなところが見つかり改善に繋がったり、「この場面では項目が出ていても良いのでは?」といった議論に発展するなど、全体を正確に把握・制御できるようになりました。

今後仕様変更が入る際も、スプレッドシートを見ながらどこをどう変えるか話し合えるので安心です。

仕様の整理方法は色々ありますが、この記事のアプローチがみなさんのプロジェクトの仕様整理や実装のヒントになると幸いです。

Discussion