Skip to content

設計がユーザーに価値を届ける

Published:  at  02:32

Table of Contents

Open Table of Contents

はじめに

どうもこんにちは。

日々ソフトウェア開発の現場であるべき世界を追求している中で、 ソフトウェア設計がユーザーに価値を届けることに直結しているんだということについて、最近改めて考えることがありましたので、思考を構造化して整理しました。 開発者やEM、PM、QA等の皆さんが改めて私の考えを元に、何か新しい気づきを得ていただけたら嬉しいと思っています。

オブジェクト指向の前

C言語には構造体と呼ばれる、複数の変数をまとめることができる入れ物と、独自の型を定義する機能があります。 C言語以外にも、C++ や Go などの言語には構造体が存在するので、多くの開発者が構造体に触れたことがあるのではないかなと思います。 この構造体と、その構造体で定義されているいくつかの変数を操作する関数を組み合わせて、抽象データ型と呼んだりします。 構造体は以下のような感じです。

#include <stdbool.h>

typedef struct {
    int balance;
} BankAccount;

void deposit(BankAccount *account, int amount) {
    account->balance += amount;
}

bool withdraw(BankAccount *account, int amount) {
    if (amount > account->balance) {
        return false;
    }

    account->balance -= amount;
    return true;
}

抽象データ型の重要な点は、データと、それを操作する関数を近くに置くことによって、開発者に分かりやすいインターフェースを提供しようとした、というところだと思います。 この点は、これから述べるオブジェクト指向における設計でも非常に重要なポイントになるので、覚えておいてください。

ただし、この抽象データ型には以下のような問題がありました。

  • データと関数が分離しているため、データの操作方法が分かりにくい
  • データの操作方法が変わると、関数の実装も変わるため、保守性が低い
  • 外からデータを直接操作できてしまうため、データの整合性が保たれない

そのため、抽象データ型を大幅に拡張する概念として、クラスが登場しました。 よく入門書などで語られているクラス、およびオブジェクト指向の説明として、りんごが〜とか、キリンが〜とか、そういった現実世界のモノや概念をクラスとして投影できるといった説明があります。 皆さんも最初にオブジェクト指向に触れる際に読んだ覚えがあるのではないかと思いますが、個人的にはよく分からない説明だなと思っています。 そういったフワッとした説明よりももっと実用的な部分にフォーカスした説明の方が個人的には分かりやすいと考えていて、 クラスはデータと関数をまとめることができる入れ物であり、データの操作方法を隠蔽することができる、という説明の方がしっくりきます。 抽象データ型の延長線上にある概念としてクラスをとらえるということです。

オブジェクト指向とSOLID原則

クラスには、よく知られた3つの重要な特徴があると思います。カプセル化、継承、ポリモーフィズムですよね。 これらの特徴を活用することで、オブジェクト指向によるソフトウェア設計がシステム開発において主流となりました。

しかし、人々はこれらの道具を手にしたからといって、それらを有効に使いこなせるわけではありませんでした。 C言語由来の手続型プログラミングの考え方が根強く残っていたため、データとそれを操作する関数を近い場所に置く考え方や、継承・ポリモーフィズムを適切に活用して、システム開発につなげることができませんでした。 そういった状況に対して、オブジェクト指向の設計原則として出現したのがSOLID原則です。

SOLID原則は、オブジェクト指向の設計原則として、以下の5つの原則を指します。

  • S: 単一責任の原則 (Single Responsibility Principle)
  • O: 開放/閉鎖の原則 (Open/Closed Principle)
  • L: リスコフの置換原則 (Liskov Substitution Principle)
  • I: インターフェース分離の原則 (Interface Segregation Principle)
  • D: 依存関係逆転の原則 (Dependency Inversion Principle)

このうち、今回は特に重要な単一責任の原則と、依存性逆転の原則について説明します。

単一責任の原則

単一責任の原則とは、クラスは変更される理由を一つだけ持つべきである、という原則です。ここでいう責任とは、単にメソッドが一つであることではなく、同じ理由で変更される処理のまとまりを指します。

例えば、注文の合計金額を計算する処理、請求書を PDF として出力する処理、注文完了メールを送る処理を一つのクラスに集めてしまうと、次のようになります。

