システムで扱うステータスの分解と変換

2024/06/27に公開

初めに

レバテック開発部の今井です。

ソフトウェア開発において、データの状態管理は非常に重要です。注文の状態、ユーザーの認証状態、プロジェクトの進行状態など、多岐にわたる状況で、適切な状態管理が求められます。しかし、ビジネス要件の変化や新機能の追加に伴い、状態管理が複雑化し、保守が難しくなることがあります。
この記事では、データの状態管理を簡単にするためにMECEを初めとした方法で分析を提案します。これによって、柔軟で効率的なシステム設計が可能になることを目指します。

TL;DR

  • MECEの原則を使ってenum型ステータスを分解する方法を解説する
  • MECEによる分解から一次情報と二次情報という区分を提案し、分析の高度化を目指す
  • 一次情報と二次情報の区分とシステム間のデータ連係の関係性について考察する

対象読者

  • システムの保守性・拡張性に興味関心のあるエンジニア

enumをMECEに分解する

この章ではenumをMECEに分解します。この方法によってenumのステータスの保守性・拡張性が向上することを説明します。

例:シンプルな注文フロー

あるオンラインショッピングサイトの注文処理システムでは、注文のステータスが「注文済み→支払い済み→配送済み」という単純な流れで管理されていました。このフローの管理のために、次のようなenum型のステータスが使用されていました。

シンプルな注文フロー

  • 注文済み→支払い済み→配送済み
    • 注文が完了すると、支払いを待つ。
    • 支払いが済むと、商品を配送する。
public enum OrderStatus {
    ORDERED,    // 注文済み
    PAID,       // 支払い済み
    SHIPPED     // 配送済み
}

新しい要件:後払いオプションの追加

新たなビジネス要件として支払時に「後払い」を可能にすることが求められました。
これにより、商品が配送された後に支払いが行われるケースが発生しました。

新しく追加されたフローの例

  • 注文済み→配送済み→支払い済み
    • 注文が完了すると、商品がすぐに配送されます。
    • 商品が到着した後、顧客は後払いで支払いを行います。

現在のenum型ステータスでは、この新しい後払いオプションを取り入れることが難しいです。新しい後払いフローを取り入れるためには、以下のような状態を追加する必要があります。

public enum OrderStatus {
    ORDERED,            // 注文済み
    PAID,               // 支払い済み
    SHIPPED,            // 配送料
    UNPAID_SHIPPED      // 未払い、発送済み(NEW!!!)
}

このように、未払いで発送済みの状態を追加すると、以下の問題が発生します。

  • 状態の重複と矛盾:
    • 新しい状態 UNPAID_SHIPPED が追加されたことで、 PAID と SHIPPED の関係が曖昧になります。 PAID は SHIPPED の前提条件なのか、または別の状態なのかが不明確です。
  • 状態の管理が複雑:
    • 各状態をチェックするロジックが複雑化します。例えば、配送されたかどうかを確認するには SHIPPED だけでなく UNPAID_SHIPPED も確認する必要があります。
  • 拡張性の問題:
    • 今後、さらに新しいステータス(例えば、「返品済み」や「キャンセル済み」)を追加する際に、全ての組み合わせを網羅する状態を定義する必要があり、管理がますます複雑になります。

解決策:MECEでステータスを分解

この問題の根本は、注文のステータスを構成する要素が「支払い状況」と「配送状況」の二つあったのに、ひとつのステータスにまとめてしまったことです。このため、新しい「後払い」オプションのような追加要件が発生した際に、ステータスの組み合わせが複雑化し、管理が困難になってしまいました。
これの回避は、ステータスをMECEに基づいて分析することでできます。まずは元の要件をMECEに分解してみましょう。

未発送 発送済み
支払い済み PAID SHIPPED
未払い ORDERED (該当なし?)

この図を見れば、未払いかつ配送済みのステータスがないことに気づきます。これが本当に存在しないのかどうかを確認するために、エンジニアたちは話し合うこともあるでしょうし、ビジネスサイドに問い合わせることもできます。

