この記事ではソフトウェア設計において
- 中途半端なモノの状態や動作を許容するとどうなるか
- 中途半端なモノの状態や動作をなくすためにどうするか
- 中途半端なモノの状態や動作をなくすことで得られるもの
についてまとめました。
短い動画でも解説してるのでぜひご覧ください。
中途半端なモノの状態や動作とは??何が悪いのか??
中途半端なものの状態や動作とは、
(自分が勝手にそう呼んでいるだけですが、)
例えば、
インスタンス化する際の引数が足りなくてもインスタンス化できる。
そのメソッドも実行できる。
ただ、そのメソッドを実行するとセットアップが足りていないため正常に動かない。
設定ファイルや設定用のデータベースが間違っている。
しかしアプリケーションは正常に起動し、
設定が関連する処理が呼ばれると正常に動かない。
このような状態に陥っているものをここでは指しています。
ここで、少し身の回りのモノに目を向けてみましょう。
例えば、フードプロセッサーやノンフライヤーなどは
電源を入れても蓋がきちんと閉められなければ
起動しないようになっています。
そのお陰で我々利用者は
使い方がよく分かってなくても、
誤用して怪我したり火傷したりするのを防げています。
このように
利用する側があまり使い方を深く理解していなくても、
誤用を防げるように、
中途半端な状態や動作を許容しないように、
することがソフトウェア設計においても重要になります。
中途半端なモノの状態や動作をなくすためにどうするか??
ここからは中途半端な状態のインスタンスの悪い例と
それを改善した良い例をコードで比較しながら見ていきます。
class Campaign(
var campaignStartDate: LocalDate?,
var campaignEndDate: LocalDate?,
var pointRate: Double?
) {
fun calculateCampaignPoint(purchaseAmount: Int): Int {
if (!isValidCampaign()) return 0
return floor(pointRate!! * purchaseAmount).toInt() // ここではpointRateが必ず入る想定
}
private fun isValidCampaign(): Boolean {
val today = LocalDate.now()
return !today.isBefore(campaignStartDate) && !today.isAfter(campaignEndDate)
}
}
@Test
fun `有効なキャンペーンの付与ポイントが算出できる`() {
val campaign = Campaign(
campaignStartDate = LocalDate.of(2023, 1, 1),
campaignEndDate = LocalDate.of(2023, 12, 31),
pointRate = 0.1
)
val actual = campaign.calculateCampaignPoint(1000)
Assertions.assertEquals(100, actual)
}
@Test
fun `中途半端な状態のキャンペーンが作れてしまってバグの危険がある`() {
val campaign = Campaign(
campaignStartDate = null,
campaignEndDate = null,
pointRate = null
)
// この間ではcampaignの初期化が完了していないため、NullPointerExceptionが発生する
// val actual = campaign.calculateCampaignPoint(1000)
campaign.campaignStartDate = LocalDate.of(2023, 1, 1)
campaign.campaignEndDate = LocalDate.of(2023, 12, 31)
campaign.pointRate = 0.1
val actual = campaign.calculateCampaignPoint(1000)
Assertions.assertEquals(100, actual)
}
Campaignクラスを使う側は、
インスタンス生成時に初期セットアップが任意のため、
もしセットアップを忘れると
その後、キャンペーンポイントを算出しようとしても
NullPointerExceptionが発生してしまいます。
誤用してしまう可能性があるということですね。
では改善した例を見ていきます。
class Campaign private constructor(
private val campaignStartDate: LocalDate,
private val campaignEndDate: LocalDate,
private val pointRate: Double
) {
companion object {
fun create(
campaignStartDate: LocalDate,
campaignEndDate: LocalDate,
pointRate: Double
): Campaign {
if (!campaignStartDate.isBefore(campaignEndDate)) throw IllegalArgumentException("キャンペーンの開始日は終了日より前である必要があります")
return Campaign(
campaignStartDate = campaignStartDate,
campaignEndDate = campaignEndDate,
pointRate = pointRate
)
}
}
・・・
@Test
fun `有効なキャンペーンの付与ポイントが算出できる`() {
val campaign = Campaign.create(
campaignStartDate = LocalDate.of(2023, 1, 1),
campaignEndDate = LocalDate.of(2023, 12, 31),
pointRate = 0.1
)
val actual = campaign.calculateCampaignPoint(1000)
Assertions.assertEquals(100, actual)
}
@Test
fun `有効期限が不正なキャンペーンは作れない`() {
assertThrows<IllegalArgumentException> {
Campaign.create(
campaignStartDate = LocalDate.of(2023, 12, 31),
campaignEndDate = LocalDate.of(2023, 1, 1),
pointRate = 0.1
)
}
}
使う側はCampaign.create()経由でのインスタンス生成が強制され、
不正な状態でのインスタンス生成ができなくなりました。
使う側が誤用する余地がなくなりましたね。
改善後のCampaignクラスも、
pointRateがマイナスで初期化されたら
おかしな挙動になります。
業務で扱う値を正確に表現してやる必要があるでしょう。
業務で扱う値の範囲を正確に表現する〜ソフトウェア設計のきほん〜また、キャンペーン期間というクラスを別途作って、
キャンペーン開始・終了日の整合性などはそちらに切り出す。
などなど改善の余地はあります。
練習兼ねて是非改善してみてください!!
中途半端なモノの状態や動作をなくすことで得られるもの
中途半端な状態や動作をなくすことで、
これらが得られると僕は考えています。
- 使う側がインスタンスの状態をいちいち気にする必要がなくなる
- 使う側が誤用する可能性を減らせる
- 中途半端に動作してデータベースを壊すような最悪の事態を防げる
- 何が悪くて動作しなかったのか特定しやすい
使う側は別のプログラマーや、
未来の自分だったりしますが、
使われる側をそこまで深く理解せずに利用したり、
誤用したりする可能性は常にあると想定しておく必要があります。
間違ったセットアップだとインスタンス化が正常に行えない。
間違ったメソッド呼び出しはコンパイルエラーが通らない。
など、なるべく早く使う側が誤用に気付けるように、
工夫することは重要です。
それが、
中途半端に動作してデータベースを壊すなどの
最悪の事態を引き起こすことを未然に防ぐことに繋がります。
今回のテーマについては以上です。
初めてソフトウェア設計をする方向けに、
重要ポイントをに絞って言語化してみたのでこちらもご覧ください。
初めてソフトウェア設計をする方に向けて|新人プログラマー時代の自分に伝えたいこと