🚀

【オブジェクト指向】クラス設計における重要な設計原則について本気で調べた

2021/02/16に公開

概要

実務でオブジェクト指向プログラミングでWebアプリケーションを開発しているが、オブジェクト指向についてフワッとした理解だった(というかちゃんと理解できてなかった)ので本気で調べてみました。

この記事ではその中でもクラス設計における重要な原則についてまとめます。

※色々調べていく内に分かったのですが、この記事で説明する設計原則はクラス設計に特化したものだけでなくソフトウェアに設計(開発)に重要な原則もあります。また言語に依存しない内容なのでこの機会にしっかり調べてよかったなと思いました。

クラス設計の立ち位置

Webアプリケーションの開発工程は下表の通りで、中でもクラス設計はプログラム内部の設計をする詳細設計に当たります。

工程 概要
要件定義 アプリ開発の目的、アプリに必要な機能の明確化
基本設計 外部設計とも言う。画面設計、機能設計を行う
詳細設計 内部設計とも言う。どのようにプログラムを組むかを確定する
開発 設計された内容を忠実にプログラムとして実現する
テスト 取りこぼしなく実装できているかテストする
リリース Webアプリ、スマホアプリを公開する

クラス設計における重要な原則

色々調べた結果、大きく分けるとこの3つかなと思います。
(もちろん他にも大事な原則や思想はあると思います)

  • 単一責任原則
  • DRY原則
  • KISS原則

※DRY原則とKISS原則はクラス設計に特化したものではなく、ソフトウェア開発原則です

単一責任原則

クラスを変更する理由は、単一でなければならない

つまり、クラスを変更する理由は1つであるべきという意味。

クラスにおいて責任は「役割」とか「責務」という言い方でも使われることが多いですが、ちょっと疑問なのが単一責任原則が

「クラスの責任(役割) は、単一でなければならない」ではなく「クラスを変更する理由は、単一でなければならない」

と言われていること。。🤔

単一責任、なのに変更理由??

こちらの記事ではこのように書かれていました。

たしかに役割は1つのほうが、見通しがよいかもしれないが、複雑にはなる。変更がとうてい生じないような場所に関して、無駄に分離してコードを複雑にする必要がないという意味を込めたい場合、たしかに「クラスを変更する理由は複数存在してはいけない」のほうがよい言葉だと感じた。

なるほど。
確かに全てのクラスを1つの役割に限定してしまうとクラス数が膨大になり、複雑さは増してしまいますね。

これは後述するKISS原則的には好ましくない。

つまり「変更がとうてい生じないような場所」を見極める経験とか能力が必要ですね。(いやー難しい)

僕もそういう意味では、単一責任の原則なのに「クラスを変更する理由は、単一でなければならない」っていう説明なのはスッと腹落ちします。

ではサンプルコードを例にしてみます。
こちらの記事から流用させていただきますm(__)m ※ちょっと簡易的にするために削ってます)

Employee.php
<?php
class Employee
{
    // プロパティ
    private $code;
    
    // コンストラクタとゲッターは省略

    // CSV出力
    public function exportCsv(){
        $filename = $this->code . '_' . $this->name . '.csv';
        $arr = $this->profile();
        return File::Csv($filename, $arr);
    }
  
   // プロフィール画面出力
    public function profile(){
        return [
            'code' => $this->code;
        ];
    }    
}
  • exportCsvメソッドはCSV出力用メソッド(人事担当者用)
  • profileメソッドは社員のプロフィール画面の出力(一般社員用)
  • profileメソッドはexportCsvメソッド内でも使われる

このクラスで利用者(アクター)が人事担当者、一般社員用の2つ出てくる点、つまり今後修正が出てきそうな責務が複数あるでこの原則に反しています。

じゃあ、ここで「CSVで個人用の電話番号も入れて欲しい!」ってなると以下のコードになりますよね。
CSVに出力する項目はprofileメソッドで定義されているのでprofileに手を加えることになります。

Employee.php
<?php
class Employee
{
    // プロパティ
    private $code;
    private $name;
    private $privateTelNumber;
    
    // コンストラクタとゲッターは省略

    // CSV出力
    public function exportCsv(){
        $filename = $this->code . '_' . $this->name . '.csv';
        $arr = $this->profile();
        return File::Csv($filename, $arr);
    }
  
   // プロフィール画面出力
    public function profile(){
        return [
            'code' => $this->code,
	    // これが要件によってはヤバいバグを生む...
	    'privateTelNumber' => $privateTelNumber,
        ];
    }    
}

これ、一般社員が「個人の電話番号を見てはいけない」という要件だったらセキュリティ面でヤバすぎるバグです。
profileメソッドで見れてしまう)

単一責任の原則にのっとったコード

Employee.php
<?php
class Employee
{
    private $code;
    private $privateTelNumber;
    
    // コンストラクタとゲッターは省略
}
EmployeeCsvPresenter.php
<?php
class EmployeeCsvPresenter
{
    private $employee;
    