ここでは、ビジネスサイドに問い合わせたとします。ビジネスサイドの回答は、「直近では不要だが将来的には後払いを導入したいので未払いかつ配送済みのステータスが必要になるかもしれない」だとします。[1]これを先ほどの図に反映します。

未発送 発送済み
支払い済み PAID SHIPPED
未払い ORDERED UNPAID_SHIPPED

この図にもとづいて、enum型を使わない選択肢や、enum型の名前を工夫する選択肢があります。今の要件を優先して何もしない選択肢もあります。いづれにしても、エンジニアたちは事前に将来の可能性を知ることで、適切な判断を下すことができます。

MECEでステータスを分解すると、このように値の組み合わせの可能性を事前に把握することができます。これは将来の拡張性や保守性に貢献します。

MECEの単位と一次情報と二次情報

この章ではMECEの単位から情報を一次情報と二次情報という視点で分析することを提案します。この提案手法によって、二次元のMECEによる分析を一般化します。

MECEはどの単位でありえるべきか?

先ほどはMECEを使って要素を分解する方法を話しました。次にMECEの要素の単位について考えます。
私は次の三つのどれかに分解できるのが理想と思います。この三つのどれかにならない場合、おそらく分解できる要素がもっと存在します。

  • true/falseの2値のフラグ
  • あるステータスの数量の区分(80以上はHigh、79~50はMiddle、49以下はLowのようなもの)
  • true/false/nullの3値のフラグ

個別のステータスとその代表としてのenum

先ほどあげた要素以外のenum的なステータスとはそもそも何者でしょうか?上記の三つのパターンに当てはまらないステータスは、一つ以上のステータスの特定のパターンを示す代表値です。

一つ以上のステータスの特定のパターンとはどういう意味か説明します。このために、先ほどあげたECサイトのステータス例を下に示します。ここで書いたとおり、enumステータスの一つであるORDERDは未払いかつ未発送というステータスを代表としていました。言い換えると、支払いの状態がOFF、発送の状態もOFFであることをORDERDと呼んでいたということです。

  • ORDERED: 未支払いかつ未発送
  • PAID: 支払い済みかつ未発送済
  • SHIPPED: 支払い済みかつ発送済み

ORDERDとそれに対する支払いの状態や発送の状態を区別すると、状態の分析を高度化できる可能性があります。そこで私は分析の高度化のために一次情報二次情報という概念を提案します。

  • 一次情報: 支払いの状態や発送の状態のように他のステータスを構成するための情報
  • 二次情報: ORDERDのような他の要素によって表現できる情報

一次情報と二次情報とは?

一次情報
大抵のシステムでは、扱うべき情報は必ず現実世界または他システムからの入力です。システムそのものから新しく発生する情報は珍しいです。
このように外部から与えられた情報が一次情報です。システムはこれよりも情報を詳細にできません。

よって、enum等のステータスをMECEに分解しても、その表の軸は一次情報以上に細かくできません。逆に言えば、enumのステータスの意味を一次情報のみで説明できるようになったら、最も詳細と言えるでしょう。

二次情報
システムが扱える情報は、本質的に一次情報だけです。
しかしながらそれだけだと不便なことが多々あります。enumのようなステータスを使えないと不便な場面は多くあるでしょう。
そこで一つ以上の一次情報を組み合わせて新たな情報を作ります。これが二次情報です。

結局、その概念で何が嬉しいのか?

enum型などであるステータスを導入したいとき、そのステータスの導入が保守性や拡張性を落とすことにならないかを分析するときに役立ちます。
一次情報であればそれ以上分解できないためおそらく保守性が高いステータスです。もしも導入したいステータスが二次情報であったならば、保守性が低いステータスの可能性があります。

このように一次情報と二次情報という区分が存在すると、分析をどこまですべきなのかの理解が容易です。

二次元のMECE以外の一次情報と二次情報

これまではの分析は、二次情報を二次元の一次情報に分解したと言えます。二次情報は二次元の一次情報以外からも現れます。ここでは一次情報が一次元の例と三次元の例を説明します。[2]

一次元の例
商品という情報があったとします。この商品は削除することができます。削除の状態管理は次のようになってるとします。

  • 削除フラグがONになったら削除済みです。削除フラグがOFFなら削除されていない
  • 削除日時という情報があり、削除日時が存在するときは必ず商品が削除されてる

