はじめに
この記事ではソフトウェア設計において
- 適切に隠蔽や公開ができなければどうなるか
- どこをどうやって隠蔽・公開するのか
- 適切に隠蔽と公開することで何が得られるか
についてまとめました。
動画解説も作ったのでご覧ください
隠蔽と公開が適切でなければ使う側の負担は増加する
まず、私たちの日常生活を例に隠蔽と公開について考えると・・・
炊飯器や電気ケトルなどは
電源を繋いでボタンひとつで
ご飯を炊いたり、お湯を沸かせます。
使う側である僕たちは、
機械がどういう内部構造で、どういう原理で
ご飯が炊けたり、お湯が沸かせるかを理解せずとも、
目的を果たせます。
もし、
電源を繋いで毎回複雑なセットアップが必要だったり、
機械をバラして何かをいじらなければ、
ご飯を炊いたり、お湯が沸かせなかったらどうでしょう??
私たちが、
炊飯器や電気ケトルの内部構造に詳しくなれるかもしれませんが、
私たちの負担が高く、使い方を間違える危険も高まります。
炊飯ボタン、湯沸かしボタンだけが使う側に公開されて、
機械の内部構造や、
ご飯を炊くまでの細かいステップが隠蔽されている。
と考えることもできます。
これはソフトウェア開発においても同じことが言えます。
使う側が直感的にシンプルに使えるように、
どこをどのように公開したり、隠蔽するかを
慎重に決める必要があります。
もし適切に公開と隠蔽が行えていなければ、
- それを使う側の全てが至る所で複雑になり変更に弱くなる
- それを使う側が誤用してバグに繋がる可能性が高まる
- リファクタリングしずらくなる
こういった事態に陥ります。
外から(使う側)見たときに、
複雑な内部構造や内部の細かい状態を公開してしまうと、
例えばその状態を利用して使う側の方で判定を行い始めたりします。
使う側とはたいていの場合複数あるので、
その判定ロジックは至る所に書かれ、
その結果変更が必要になるとそれらを漏れなく修正しなければならなくなります。
また、
外から(使う側)見た時の詳細でしかない部分が、
全然関係ない場所で利用(依存)されるせいで、
外から見た振る舞いそのままに、
内部の処理を最適化しようとしてもしずらくなってしまい、
使う側だけでなく使われる側の視点でも
変更に弱くなってしまいます。
実装の詳細が公開されているコードと適切に隠蔽できているコード比較
ここからは、
具体的に悪い例と良い例をコードで見て比較してみます。
今回のサンプルコードのテーマはワークフローのタスククラスです。
・タスク開始時のステータスは開始
・customerとoperatorがそれぞれチェック済みになるとステータスは完了
class WorkFlowTask(
private var consumerCheck: Boolean = false,
private var operatorCheck: Boolean = false,
var taskStatus: TaskStatus = TaskStatus.START // タスクステータスが公開されていて外から参照・更新可能
) {
fun taskUpdate(consumerCheck: Boolean, operatorCheck: Boolean) {
this.consumerCheck = consumerCheck
this.operatorCheck = operatorCheck
taskStatusUpdate() // タスクの進捗に合わせてステタースも整合性をとって更新する
}
private fun taskStatusUpdate() {
if(consumerCheck && operatorCheck) {
taskStatus = TaskStatus.COMPLETE
}
}
fun isComplete(): Boolean {
return TaskStatus.COMPLETE == taskStatus
}
}
enum class TaskStatus {
START,
COMPLETE
}
fun taskExecute() {
val task = WorkFlowTask()
task.taskStatus = TaskStatus.COMPLETE
task.isComplete() // ステータスだけいきなり更新可能(タスク進捗との整合性を破壊できる)
}
fun taskCheck(task: WorkFlowTask): Boolean {
return TaskStatus.COMPLETE == task.taskStatus // ステータスを参照して判断するロジックが作れた
}
タスクの進捗(operatorやconsumerのチェック)と整合性をとりながら
タスクステータスを更新したいのですが、
タスクステータスを公開しているため、
整合性が取れないような更新ができてしまいます。
さらに、
タスクステータスを外から参照して判断を行うようなロジックを
至る所に作ることが可能になってしまいました。
外から電子回路つついてスイッチ入れることもできる。
けど、手順を誤ると思うように炊飯器が作動しない
といった感じでしょうか。。。
では、
電子回路は外から見えなくしてみましょう。
class WorkFlowTask(
private var consumerCheck: Boolean = false,
private var operatorCheck: Boolean = false,
private var taskStatus: TaskStatus = TaskStatus.START // 外から参照・更新できなくなる
) {
fun taskUpdate(consumerCheck: Boolean, operatorCheck: Boolean) {
this.consumerCheck = consumerCheck
this.operatorCheck = operatorCheck
taskStatusUpdate()
}
private fun taskStatusUpdate() {
if(consumerCheck && operatorCheck) {
taskStatus = TaskStatus.COMPLETE
}
}
fun isComplete(): Boolean {
return TaskStatus.COMPLETE == taskStatus
}
}
fun taskExecute() {
val task = WorkFlowTask()
task.taskUpdate(consumerCheck = true, operatorCheck = true)
// task.taskStatus = TaskStatus.COMPLETE コンパイル通らない
task.isComplete()
}
// コンパイル通らない
// fun taskCheck(task: WorkFlowTask): Boolean {
// return TaskStatus.COMPLETE == task.taskStatus
// }
内部の詳細であるタスクステータスは
外から参照・更新できなくなりました。(コンパイルエラーになる)
これによって使う側は、
内部の詳細を気にせずシンプルに、
ワークフロータスククラスを利用できるようになり、
タスクの進捗の実態とタスクステータスの整合性は必ずとれるようになりました。
また、
外から内部の詳細を参照できないので、
内部の詳細を利用したロジックが外に染み出る心配もありません。
隠蔽と公開を適切にすることで得られるもの
隠蔽と公開を適切に行うことで、
使われる側は実装の詳細を自由にリファクタリングや変更でき、
使う側もシンプルになり誤用などの利用時のミスが減らせるようになります。
実装の詳細(図でいう非公開メソッド)が外から自由に利用されたり、
内部の状態が外から自由に見たり変更できなくなることで、
他の箇所で隠蔽した実装の詳細を流用したり同じような判定処理が
作られることを防ぐことにも繋がります。
それによって、
隠蔽した部分の一部が仕様変更になった際、
影響箇所を1箇所のみに限定できます。
しかし、
公開の裏で隠蔽した非公開部分をどこまで1まとまりのクラスや関数
などとして扱うかについても別途慎重に判断する必要があります。
そちらについてはまた別の記事でまとめたいと思います。
初めてソフトウェア設計をする方向けに、
重要ポイントをに絞って言語化してみたのでこちらもご覧ください。
初めてソフトウェア設計をする方に向けて|新人プログラマー時代の自分に伝えたいこと参考文献
Chapter2 抽象化レイヤー
Good Code, Bad Code ~持続可能な開発のためのソフトウェアエンジニア的思考