はじめに
SOLID 原則の 2 つ目は OCP(開放/閉鎖の原則) です。
これは「既存コードを壊さずに拡張できる設計」を目指す重要な原則で、変更に強い設計 の基盤になります。
1. 定義
開放/閉鎖の原則(OCP) とは:
ソフトウェアの構成要素は、拡張に対して開かれており、修正に対して閉じられているべきである。
(Open for extension, Closed for modification)
つまり:
- 新しい機能を「追加(拡張)」できる
- 既存の動作を壊す「修正」は最小限にする
2. 直感的な例
OCP 違反(条件分岐が増え続けるパターン)
class DiscountCalculator {
fun applyDiscount(type: String, price: Int): Int {
return when (type) {
"percentage" -> (price * 0.9).toInt()
"amount" -> (price - 100).coerceAtLeast(0)
else -> price
}
}
}
- 新しい割引タイプを追加するたびに
when
に修正が必要 - 既存コードに手を入れる → バグリスク増大
OCP 準拠(拡張で対応できる設計)
interface DiscountPolicy {
fun apply(price: Int): Int
}
class PercentageDiscount : DiscountPolicy {
override fun apply(price: Int) = (price * 0.9).toInt()
}
class AmountDiscount : DiscountPolicy {
override fun apply(price: Int) = (price - 100).coerceAtLeast(0)
}
// 利用側は抽象に依存
class DiscountCalculator(private val policy: DiscountPolicy) {
fun calculate(price: Int) = policy.apply(price)
}
- 新しい割引方式を追加する場合は
DiscountPolicy
を実装するだけ -
DiscountCalculator
は一切修正不要
3. OCP のメリット
- 変更による副作用が少ない(既存コードに触らないため)
- テスト容易性が向上(ポリシーごとに個別テストできる)
- 再利用性が高まる(既存の利用コードはそのまま新機能を使える)
- 保守性が向上(新しい要件に柔軟に対応可能)
4. 実務での適用パターン
-
戦略パターン(Strategy Pattern)
→ 上記の割引ポリシーのように振る舞いを差し替える -
DI(依存性注入)
→ 抽象に依存し、実装は注入で切り替える -
プラグイン構造
→ 新しい機能をモジュールとして追加
5. Flutter / Android での例
Flutter(Widgetの差し替え)
abstract class ThemeColor {
Color primary();
}
class LightTheme implements ThemeColor {
@override
Color primary() => Colors.blue;
}
class DarkTheme implements ThemeColor {
@override
Color primary() => Colors.black;
}
class ThemedButton extends StatelessWidget {
final ThemeColor theme;
ThemedButton(this.theme);
@override
Widget build(BuildContext context) {
return ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: theme.primary()),
onPressed: () {},
child: Text("OCP Example"),
);
}
}
Android(DI + Strategy)
interface Logger {
fun log(message: String)
}
class ConsoleLogger : Logger {
override fun log(message: String) = println("Console: $message")
}
class FileLogger : Logger {
override fun log(message: String) { /* ファイル出力 */ }
}
class UserService(private val logger: Logger) {
fun createUser(name: String) {
logger.log("User created: $name")
}
}
→ ログの出力先を変えても UserService
は修正不要
6. よくある誤解
-
「既存コードを一切修正してはいけない」ではない
→ インターフェース定義など「拡張のための入口」を設ける最初の修正は必要 -
「全部を抽象化すればよい」ではない
→ 過剰な抽象は逆に複雑性を増すので、変更可能性が高い部分だけに適用
まとめ
- OCP = 変更に強い構造
- 「修正」ではなく「拡張」で要件対応するのが基本姿勢
- 実務では Strategy / DI / Plugin構造 がよく使われる
- SRP と組み合わせるとより効果的(責務を分けて、拡張で追加)