    // コンストラクタは省略
    
    public function export()
    {
        $filename = $this->code . '_' . $this->name . '.csv';
        $body =  [
            'code' => $this->code,
            'privateTelNumber' => $this->privateTelNumber,
        ];
        return File::Csv($filename, $body);
    }
}
EmployeeProfilePresenter.php
<?php
class EmployeeProfilePresenter
{
    private $employee;
    
    // コンストラクタは省略
    
    public function profile()
    {
        return [
            'code' => $this->code,
        ];
    }    
}
  • CSV出力:EmployeeCsvPresenterクラス
  • プロフィール画面の出力:EmployeeProfilePresenterクラス

に分離しました。
(コード丸々流用していますが、このコード間違ってない?と思っています...が今は設計原則の話にフォーカスしたいので目を瞑らせてください🙈 )

単一責任の原則にのっとってクラス分けした結果、以下のメリット、デメリットが生じます。

メリット/デメリット 概要
メリット ・影響範囲を少なくできる
→仮にEmployeeCsvPresenterの改修が必要だったとしても、EmployeeProfilePresenterに影響は及ぼさない

・コンフリクトを防げる
→CSV出力機能、プロフィール画面出力機能を同時に改修する必要が生じても
クラスが分離されていることでコンフリクトが発生せず開発効率をあげることができる
デメリット クラスの数が増え複雑性が増す(=シンプルではなくなる)

要はバランスってことですね。

DRY原則

Don't Repeat Yourselfの略で

重複を避けよ

という意味。

コードの重複があると、1つの箇所の変更が生じた場合、重複している箇所を全て修正しなければなりません。

規模が小さい開発で2、3箇所の重複ならまだ大丈夫かもしれませんが、重複箇所が10箇所とかそれ以上に増えてしまうとどこかのタイミングで絶対に修正漏れが生じますよね。

これがクリティカルバグに繋がる可能性もありますもんね...😱

プログラム上のコードはできるだけ重複を避けることが大切ですよね。

クラス設計においてこのDRY原則にのっとると適切な継承クラスの分離などの作業が挙げられます。

と、ここまで調べながら書いてて、なるほどなあ〜と思っていたんですが...

DRY原則とはコードを重複させない

というのはどうやら誤認らしいです!!

こちらの記事にはこのように書かれています。

DRYは、単に「コードを重複させない」という原則ではなく、DBスキーマ、テスト、ビルドシステム、ドキュメントなども対象になっており、「ソフトウェア開発全体において情報を重複させない」という原則

つまり、正しくは

もちろんコードもだけどコードに限らずソフトウェアの開発を構成するものはなるべく重複は避けましょう

ってことですね。

※同じコードを何度も書くんじゃねえよ!っていう原則はOAOO原則というのがあります。

KISS原則

Keep it simple, stupid!の略で「シンプルにしておけ!」という意味。
(stupidの意味を調べたら「愚か」「間抜け」「つまらない」とかが出てきたのでぜひシンプルにしておきましょう😇 w)

シンプルにしておけ!この間抜け

という意味ですね。

よくコードレビューでのコメントでも「こう書くともっとスッキリ書けるよ!」とかあると思いますが、基本的にはプログラムはシンプル(=可読性が高い)であることが美徳とされています。

またプログラムを組む上で最も重要なことです。

と書きながらも、このKISS原則はある程度、「単一責任原則」とはトレードオフになります。

単一責任原則を重視すればするほど、(当たり前ですが)クラス数が増えていき、シンプルなプログラムとは言えなくなります。

「単一責任原則」と「KISS原則」の重視度合いがどちらよりになるかはケースバイケースになるみたい...
(結局は実践を積まないとですね...)

その開発チームのリーダーやメンバーの好みによるみたいですが、こういう設計思想を把握しておくだけでもクラス設計に携わるときに「この開発は単一責任原則を結構重視するな〜」とかがわかるかもしれないですね。

僕がここまで調べた結果、KISS原則を極力重視しつつ、可能な範囲で単一責任原則を取り入れるのが良いのかななんて思っています。
(まだ1からクラス設計に関わったことがないのでこの考えは変わるかもしれませんが)

※また、受託開発とかでクライアントの要望を全て盛り込んだ結果、システムが複雑になり処理速度が重くなってしまうケースもこの原則に反することになります。大抵のことは技術的に可能ですが、それを実際に実装すべきなのかどうかは別物として切り離して考える必要がありますね。つまり、「この機能は実装しようと思えばできますが、実装することによって、◯◯とか△△とかのデメリットがありますよ」と先を見越した提案とかができないといけないなと思いました。

まとめ

ソフトウェアの設計(開発)原則はたくさんあるけど、中にはトレードオフなものもあります。

  • どの原則をどこまで重視するのか
  • 原則のデメリットをどこまで許容できるか
  • そのクラスは今後変更の可能性が大きいのか小さいのか

この辺りを考えながらクラス設計する必要があるし、ここが難しいんだろうなと感じました。

参考

Discussion