この場合、削除フラグは削除日時の有無から導出できます。よってそれぞれを一次情報と二次情報で分類すると以下のようになります。

  • 削除フラグ: 削除日時から導出できる二次情報
  • 削除日時: 一次情報

三次元の例
ECサイトの配送料があったとします。この配送料は次の条件で割引が効きます。

  • 配送料は都道府県によって判定する
  • 配送料無料クーポンを使う。ただし、一部離島は1000円引きとする
  • 一度に購入した金額が10000円以上の場合、無料。ただし、一部離島は1000円引きとする
  • 一部離島は配送先の住所によって判定する

この場合、配送料は次の三つの要素で決まります。この場合、この三つの要素が一次情報であり、
配送料が二次情報となります。

  • 住所: 都道府県と一部離島かどうか
  • クーポン: クーポンを使うかどうかで決まる
  • 購入金額: 購入金額が10000円以上かそれより低いかで決まる

一次情報と二次情報の実装

ここまで一次情報と二次情報の話をしてきましたが、それはすべて設計上の話です。次からはアプリケーションコードとDBの二つの実装の話していきます。

アプリケーションコード
少なくともWebアプリケーションの場合、次のようにすることをお勧めします。

  • 二次情報は性能の問題がない限り毎回計算するようにして下さい
  • 二次情報はローカル変数でもない限り、不変にして下さい。二次情報が変更可能になると、一次情報と変更された二次情報の間で差が生まれます

DB
性能やクエリの問題がない限り、二次情報は永続化しないで下さい。二次情報は一次情報から算出できるため、本質的に正規化を崩しています。
もしも二次情報を永続化する場合があっても、正は一次情報であることは徹底すべきです。正規化が崩れている自覚を持って二次情報を扱うべきです。

一次情報と二次情報の連鎖

この章では、前章までの一次情報と二次情報の区分をシステム間連携にまで拡張します。この拡張によってシステムのカプセル化の側面のひとつに情報の捨象があることを示します。

二次情報が他システムに渡されたときにそれは一次情報となる

もしもシステムが取りあつかうデータがすべて現実世界から入力されたもののままだと大変です。
ECサイトの例で言えば、会計ルールが支払い済みかつ配送済みの場合に売上計上するならば、会計システムは「支払い済みかつ配送済み」かそうではないかだけを管理すれば良いです。
この理由で、「支払い済みかつ配送済み」かそうではないかだけを会計システムに渡した場合、会計システムにとって、「支払い済みかつ配送済み」かそうではないかがtrue/falseで管理される一次情報です。
このように外部のシステムに渡された二次情報は、渡された外部のシステムにとって一次情報となります。

この考え方は、二次情報と一次情報が混ざっているステータスの分解に役立ちます。

例:ECサイトの詳細な注文ステータス

ECサイトの注文ステータスが詳細な例を考えます。今度も注文を受けたら支払いを行い、支払いが完了したら配送するというフローです。しかし、今度の管理したい項目は先ほどよりも詳細です。

public enum OrderStatus {
    ORDERED,                         // 注文済み
    PARTIALLY_PAID,                 // 一部支払い済み
    PAYMENT_AUTHORIZED,             // 支払い与信取得済み
    PAYMENT_COMPLETED,              // 支払い完了
    SHIPMENT_PREPARING,             // 配送準備中
    SHIPMENT_DISPATCHED,            // 出荷済み
    SHIPMENT_IN_TRANSIT,            // 配送中
    SHIPMENT_READY_FOR_PICKUP,      // コンビニ受け取り準備完了
    SHIPMENT_PICKED_UP,             // コンビニで受け取り済み
    SHIPMENT_DELIVERED,             // 配達完了
}

解決策:支払いと配送のフェーズに分ける

この詳細なステータスの分解は簡単ではないです。例えば、支払いのステータスは次の四つです。これを一部支払いフラグや、与信フラグに分解することは可能ですが、配送状態の管理からみたら煩雑な管理です。

  • ORDERED: 未支払い
  • PARTIALLY_PAID: 一部支払い済み
  • PAYMENT_AUTHORIZED: 与信だけ取ってる
  • PAYMENT_COMPLETED: 支払い完了

