🐷

Swift的 SOLID原則

に公開

SOLID の生みの親と普及の親

最近は、なんとなく知ってるけど、あんまりよく知らないことを調べるときに、ちょこっとその歴史から調べることにしてる。

ということで本日はみんな大好き SOLID原則。

2000年にロバート・C・マーティンっていうアメリカの偉い技術者が、ちょこちょこと考えていた設計原則を「Design Principles and Design Patterns」という論文にして発表したことが元ネタ(私はジャズとヒップホップの垣根をなくしたロバートの方が好きだけど)。

そして、その論文で紹介されていた原則を SOLID という、なんともキャッチーな名前にパッケージして、世の中に広めていったのがマイケル・フェザーズっていう、これまた偉いおっちゃん。そして、2004年以降には、「SOLID原則って知ってますか?」って技術質問で使われるぐらいに庶民エンジニアにも広く知られるようになっていった。

私はSwiftエンジニアなので、SwiftでわかるようにSOLIDしてく

単一責任の原則 (Single Responsibility Principle)

クラスとか構造体は「たった一つの責任」を持つべき

// NG: User 管理とファイル保存がごちゃ混ぜ
class UserManager {
    func createUser(name: String) {
        // ユーザー作成処理
    }
    func saveToFile() {
        // データをファイルに書き出し
    }
}

// OK: 責任を分割
struct User {
    let name: String
}

class UserCreator {
    func create(name: String) -> User {
        return User(name: name)
    }
}

class UserFileStore {
    func save(_ user: User) {
        // user データをファイルに書き出す処理
    }
}

分かれてた方が、修正しやすいし、テストしやすいし、影響範囲がわかりやすいでしょ!一番上にあるだけあって、一番大事な気がする。

開放/閉鎖の原則 (Open/Closed Principle)

※ ちなみに、「Principle」って「原理」って意味ね。

拡張性は快く受け入れて、変更にはスパルタであれ!
例えでいうと、メソッドの追加はいいけど、メソッド内の変更はダメって感じ。なぜなら、変更は既存コードに影響範囲が及んでしまうから。もしバグったらどうすんねん!っていうあれ。

// NG: 支払い方法を if 文で増やすたびに修正
class PaymentProcessor {
    func pay(amount: Double, method: String) {
        if method == "credit" {
            // クレジットカード支払い
        } else if method == "paypal" {
            // PayPal 支払い
        }
    }
}

// OK: プロトコルで拡張可能に
protocol PaymentMethod {
    func pay(amount: Double)
}

class CreditCard: PaymentMethod {
    func pay(amount: Double) { /* クレジットカード支払い */ }
}

class PayPal: PaymentMethod {
    func pay(amount: Double) { /* PayPal 支払い */ }
}

class PaymentProcessor {
    func pay(amount: Double, method: PaymentMethod) {
        method.pay(amount: amount)
    }
}

// 新しい支払い方法も PaymentMethod を実装するだけ
class ApplePay: PaymentMethod {
    func pay(amount: Double) { /* Apple Pay 支払い */ }
}

L: リスコフの置換原則 (Liskov Substitution Principle)

なんか急に名前の主張が強いよね。これは元々、バーバラ・リスコフ(Barbara Liskov)っていう工学者が1987年の「Data Abstraction and Hierarchy」っていう論文で定義してたものらしいよ。


置き換え可能そうなジェスチャーしてるね

これは、継承関連の話で、子は親と置き換え可能であるべきっていうこと。ちょっと文字にすると怖いけど。要は、振る舞いが変わるなら継承するな!っていう感じ。

// NG: Rectangle → Square の継承で問題が起きる例
class Rectangle {
    var width: Double = 0
    var height: Double = 0
    func area() -> Double { width * height }
}

class Square: Rectangle {
    override var width: Double {
        didSet { height = width }
    }
    override var height: Double {
        didSet { width = height }
    }
}

func printArea(of rect: Rectangle) {
    rect.width = 5
    rect.height = 10
    // 四角形なら 50 が期待値
    print(rect.area())  // Square を渡すと 100 になる
}

// Square を Rectangle の代わりに渡すと期待値が崩れてしまう
printArea(of: Square())
// OK: 継承せずプロトコルで共通インターフェースを定義
protocol Shape {
    func area() -> Double
}

struct Rectangle: Shape {
    let width: Double
    let height: Double
    func area() -> Double { width * height }
}

struct Square: Shape {
    let side: Double
    func area() -> Double { side * side }
}

func printArea(of shape: Shape) {
    print(shape.area())
}

// Rectangle / Square ともに期待通りの振る舞い
printArea(of: Rectangle(width: 5, height: 10))  // 50
printArea(of: Square(side: 5))                  // 25

インターフェース分離の原則 (Interface Segregation Principle)

使わないメソッドを私に強制しないで!っていうあれです。インターフェースを小さく分割すれば、再利用性も上がるしね。

// NG: 大きすぎるプロトコル
protocol Worker {
    func work()
    func eat()
}

class HumanWorker: Worker {
    func work() { /* 作業 */ }
    func eat()  { /* 昼食 */ }
}

class RobotWorker: Worker {
    func work() { /* 作業 */ }
    func eat()  { /* ロボットは食べないのに必須 */ }
}
// OK: 分割したプロトコルを組み合わせ
protocol Workable {
    func work()
}

protocol Eatable {
    func eat()
}

class HumanWorker: Workable, Eatable {
    func work() { /* 作業 */ }
    func eat()  { /* 昼食 */ }
}

class RobotWorker: Workable {
    func work() { /* 作業 */ }
    // eat() は不要
}

依存性逆転の原則 (Dependency Inversion Principle)

まあ、具象に依存せずに抽象に依存してねっていうあれ。DDD とか Clean Architecture 的な思想の源流ですね。DB に依存しなければ、将来そのコードを殺さずに、使いまわしたりできるからね。DB を置き換えればいいだけになるので。

// NG: 高水準モジュールが具体クラスに依存
class MySQLDatabase {
    func query(_ sql: String) { /* MySQL 用クエリ */ }
}

class UserService {
    let db = MySQLDatabase()
    func fetchUser(id: Int) {
        db.query("SELECT * FROM users WHERE id=\(id)")
    }
}
// OK: 抽象プロトコルを介して依存注入
protocol Database {
    func query(_ sql: String)
}

class MySQLDatabase: Database {
    func query(_ sql: String) { /* MySQL 用クエリ */ }
}

class SQLiteDatabase: Database {
    func query(_ sql: String) { /* SQLite 用クエリ */ }
}

class UserService {
    private let db: Database
    init(database: Database) {
        self.db = database
    }
    func fetchUser(id: Int) {
        db.query("SELECT * FROM users WHERE id=\(id)")
    }
}

// 利用側で具体実装を切り替え可能
let mysqlService = UserService(database: MySQLDatabase())
let sqliteService = UserService(database: SQLiteDatabase())

Discussion