// 悪い例:価格計算・請求書作成・メール送信という異なる責務を一つのクラスに持たせている
final class OrderService
{
    /**
     * 注文金額を計算する
     *
     * 悪い点:価格ルールが変わるたびに、このクラスを変更する必要がある
     *
     * @param list<int> $itemPrices 商品ごとの価格一覧
     * @return int 注文の合計金額
     */
    public function calculateTotal(array $itemPrices): int
    {
        return array_sum($itemPrices);
    }

    /**
     * 請求書の PDF を生成する
     *
     * 悪い点:帳票の見た目や PDF ライブラリの変更も、このクラスの変更理由になる
     *
     * @param array{orderNumber: string, total: int} $order 注文番号と合計金額
     * @return string 生成した PDF のファイルパス
     */
    public function generateInvoicePdf(array $order): string
    {
        // PDF ライブラリを使って請求書を生成する
        return 'invoice.pdf';
    }

    /**
     * 注文完了メールを送信する
     *
     * 悪い点:メール文面や送信方法の変更まで、このクラスに影響する
     *
     * @param string $emailAddress 送信先のメールアドレス
     * @param string $orderNumber 注文番号
     */
    public function sendConfirmationEmail(string $emailAddress, string $orderNumber): void
    {
        // メール送信サービスを使って注文完了メールを送る
    }
}

この OrderService は、価格ルール、帳票の見た目、メールの文面という別々の理由で変更する必要があります。 このようなクラスは多くの関心事を抱えるため、変更のたびに意図しない箇所を壊しやすくなります。

責任ごとにクラスを分ければ、それぞれの変更は必要な箇所に閉じ込められます。

// 改善後:変更理由ごとにクラスを分け、それぞれが一つの責務だけを持つようにする
final class OrderTotalCalculator
{
    /**
     * 注文金額を計算する
     *
     * 変更点:価格ルールに関する変更を、このクラスだけに閉じ込める
     *
     * @param list<int> $itemPrices 商品ごとの価格一覧
     * @return int 注文の合計金額
     */
    public function calculate(array $itemPrices): int
    {
        return array_sum($itemPrices);
    }
}

final class InvoicePdfGenerator
{
    /**
     * 請求書の PDF を生成する
     *
     * 変更点:帳票の見た目や PDF ライブラリに関する変更を、このクラスだけに閉じ込める
     *
     * @param array{orderNumber: string, total: int} $order 注文番号と合計金額
     * @return string 生成した PDF のファイルパス
     */
    public function generate(array $order): string
    {
        // PDF ライブラリを使って請求書を生成する
        return 'invoice.pdf';
    }
}

final class OrderConfirmationMailer
{
    /**
     * 注文完了メールを送信する
     *
     * 変更点:メール文面や送信方法に関する変更を、このクラスだけに閉じ込める
     *
     * @param string $emailAddress 送信先のメールアドレス
     * @param string $orderNumber 注文番号
     */
    public function send(string $emailAddress, string $orderNumber): void
    {
        // メール送信サービスを使って注文完了メールを送る
    }
}

このように分けると、価格ルールの変更は OrderTotalCalculator、請求書の変更は InvoicePdfGenerator、メールの変更は OrderConfirmationMailer に閉じ込められます。結果として、クラスを利用する側が依存しなければならない機能を減らし、変更の影響範囲を小さくできます。

依存性逆転原則

依存性逆転の原則とは、高水準の方針と低水準の詳細のどちらも、具体的な実装ではなく抽象に依存させる、という原則です。高水準の方針とは「注文を受け付ける」「支払いを確定する」といった、システムの中心となる業務ルールを指します。一方、データベースや外部決済サービス、メール送信サービスなどは、それを実現するための詳細です。

クラスAがクラスBを直接利用する場合、依存関係は次のようになります。

flowchart LR
    classA[クラスA] -->|依存する| classB[クラスB]

この状態では、クラスAはクラスBのインターフェースに依存しています。そのため、クラスBのインターフェースや利用方法が変わると、クラスAにも修正が必要になります。また、クラスBの実装詳細の変更であっても、クラスAがクラスBの詳細を知ってしまっている設計では、クラスAに影響が及ぶ可能性があります。