このような場合はフェーズにわけて、各フェーズごとに二次情報を次のフェーズに渡すようにします。
今回の例ならば、支払いフェーズと配送フェーズに分割し、支払いフェーズは「支払い完了 or 未完了」という二次情報を配送フェーズに渡せば良いのです。
これによって支払いの一次情報の多さを隠蔽しながら、次のフェーズに必要なだけの情報を与えられます。

今回はフェーズで分けましたが、分割の単位がフェーズである必要性はないです。分割単位はシステムでもコンポーネントでもモジュールでもクラスでも関数でもいいです。必要なことは、各分割単位の粒度に合わせながら、複雑な状態管理の一次情報そのままではなくて利用に便利な二次情報に変換して、それを渡すということです。

境界づけられたコンテキストと二次情報

もしも二次情報が一次情報となる境界がマイクロサービスの間にあったとしたら、その差は境界づけられたコンテキストを浮き彫りにします。

境界づけられたコンテキストの存在を匂わす要素は多くありますが、あるシステムの二次情報を一次情報として扱って良いことはそのひとつです。
二次情報を一次情報にするとは、一次情報を捨象するということです。情報量が減るから普通は嬉しくないはずです。しかし、コンテキストが変わる場合は情報が減った方が嬉しいことがしばしばあります。

例えば、あるシステムを開発し、それを営業が売り、その売り上げを会計が計上します。ここで三者が扱う情報について整理しましょう。

  • 開発: 顧客のための機能追加をしており、hogeメソッドがどのぐらい完成してfooメソッドが完成してbarメソッドが未着手という情報がある
  • 営業: 顧客にどのぐらいで何が完成するか伝える必要がある。顧客に進捗を伝えるときに「hogeメソッドがどのぐらい完成してfooメソッドが完成してbarメソッドが未着手」と説明することはない
  • 会計: だれに何がどれだけ売れたかを知りたい。個々の機能開発の進捗を知る必要がない

もしも一次情報が二次情報に捨象されないと、経理に「hogeメソッドがどのぐらい完成してfooメソッドが完成してbarメソッドが未着手」という情報が渡されることになります。これは明らかに不要な情報が含まれています。
このように二次情報として捨象されることは別のコンテキストにとって意味があります。そのため、二次情報の捨象が求められることは境界づけられたコンテキストを示すひとつの手がかりとなります。

まとめ

この記事では、データの状態管理をシンプルかつ効果的に行うための方法について解説しました。

MECEの原則を適用することで、複雑な状態管理を整理し、明確な分類が可能になります。これにより、システムの拡張性と保守性を向上させることができます。
また、一次情報と二次情報の概念を導入し、分析を高度化するとともに、システム間のデータ連携や情報管理も整理しました。
これらの方法を実践することで、複雑な状態管理をシンプルかつ拡張性のあるものに変えることができたら嬉しいです。この記事が、皆さんのシステム設計や開発に役立つことを願っています。

あと、一次情報と二次情報のようなアイディアは先人がたくさんいる気もするので、先人の事例や研究があったら教えて欲しいです。[3]

脚注
  1. ここではMECEの表を元にビジネスサイドと会話していることに注目して欲しいです。仕様を表現した図表をもとに開発サイドとビジネスサイドの交流を促しています。これはDDDの目指すモノの一つであることから、このMECEの表もドメインモデルと呼べるかもしれないです。 ↩︎

  2. おそらくですが、テスト設計にも関連ありそうだと考えています。一次元は境界値分析ですし、三次元以上はデシジョンテーブルや遷移表と関連があると思います。 ↩︎

  3. 一次情報と二次情報のようなアイディアはRDBの理論である関数従属性に似ていますが、異なる点もあります。関数従属性は情報の特定に関する理論です。一次情報と二次情報の関係は確かにそのような点があります。一方で、一次情報の扱いが異なります。本記事の一次情報とは外部から与えられた情報のことであり、関数従属とは異なるアイディアです。 ↩︎

レバテック開発部

Discussion