プログラミングにおける「関心の分離」(責務の分離、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へのバグ埋め込みリスクはかなり低く抑えることができます。
ソフトウェアの変更は、そのソフトウェアを使い続ける限り必ずおこります。変更に対する対応力を高めておくことは保守面で重要と言えます。
「関心の分離」の例示:クラス編
オブジェクト指向プログラミングをサポートする開発言語では、クラスについても関心ごとに分離することを意識する必要があります。
例えば、「財務会計計算」と「管理会計計算」の機能を持つ財務分析システムを考えます。「財務会計」と「管理会計」はいずれも会計に関する計算をすることには変わりありませんが、その目的は大きく異なりますので、クラスを別にして実装することが基本指針となります。
この例を踏まえ、関心の分離ができている例、できていない例を具体的に図示してみます。
実装全体
会計計算クラス(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 ManagementAccountingCalculation {
//actual変数をベースに会計計算を行う関数
//このクラスでは管理会計計算のみを行う。
int RunCalculation(List actual) {
//管理会計計算
}
}
クラス分けを行い実装をした例。
この場合であれば、管理会計の修正はManagementAccountingCalculationクラスのみを変更すればよく、財務会計計算側にバグを埋め込む可能性を低く抑えられる。
後者の例のほうが、後からの変更に強いプログラムを作成できています。
このような分け方をできることがオブジェクト指向プログラミングの一つの強みと言えます。オブジェクト指向については、関連記事を記載してありますので是非合わせてご参照ください。
「関心の分離」の例示:オブジェクト指向言語におけるポリモーフィズムを活用した分離手法
ここまで、関心の分離を関数分け・クラス分けで実現する方法を紹介しました。これを応用してオブジェクト指向言語におけるポリモーフィズムを活用した分離手法を紹介します。
ポリモーフィズムは拡張性(機能を派生・分離させること)を実現する機能です。今回はそれら機能の一つ、インターフェースと継承を使い関心の分離を実現してみます。
例えばウェブシステムなどで、ユーザーが使用するブラウザー別に表示内容を分ける事例を考えてみましょう。ブラウザー個別の処理と共通処理が両方必要なケースです。
関心の分離が不十分なコード
//表示する関数
void RunDisplay(string browserName) {
if (browserName == "Chrome") {
DisplayFor_Chrome(); //Chromeの場合の表示処理
} else if (browserName == "Edge") {
DisplayFor_Edge(); //Edgeの場合の表示処理
} else if (browserName == "Safari") {
DisplayFor_Safari(); //Safariの場合の表示処理
} else if (browserName == "Firefox")
DisplayFor_Firefox(); //Firefoxの場合の表示処理
}
Display_Common(); //共通の表示処理
}
上記コードは、一応関数で処理を分けているものの、ifでルート分けしているだけで不十分です。例えばSafariとFirefoxだけ処理するなど複雑化するたびにジワジワとスパゲッティコードへと進化しがちな書き方です。
今回はユーザーのブラウザー別に処理を実装したいので、まずは一本の処理からクラスごとに分離してみましょう。それぞれに上記コードに対応するDisplay関数を実装します。
同時に、クラスに分けたインスタンスを使って共通して処理するため、インターフェースIBrowserDisplayを実装します。
ブラウザー別にクラス分け・表示処理実装
//Chrome用表示クラス
class ChromeDisplay : IBrowserDisplay {
public void Display() {
//Chromeの場合の表示処理
}
}
//Edge用表示クラス
class EdgeDisplay : IBrowserDisplay {
public void Display() {
//Edgeの場合の表示処理
}
}
//Safari用表示クラス
class SafariDisplay : IBrowserDisplay {
public void Display() {
//Safariの場合の表示処理
}
}
//Firefox用表示クラス
class FirefoxDisplay : IBrowserDisplay {
public void Display() {
//Firefoxの場合の表示処理
}
}
//インターフェース(Display関数の実装を保証)
interface IBrowserDisplay {
void Display();
}
このように実装することで、次のコードのように一つの処理で実体クラスごとの処理を実行する多様性が確保できます。
ブラウザー種類別の表示処理が各クラスごとに分離して実装されるので、複雑な条件にも対応しやすくなっています。関心の分離的に言えば、RunDisplay関数はブラウザー別の表示処理の内容には関心がなく、Display関数の細かい内容は各ブラウザークラスの中でやってください、といったところでしょうか。
変更結果
//IBrowserDisplayインターフェースを実装したクラスのインスタンスを引数に取る関数
void RunDisplay(IBrowserDisplay browserDisplay) {
//IBrowserDisplayはDisplay関数の実装を保証しているので、Display()を実行できる
browserDisplay.Display(); //browserDisplayの中身によって各クラスのDisplay関数が実行される
Display_Common(); //共通の表示処理
}
RunDisplay(new ChromeDisplay()); //Chromeの場合の表示処理が実行される
RunDisplay(new Edge()); //Edgeの場合の表示処理が実行される
尚、「関心の分離をきちんとすればifは不要になる」などと言われることがあります。「不要になる」は言い過ぎ感がありますが、実際に上記コードでも実際にブラウザー選択のifがポリモーフィズムにより不要となったことが見て取れます。
関心の分離の典型的な失敗:コードの副作用は、バグのリスクを高め生産性を下げる
コードの副作用は、その処理の目的以外の値をついでに変えてしまうという点で、「関心の分離」の失敗の典型と言えます。副作用をケアしないコードを積み重ねれば低品質なプログラムが出来上がることは間違いありません。
今回は例として、FinancialCalculationという整数リストを引数に取り独自の財務計算を行い結果を整数で返す関数を題材に、事例を見ていきます。
副作用の事例①:引数が変化する
関数を実行すると引数が変化する、といった事例。
FinancialCalculation関数の中で引数インスタンスに対してAdd(push)を実行するとこのような結果となります。
//関心の分離が出来ていない事例
void FinancialCalculation(List<int> actual) {
//計算処理の途中で引数のリスト構造体に要素を追加
actual.Add(15);
}
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関数によってリスト要素が変化した
優秀なプログラマは、後日他の人が自分のコードを見た、あるいは使ったときのことを考える
コードの副作用の件について全般的に言えることですが、「他の人が後に自分のコードを見たり呼び出したりしたときに面倒なことにならないか?」という視点を持つことは大切のように感じます。
副作用が埋め込まれている恐れがある場合、他の人がコードを検証や利用するときに、関数などの名前だけではなく処理の中身まで確認しに行かなければなりません。他人のコードの解読作業は、生産性の観点からできれば避けたいものです。副作用がないだろうという信頼関係の下でチーム開発が出来れば、このような無駄なことに工数を割くこともなくなるはずです。
(尚、ここでは「他の人」と表現しましたが、「3日たてば他人のコード」という格言があるように自分の書いたコードも3日たてば細かい内容は忘れてしまうものです。未来の自分のためにも、関心事をきちんと分離したコードを常に書くようにするべきでしょう。)
副作用の事例②:計算用関数を実行すると、インスタンスのプロパティやDBの値が更新される
「計算」と名の付く関数は、実行側は引数を入れて戻り値で結果を得ることを期待しており、これが関心です。
これにもかかわらず、関数を実行することによってプロパティの値を変化させたりDBにアクセスしたりするのは想定外のバグの要因となります。関数が数百~数千行規模となってくると尚更です。
コードの量が多少増えたとしても、プロパティの変更やDBの参照・変更は、しかるべき場所や名前を付けた関数の中で、別に実行するべきなのです。
ここで注意していただきたいのは、今回は計算用関数を論じているのでDBやプロパティ操作が好ましくない、という話であって、関数からDBやプロパティの操作をすることが常にNGという意味ではありません。
そういった処理を実装するのであれば、そのための適切な関数やクラスを別途用意するべき、ということです。(下図参照)
副作用の事例③:プロパティの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などとつければ何のデータなのかが不明です。CommonToolといった何でもボックス的なクラスも、この記事で論じてきたことを踏まえると不適切です。
コメントで補完するといった手も考えられますが、効率性などを考えるとやはり命名により役割を示すのがベストプラクティスといえます。
尚、プログラミングにおける命名に関しては、有名書籍「リーダブルコード」の第2~3章にかなり詳しく論じられています。気になる方は一読されてみるといいでしょう。
(注意点)多くの関心事は階層構造を持ち得る
厄介なことに、多くの関心事は階層構造になっています。このことを意識しないと、ここまで述べたようにメソッドやクラスをきちんと分割しても努力が水泡に帰す可能性があります。
すなわち、今回の財務計算モデルのようなタスクであれば、「財務の状況を知りたい」→「財務状況のうち資金繰りの適正さを知りたい」→「資金繰りに含まれる各銀行口座の動向を知りたい」→「毎日の入出金データを取得したい」・・・といった具合に、大きな関心事は更に小さな関心事に分割できるのです。
こういった構造に立ち向かう一つのプラクティスとしては、最小単位の関心事で分離しつつ、それをまとめ上げる大きな関心事を受け持つメソッドなどから個別にそれらを呼び出すといった設計が考えられます。
いずれにせよ実装者としては、関心事はフラットではなく階層構造であり、どの階層の関心事で分離するのか?は常に意識する必要があると言えます。
(ご参考)疎結合化と依存性注入や具体的事例、諸々。
「疎結合化」は、関心の分離と並びシステム構築における重要概念
疎結合化は、関心の分離と並び変更に強いソースコードを作るうえで重要な近隣概念と言えます。
私も初心者の頃は疎結合化に失敗したダメコードを大量生産し、後からの変更に苦しんだ経験を経てきました。概念の解説や筆者の体験談も含め↓にまとめさせていただいています。ご興味があれば。
「依存性の注入」は関心の分離を実現する重要テクニック
「依存性の注入」(DI)は、関心の分離実現のため、コンポーネント間の疎結合化を行うプログラミング技法です。
多くのオープンソースソフトウェアなどで実際にDIが採用されており、保守性の高いコードを作成するための必須知識といえます。
字面の問題もあり取っつきにくい印象がある概念ですが、意味するところはシンプルです。是非、次の関連記事も併せてご参考ください。
SQLとアプリケーションの「関心の分離」について
SQLにアプリケーション的処理をプログラムするべきではないという意見は随所で見られます。
とはいえ不用意なSQLの実行は処理速度のボトルネックになりやすく、一概に分離せよと処理できる議論ではないのが難しいところです
そういった葛藤も含めて詳細を関連記事に記載しておりますので、併せてご参考ください。関心の分離の具体例としても良質な題材です。
「関心事の分離」、「関心の分離」、「責務の分離」は何が違うのか?
基本的には、いずれもSoC(Separation Of Concerns)を意味するワードであり、表記ゆれと捉えて差し支えありません。
プログラムを関心=何をしたいのかで分けるとは、すなわち責務を分離していることに他ならないからです。
今回は以上です。ありがとうございました。
記事筆者へのお問い合わせ、仕事のご依頼
当社では、IT活用をはじめ、業務効率化やM&A、管理会計など幅広い分野でコンサルティング事業・IT開発事業を行っております。
この記事をご覧になり、もし相談してみたい点などがあれば、ぜひ問い合わせフォームまでご連絡ください。
皆様のご投稿をお待ちしております。
些細な間違い?の指摘です。
管理会計計算クラス(ManagementAccountingCalculationクラス)
のコード部分のクラス名がFinancialAccountingCalculation になっています。FinancialではなくManagement だと思いました。
読みやすい記事ありがとうございました。