【関心の分離/責務の分離】
変更に強いプログラミング実践手法

記事イメージ


(最終更新日:

プログラミングにおける「関心の分離」(責務の分離、Separation Of Concerns/SOC)は、品質と保守性を高める上で重要な考え方です。しかしながら初心者にとっては理解しにくく、シニアクラスでも考慮が不十分なコードを書いている場合も見かけます。

今回は、より理解が進むように、例を用いて実践方法を解説明してみようと思います。

関心が分離されたコードとは。

関心(関心事/かんしんじ)とは、プログラムで処理したいタスクや役割などを指します。

そして関心の分離ができているコードとは、おおむね次のようなものを指します。

関心が分離されたコードとは。
  • 関数やクラスが一つの関心(目的・責務・役割)に専念している。複数の関心を一つの処理に混ぜていない。
  • 処理に関心事以外の副作用がない。
  • クラスや関数の命名が関心(目的・責務・役割)を適切に表している。

尚、コードの長短に関しては、本質的には関心の分離とは無関係です。

例え千行、万行の量があろうとも、一つの関心に専念している限りそれは優秀なコードである可能性は十分あります。

関心の分離ができているコードは理解しやすく変更に強い

関心の分離の核心は「整理整頓」です。

何がどこにあるか、コードを適切に分けて配置することで、後から読む人が理解しやすく、局所的な修正をしやすくすることができます。

なぜ変更に強いコードが必要か?→そのプログラムを使い続ける限り、変更は必ず起きる。

ソフトウェアが完成後に微調整されること、すなわちプログラムにとって重要なファクタ・不要な機能が完成後に見つかることが多いという事実は、ソフトウェアエンジニアリングの世界に広く知られた経験則です。

ソフトウェア工学における有名著書、いわゆるエヴァンス本にも次のようにプログラムの変更の可能性について言及されています。

重大な発見はいつでも、設計や実装をするために努力する際に現れるからだ。非常に特殊で予期せぬ問題というものが常に発生する。あらかじめ作られるモデルは、的外れな対象を掘り下げる一方で、重要な問題を見過ごしてしまうのである。

引用:エリック・エヴァンスのドメイン駆動設計 第3章

したがって、中・長期にわたり使い続けるプログラムであれば、必ず変更可能性に考慮して実装するべきであり、その基本である「関心の分離」は緻密に実行される必要があるのです。

「関心の分離」の例示:関数編

例えばA,B,Cという機能をもつソフトウェアで、Bの機能だけを修正するケースを考えます。

関心の分離がされていないコードは、A,B,Cの機能を一つの関数に放り込み、順番に処理しています。


//関心の分離ができていないケース

//A,B,Cの処理が一個の関数に入っている。
//処理Bだけ直したつもりでも、AやCに影響が出る可能性が高い。
void abc() {
    //Aの処理   
    //Bの処理
    //Cの処理
}

この場合、Bの処理を直すにはAの結果や変数定義の状態を受け継ぎ、Cに対して結果や変数定義を引き継ぐことになります。

なので、変更する時にはAとCに影響が出ないように注意する必要があります。本来Bの変更に専念すれば良いところ、これではAとCに新たにバグを埋め込むリスクを生じさせてしまうのです。

このような余計なリスクを防ぐために、関心ごとに関数(クラス)を分離します。分離すると次のようなコードになります。


//関心の分離ができているケース

//処理A,B,Cは個別の関数に格納されている。
//処理Bを直したい場合、b関数だけ修正すればよく処理AとCには影響しない。

void a() {
    //Aの処理
}
void b() {
    //Bの処理
}
void c() {
    //Cの処理
}

このようなコードであれば、A,B,Cの処理は独立しており、b関数の改修によるaとcへのバグ埋め込みリスクはかなり低く抑えることができます。

ソフトウェアの変更は、そのソフトウェアを使い続ける限り必ずおこります。変更に対する対応力を高めておくことは保守面で重要と言えます。

「関心の分離」の例示:クラス編

オブジェクト指向プログラミングをサポートする開発言語では、クラスについても関心ごとに分離することを意識する必要があります。

例えば、「財務会計計算」と「管理会計計算」の機能を持つ財務分析システムを考えます。「財務会計」と「管理会計」はいずれも会計に関する計算をすることには変わりありませんが、その目的は大きく異なりますので、クラスを別にして実装することが基本指針となります。

この例を踏まえ、関心の分離ができている例、できていない例を具体的に図示してみます。

【NG】関心の分離ができておらず、一つのクラスに混ぜて実装されているケース

実装全体

会計計算クラス(AccountingCalculationクラス)


class AccountingCalculation {
    public bool IsManagementAccounting { get; set; }
    
    //コンストラクタ引数isManagementAccountingが
    //trueであれば管理会計計算、falseであれば財務会計計算を行う
    public AccountingCalculation(bool isManagementAccounting) {
        this.IsManagementAccounting = isManagementAccounting;
    }
    
    //actual変数をベースに会計計算を行う関数
    int RunCalculation(List actual) {
        if (this.IsManagementAccounting) {
            //管理会計計算
        } else {
            //財務会計計算
        }
    }
}

プロパティを共有しており、コンストラクタ引数で財務会計・管理会計を切り替える実装。
この実装の場合、例えば管理会計だけを修正しようと思った場合、財務会計のほうにバグを新たに発生させてしまう可能性があり、改修の難易度が高い。


【OK】関心の分離を行い、クラスを分けて実装されているケース

実装全体

財務会計計算クラス(FinancialAccountingCalculationクラス)


class FinancialAccountingCalculation {    
    //actual変数をベースに会計計算を行う関数
    //このクラスでは財務会計計算のみを行う。
    int RunCalculation(List actual) {
        //財務会計計算
    }
}

管理会計計算クラス(ManagementAccountingCalculationクラス)


class FinancialAccountingCalculation {    
    //actual変数をベースに会計計算を行う関数
    //このクラスでは管理会計計算のみを行う。
    int RunCalculation(List actual) {
        //管理会計計算
    }
}

クラス分けを行い実装をした例。
この場合であれば、管理会計の修正はManagementAccountingCalculationクラスのみを変更すればよく、財務会計計算側にバグを埋め込む可能性を低く抑えられる

後者の例のほうが、後からの変更に強いプログラムを作成できています。

このような分け方をできることがオブジェクト指向プログラミングの一つの強みと言えます。オブジェクト指向については、関連記事を記載してありますので是非合わせてご参照ください。

関連記事:【事例にみるオブジェクト指向プログラミングの利点】カップの中身の判定、満たす処理は、誰に関連した機能?

コードの副作用は、処理の理解を阻害し、バグのリスクを高める

コードの副作用は、その処理の目的以外の値をついでに変えてしまうという点で、「関心の分離」の失敗の類型と言えます。

今回は例として、FinancialCalculationという整数リストを引数に取り独自の財務計算を行い結果を整数で返す関数を題材に、事例を見ていきます。

題材とする関数。材料を引数で与え、計算結果を得る目的です。
題材とする関数

副作用の事例①:引数が変化する

関数を実行すると引数が変化する、といった事例。

FinancialCalculation関数の中で引数インスタンスに対してAdd(push)を実行するとこのような結果となります。


//関心の分離が出来ていない事例
var actual = [3, 5, 10];
var result = FinancialCalculation(actual);
print(actual); // [3, 5, 10, 15] ←リストを引数に指定したら要素が変化した!

FinancialCalculation(財務計算)という名前からは、引数が変化するということは想像できません。

もしこのように元のリストを変更したい場合、次のコードのようにリストを変化させる関数を別途用意し、FinancialCalculationは結果を返すことに専念するべきです。


//関心を分離したコード
var actual = [3, 5, 10];
var result = FinancialCalculation(actual);
print(actual); // [3, 5, 10] ←変化していない

var actual = ModifyActualList(actual);
print(actual); // [3, 5, 10, 15] ←ModifyActualList関数によってリスト要素が変化した

副作用の事例②:計算用関数を実行すると、インスタンスのプロパティやDBの値が更新される

「計算」と名の付く関数は、実行側は引数を入れて戻り値で結果を得ることを期待しており、これが関心です。

これにもかかわらず、関数を実行することによってプロパティの値を変化させたりDBにアクセスしたりするのは想定外のバグの要因となります。関数が数百~数千行規模となってくると尚更です。

コードの量が多少増えたとしても、プロパティの変更やDBの参照・変更は、しかるべき場所や名前を付けた関数の中で、別に実行するべきなのです。

NG例。関心の範囲外の処理を実装してしまうと、バグの要因となります。(処理のどこで値が変化するのかがわかりにくいため)
NG例

ここで注意していただきたいのは、今回は計算用関数を論じているのでDBやプロパティ操作が好ましくない、という話であって、関数からDBやプロパティの操作をすることが常にNGという意味ではありません。

そういった処理を実装するのであれば、そのための適切な関数やクラスを別途用意するべき、ということです。(下図参照)

OK例。関心に応じて処理を分離した実装に修正した。
OK例

副作用の事例③:プロパティのGetter/Setter内で別のプロパティを変更する

プロパティのGetter/Setterは、開発者は専らそのプロパティに対して参照や変更を意図してアクセスするはずです。

したがって、その処理最中に別のプロパティに副作用を及ぼすと、「まさかプロパティ値を取っただけで他のプロパティが変化するとは」という想定外の事態を招きます。

一人でコーディングしているプログラムならまだ良いですが、チームで開発している場合などはこの様な実装は混乱をきたす可能性が高いので避けるべきです。


//NG実装例
class AntiSoc {

    //PropertyAの値を取る・変更したつもりが、PropertyBの値も変えてしまう実装
    public string? PropertyA {
        get {
            this.PropertyB = "値が変ったよ";
            return _propertyA;
        }
        set {
            this.PropertyB = "値が変ったよ";
            _propertyA = value;
        }
    }
    string? _propertyA;

    public string? PropertyB { get; set; }
}

関心の分離を行った際は、クラスや関数に適切な命名を。

分離を行うと、各クラスや各関数は「何をどうする」といった役割に整頓できるはずです。このようなパーツ群に対して、その内容が理解できるように適切な名前を付けることは、後から自分が、あるいはチームメンバーが理解を行う上で重要です。

NGな例として、Caluといった関数名を付けると何の計算なのか不明ですし、getDataなどとつければ何のデータなのかが不明です。

コメントで補完するといった手も考えられますが、効率性などを考えるとやはり命名により役割を示すのがベストプラクティスといえます。

(注意点)多くの関心事は階層構造を持ち得る

厄介なことに、多くの関心事は階層構造になっています。このことを意識しないと、ここまで述べたようにメソッドやクラスをきちんと分割しても努力が水泡に帰す可能性があります。

すなわち、今回の財務計算モデルのようなタスクであれば、「財務の状況を知りたい」→「財務状況のうち資金繰りの適正さを知りたい」→「資金繰りに含まれる各銀行口座の動向を知りたい」→「毎日の入出金データを取得したい」・・・といった具合に、大きな関心事は更に小さな関心事に分割できるのです。

こういった構造に立ち向かう一つのプラクティスとしては、最小単位の関心事で分離しつつ、それをまとめ上げる大きな関心事を受け持つメソッドなどから個別にそれらを呼び出すといった設計が考えられます。

いずれにせよ実装者としては、関心事はフラットではなく階層構造であり、どの階層の関心事で分離するのか?は常に意識する必要があると言えます。

(ご参考)依存性注入や具体的事例、諸々。

「依存性の注入」は関心の分離を実現する重要テクニック

「依存性の注入」(DI)は、関心の分離実現のため、コンポーネント間の疎結合化を行うプログラミング技法です。

多くのオープンソースソフトウェアなどで実際にDIが採用されており、保守性の高いコードを作成するための必須知識といえます。

字面の問題もあり取っつきにくい印象がある概念ですが、意味するところはシンプルです。是非、次の関連記事も併せてご参考ください。

関連記事:依存性注入/DIはごくシンプルな概念。よってシンプルに説明してみる。

より具体的な事例について、私の経験談に基づいた関連記事を作成しております。

私も初心者の頃は特にダメコードを大量生産し、後からの変更に苦しんだ経験を経てきました。そのうちの一つを↓の記事にまとめていますので、是非合わせてご参考ください。

関連記事:【失敗事例に学ぶDDD】MVCパターンではDBと計算ロジックは分離すべし

SQLとアプリケーションの「関心の分離」について

SQLにアプリケーション的処理をプログラムするべきではないという意見は随所で見られます。

とはいえ不用意なSQLの実行は処理速度のボトルネックになりやすく、一概に分離せよと処理できる議論ではないのが難しいところです

そういった葛藤も含めて詳細を関連記事に記載しておりますので、併せてご参考ください。関心の分離の具体例としても良質な題材です。

関連記事:【関心の分離とSQL】複雑なSQLを書くべきではないという問題提起について

「関心事の分離」、「関心の分離」、「責務の分離」は何が違うのか?

基本的には、いずれもSoC(Separation Of Concerns)を意味するワードであり、表記ゆれと捉えて差し支えありません。

プログラムを関心=何をしたいのかで分けるとは、すなわち責務を分離していることに他ならないからです。

今回は以上です。ありがとうございました。

記事筆者へのお問い合わせ、仕事のご依頼

当社では、IT活用をはじめ、業務効率化やM&A、管理会計など幅広い分野でコンサルティング事業・IT開発事業を行っております。

この記事をご覧になり、もし相談してみたい点などがあれば、ぜひ問い合わせフォームまでご連絡ください。

皆様のご投稿をお待ちしております。

記事筆者へ問い合わせする

※ご相談は無料でお受けいたします。
IT活用経営を実現する - 堺財経電算合同会社