そこで、クラスBが提供する機能を表すインターフェースB’を用意します。クラスAはクラスBではなくインターフェースB’に依存し、クラスBはインターフェースB’を実装します。

flowchart LR
    classA[クラスA] -->|依存する| interfaceB[インターフェースB']
    classB[クラスB] -->|実装する| interfaceB

このように クラスA → インターフェースB' ← クラスB という関係にすると、クラスAが知る必要があるのはインターフェースB’だけになります。インターフェースB’を変更しない限り、クラスBの内部アルゴリズムや利用するライブラリなど、実装詳細をどれだけ変更してもクラスAには影響しません。

たとえば注文を確定する処理が「決済を実行できる」というインターフェースに依存し、各決済サービスがそのインターフェースを実装するようにすれば、業務ルールは具体的な決済サービスの詳細を知らずに済みます。決済手段の追加・差し替えやテスト用実装への置き換えを、中心的な処理への影響を抑えながら行えるようになります。

依存関係とシステムの変更に伴う影響範囲

単一責任の原則と依存性逆転の原則に触れたところで、改めてクラス同士の依存関係について考えてみましょう。

flowchart LR
    classA[クラスA] -->|依存する| classB[クラスB]

クラスAとクラスBがあるとします。クラスAはクラスBの機能を利用するために、クラスBのインターフェース(API)を参照します。 このとき、クラスBのインターフェースに変更が加えられたとします。クラスBのインターフェースが変更されると、クラスAの実装にも影響が及びます。 一方で、クラスAの実装が変更されても、クラスBの実装には影響が及びません。

つまり、依存方向によってソフトウェアの変更に伴う影響範囲が変わる、ということです。 今回の場合、クラスBを変更することでクラスAにも影響範囲が及んでいるため、クラスAの壊れやすさがクラスBの変更にも依存しているという構造になっています。 今回はクラスAとクラスBという一対一の関係で依存関係を見てきましたが、実際のシステムでは非常に多くのクラスが実装されていることと思います。非常に多岐に渡るクラス同士の依存関係が存在するはずです。

flowchart LR
    classA[クラスA] --> classB[クラスB]
    classA --> classC[クラスC]
    classB --> classD[クラスD]
    classB --> classE[クラスE]
    classC --> classE
    classC --> classF[クラスF]
    classD --> classF
    classE --> classG[クラスG]
    classF --> classG
    classG --> classB

ここで、先ほど触れた単一責任の原則と依存性逆転の原則を思い出してください。 単一責任の原則は、クラスはたった1つだけ責務を持つべきである、という原則です。クラスに複数の責務がある場合、必要以上に多くのクラスによって依存されることになり、少しの変更で多くの依存元に影響を及ぼしてしまうことになります。 クラスの責務を一つに限定することによって、不要な依存関係を減らし、変更の影響範囲を最小限にすることができます。

一方で、依存性逆転の原則は、間に抽象(インターフェース)を挟むことによって、依存関係の方向を逆転させるという原則です。 依存の方向を逆転させることによって、影響を及ぼしたくないシステムのコアとなる処理の変更が、他のクラスに影響を与えないようにすることができます。

インターフェース設計が重要である

これまで述べたような依存関係をコントロールする上で、インターフェース設計が非常に重要です。 インターフェース設計が適切でない場合、割れ窓理論的に、知らず知らずのうちに同じような関数を別の場所に実装してしまったり、複雑度の高い設計になってしまったりします。 では良いインターフェース設計とはどういった設計でしょうか?

良いインターフェース設計というのは、実装詳細がうまく隠蔽されており、利用者にとっては適切な入力値を入れれば、中で何が行われているかの詳細は気にしなくても、適切な出力が出てくる、ということです。 例えば、洗濯機を考えてみてください。皆さんは洗濯機の仕組みを完全に理解した状態で洗濯機を日々使っているでしょうか? もしかしたら一部の人は本当に理解しているかもしれません(!)。しかし、ほとんどの人は洗濯機の仕組みを理解していなくても、簡単に洗濯機を使うことができると思います。 内部の仕組みを理解していなくても、洗濯物と洗剤、柔軟剤を入れて、適切なボタンを押せば、数十分間待機したのちに洗濯物が出力されるわけです。 良いインターフェース設計とはまさに、この洗濯機の例のように、利用者が内部の仕組みを理解していなくても、適切な入力を行えば適切な出力が期待できる、という状態を達成するような設計のことです。

例えば以下のようなインターフェースを考えてみましょう。

// 悪い例:名前と引数から何をする処理なのか分かりにくく、DB 接続の詳細まで呼び出し側に要求している
final class Service
{
    /**
     * 何らかの処理を行う
     *
     * @param array<string, mixed> $arg1 処理に必要なデータ
     * @param string $arg2 接続先の情報
     * @return array<string, mixed> 処理結果
     */
    public function execute(array $arg1, string $arg2): array
    {
        // DB 接続や SQL の詳細を呼び出し側から受け取っている
        $pdo = new PDO($arg2);
        $statement = $pdo->prepare('UPDATE orders SET status = :status WHERE id = :id');
        $statement->execute([
            'id' => $arg1['id'],
            'status' => $arg1['status'],
        ]);

        return ['result' => true];
    }
}

これは設計として良くありません。このクラスやメソッドの利用者(依存する側)にとって、それらが内部で何をしているか実装詳細を知っていないと使えない状態になってしまっています。 利用者は本来内部実装を知る必要はないはずです。

では、これを良いインターフェース設計に改善してみましょう。

final readonly class OrderId
{
    /**
     * @param non-empty-string $value 注文を一意に識別する値
     */
    public function __construct(public string $value)
    {
    }
}

final readonly class PaymentMethod
{
    /**
     * @param non-empty-string $value 利用する支払い方法を表す値
     */
    public function __construct(public string $value)
    {
    }
}

final readonly class PaymentResult
{
    /**
     * @param non-empty-string $transactionId 決済を識別する取引ID
     */
    public function __construct(public string $transactionId)
    {
    }
}

// 改善後:注文の支払いを確定するという業務上の操作だけを公開する
final class OrderPaymentService
{
    /**
     * 指定した注文の支払いを確定する
     *
     * 呼び出し側は DB 接続や外部決済サービスなどの実装詳細を知る必要がない
     *
     * @param OrderId $orderId 支払いを確定する注文のID
     * @param PaymentMethod $paymentMethod 注文に使用する支払い方法
     * @return PaymentResult 確定した支払いの結果
     */
    public function confirm(OrderId $orderId, PaymentMethod $paymentMethod): PaymentResult
    {
        // 決済処理やデータ更新の詳細はこのクラスの内部に隠蔽する
        return new PaymentResult('transaction-id');
    }
}

利用者は、明瞭なクラス・関数名と引数によって、このクラスやメソッドが何をしているのか、何を入力したら何が出力されるのかが明確に理解できるようになりました! このインターフェース設計は、ソフトウェア設計のありとあらゆる場面で本質的に重要なポイントだと思います。 クラスやメソッドのインターフェースから、Web API のリクエスト・レスポンスの設計、DB のテーブル設計、フロントエンドのコンポーネント設計まで、あらゆる場面で今まで述べたインターフェース設計が設計の本質になっていると思います。

ドメインの言葉でインターフェースを表現すること

世の中のシステムには、常に解決すべき課題が存在しています。例えば、会計処理だったり、商品の受発注だったり、決済だったり、様々な課題解決のためにシステムは生み出されています。 そのようなシステムが解決すべき問題領域をドメインと言います。

その時、ドメインの言葉でインターフェースを表現することが、良いインターフェース設計のために重要です。 先ほどの例を思い出してください。明瞭なクラス・関数名と引数によって、何をしていて何を入力したら何が出力されるのか明確に理解できるようになったはずです。 開発者は多くの時間をコードを読むことに捧げています。今まで述べたことを体現するためには、対象のドメインのモデル(世界)をシステムに投影することが大切です。

そのためには、開発チーム自身がそのシステムやプロダクトのドメインやビジネスを一番に理解している必要があります。 ドメインの言葉でインターフェースを表現することによって、開発者がそのシステムやプロダクトのドメインやビジネスを実行できるコードとして表現することができるようになります。 ただし、ソフトウェア開発だけでも多くの考えるべきことがあるのに、さらにドメインにも一番に詳しくなるというのはどうにも実現は難しいです。

なので現実的には、PO(プロダクトオーナー)やPM、事業責任者との密なコミュニケーションによって、自己文書化されたコードに落とし込んでいく必要があります。 自己文書化されたコードとは、コードを読めばそのシステムやプロダクトのドメインやビジネスが理解できるようなコードのことです。

Dev と Biz が二人三脚で開発していく体制へ

では、こういった Dev と Biz が密に連携を取りながら開発していくにはどうすれば良いでしょうか? ウォーターフォールのように企画と開発にコミュニケーション上の壁がある受発注の関係では、そういった世界を目指していくことはできません。

我々は良いソフトウェア設計を行うことによって、最終的に何を得ることができるでしょうか? 依存関係が適切にコントロールされた設計を行うことは、開発にかかるリードタイムを最小限にして、最短でリリースできる世界を得ることができます。 より短いサイクルで仮説検証を繰り返しながら、ユーザーや顧客に届けたい機能を素早く提供することができるようになるわけです。

そこで、アジャイル開発が出てきます。 アジャイル開発とは、開発チームとビジネスチームが二人三脚で密にコミュニケーションを取りながら、短い期間でリリースを繰り返して、ユーザーに価値が届けられているのかどうかの仮説検証を繰り返していく開発手法です。 素早く価値を届けるためには、開発チームがビジネスを理解し、ビジネスチームが開発を理解しながら、取り組んでいかなければなりません。

良いインターフェース設計は、開発チームがビジネスを理解するための重要な手段の一つです。 つまり、良い設計を行うことと、ユーザーに価値を届けることは、密接に関係している、ということが言えると思います。

ソフトウェア設計とユーザーに価値を届けることの関係

これまでの説明を振り返ってみましょう。 最初は構造体の話から始まり、オブジェクト指向のクラス、SOLID原則、依存関係、インターフェース設計、アジャイル開発と話が展開していきました。 最初はコーディングの話をしていたのに、気づけば組織やユーザーに価値を届けることにまで話が広がっていきました。

これは一体どういうことでしょうか?どういったことを意味しているのでしょうか?

つまり、ソフトウェア設計からアジャイル開発、そしてユーザーに価値を届けることまで、全てのソフトウェア開発の営みが一本のレールの上につながっている、ということなんです。 皆さんがソフトウェアエンジニアとして開発を行ってきた中で、もしかしたらプログラミングの話、DDDやClean Architectureのような設計の話、アジャイルやスクラムといった開発手法の話を全て別々の概念として学んだかもしれません。 これらの概念は全く別々の概念の形をしているので、それらに関連性を見出すことは難しいかもしれません。 しかし、今までの説明を通して、それらが連続した概念なんだ、ということが何となく理解できたのではないでしょうか。

人、組織、システムの3つを同時に変えること

そう考えると、ソフトウェア開発を上流工程と下流工程に分けて考えることが如何にナンセンスか、と思いませんか? よく上流工程はPMがやって、下流工程は外注としてPGに流す、と、上流工程と下流工程という分断された世界観で日本のソフトウェア開発は語られます。 しかし、上流工程と下流工程を分けることなんて本来できないはずです。 なぜなら、コードの設計が最終的にユーザーに価値を素早く届ける、そして最終的にビジネスの収益向上に繋がっている以上、下流(とあえて言いますが)の設計がボトムアップで上流にまで影響を及ぼすはずです。 なので、最上流から最下流まで、一つの開発チームが一気通貫で担当することが最適なソフトウェア開発のあり方だと思います。

このようにして見ていくと、人、組織、システム、この3つを同時に変えていくことが必要だ、ということが見えてきます。 システムだけを変えようとしても、組織構造が伴わなければ本当の意味で良いシステムを追求していくことはできません。 一方で、組織構造だけを変えても、システムの形が組織のあり方に適合していなければ、うまく相乗効果を生み出していくことはできません。 また、組織とシステムを同時に変えていこうと思ったなら、それを達成する人を採用・育成していくことが不可欠です。

最後はすごく抽象的な話になってしまいましたが、一つでも多くのこういった世界観を満たしていくために、我々はソフトウェア設計を通して変革していかなければなりません。