はじめに
こんにちは。保守性の高いソフトウェアの開発を目指すエンジニア @blue32a です。 辛い開発をなんとか解決して未来につなげるため、主にソフトウェア設計からアプローチしています。
今回は改修が積み重なったコードでよくある問題から、それを解決するための観点を考え、記事としてまとめてみました。
よく見かけるコード
皆さんは次のようなコードを目にしたことはあるでしょうか?
function hoge(data, flagA, flagB, flagC) {
// 処理A
if (flagA) {
// 処理B
}
// 処理C
if (flagB) {
// 処理D
if (flagA || flagC) {
// 処理E
}
}
// 処理F
}
実際にはこの数倍の行数があると考えていください。
改修が積み重なったシステムでは、このようにフラグによる条件分岐でごちゃごちゃしていて分かりにくくなってしまったコードをよく見かけることでしょう。
このようなコードには次のような問題があります。
- パターンが多く複雑で、内容を把握することが困難
- さまざまな状況で利用されるので、変更されたときの影響が広い範囲に及ぶ
このようなコードを変更しなければならないときは、とても辛い思いをすることでしょう。
なぜこうなってしまうのか
おそらく、最初はもっと小さなコードだったことでしょう。
function hoge(data) {
// ...
}
ここから様々な要求があって変更されていくことになりますが、このときに「変更のための行数をできるだけ少なくする」「対応漏れがでないようにする」といった考えから「フラグを用いた条件分岐で複数のユースケースで使えるようにする」といった設計になっていくと思います。
こうしたごちゃごちゃして変更が難しい設計を回避し、使いやすく変更しやすい設計にするにはどうすれば良いでしょうか。そのために「使いやすさ」について深堀りしてみます。
「使いやすい」の解像度を上げる
そもそも「使いやすい」というのはどういうものでしょうか?2つの観点で考えみます。
単純:小さな一つの処理を行うことができる。小さいので覚えやすいし、保守しやすいが、これだけでユースケースそのものを実現することはできない。
容易:複雑な処理を少ない手順で行うことができる。ユースケースを実現させることができるが、使うために覚えることが多く、要求の変化により変更されやすい。
この”単純”と”容易”の2つの「使いやすさ」にはそれぞれこのような違いがあります。
よくシンプル(単純)な設計であることが良しとされつつも実際にはごちゃごちゃとした設計になってしまうのは、単純な機能だけではユースケースを実現させることができない点にあるのではないかと思います。実際の要求が複雑である以上、その複雑さはどこかの機能で受け持つ必要があります。
そこで、使いやすさの観点を”単純”と”容易”に分けて扱うことで、使いやすく変更しやすい設計を考えていきたいと思います。
方針は次のとおりです。
”単純”な機能をまとめて”容易”な機能を作り、1つのユースケースを満たす。
実装のイメージ
単純な機能
最初のコードの例を思い出してください。まずはそこから”単純な機能”を抽出します。
function simpleA() {
}
function simpleB() {
}
function simpleC() {
}
function simpleD() {
}
function simpleE() {
}
function simpleF() {
}
(これらのコードはあくまでイメージするためのコードです。改善前のコードと完全に対応するものではないことに注意してください)
”単純な機能”はそれぞれが数行程度の小さな機能です。例えば「税率の計算」「リストの並び替え」「値の変換」「ある条件を判定する」などがあります。
小さな1つの処理しか扱わないので内容を理解しやすく、変更の影響も把握しやすいでしょう。変更する理由も限られるので、比較的安定した機能と言えます。
しかしながら、これ1つで「商品の購入」や「帳票の出力」などといったユースケースを実現することはできません。
そこで、抽出した”単純な機能”をまとめた”容易な機能”を作っていきます。
容易な機能
function easyA() {
simpleA();
simpleB();
simpleC();
simpleF();
}
function easyB() {
simpleA();
simpleC();
simpleD();
simpleF();
}
function easyC() {
simpleA();
simpleB();
simpleC();
simpleD();
simpleE();
simpleF();
}
この”容易な機能”では複数の”単純な機能”を活用し、「商品の購入」や「帳票の出力」などといったユースケースを実現するための処理を実装していきます。
そのために”単純な機能”よりも行数が多く、複雑なコードになるでしょう。しかし、”単純な機能”を分割したことにより改善前のコードよりは見通しが良くなっています。
これらの”容易な機能”はそれぞれのユースケースに対応します。
ユースケース
function usecaseA() {
// ...
easyA();
// ...
}
function usecaseB() {
// ...
easyB();
// ...
}
function usecaseC() {
// ...
easyC();
// ...
}
これで「”単純”な機能をまとめて”容易”な機能を作り、1つのユースケースを満たす」という方針に沿った形になりました。
”容易な機能”を1つのユースケースに対応させることで、ユースケースの変更が他の”容易な機能”に影響することを防ぐ形になっています。
この構造を図にすると次のようになります。
このように"容易な機能"は1つのユースケースに対応するので、ユースケースの変更は1つの"容易な機能"に収まります。ここが「フラグにより複数のユースケースに対応した機能」と異なるところです。
"容易な機能"の詳細は複数の"単純な機能"として共通化されています。"単純な機能"に対する変更は複数の"容易な機能"に影響することになりますが、単純ゆえにその影響は把握しやすくなります。
まとめ
フラグによるごちゃごちゃとした設計を回避し、使いやすく変更しやすい設計にするため、「使いやすさ」を”単純”と”容易”に分けて考えてみました。
「”単純”な機能をまとめて”容易”な機能を作り、1つのユースケースを満たす」という方針にそって、まずは小さな一つの処理を実現する”単純な機能”を作っていき、次にそれを組み合わせ”容易な機能”とすることで、ユースケースを実現していきます。このような観点で設計していくことで、「機能の共通化」「対応漏れの防止」という点に取り組みつつ、「フラグによるパターンの増加」「変更の影響が広い範囲に及んでしまう」といった問題を回避することを目指します。
今回の例では”単純”と”容易”で2層の構造になりましたが、実際のプロダクトはもっと規模が大きく、いくつものレイヤーに分かれることも多いので、簡単に”単純”と”容易”に分けることは難しいかもしれません。
しかし、「”単純”と”容易”のどちらの観点で使いやすい機能にするか」という点を考えることによって、それぞれの機能をより使いやすく変更しやすい設計にしていく軸になると思います。
この記事の内容が参考になれば幸いです。