SOLID原則とは?Swift(iOS)で理解する設計開発思想

この記事からわかること

  • SOLID原則とは?
  • ソフトウェア開発における設計思想
  • Swift/iOS例える各原則の役割
  • Single Responsibility Principle単一責任の原則とは?
  • Open-Closed Principle開放・閉鎖の原則とは?
  • Liskov Substitution Principleリスコフの置換原則とは?
  • Interface Segregation Principleインターフェース分離の原則とは?
  • Dependency Inversion Principle依存性逆転の原則とは?

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

今回はオブジェクト指向プログラミングにおける開発設計思想の1つである「SOLID原則」についてSwift(iOS)と絡めながらまとめていきたいと思います。

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

おすすめ記事:【Swift】MVVMアーキテクチャとは?ViewModelの役割

SOLID原則とは?

そもそも「SOLID原則」とはオブジェクト指向型の言語を用いたソフトウェア開発において、拡張性や保守性を高めるために利用されるガイドラインみたいなものです。SOLID原則は以下の5つの原則で構成されています。

  1. Single Responsibility Principle:単一責任の原則
  2. Open-Closed Principle:開放・閉鎖の原則
  3. Liskov Substitution Principle:リスコフの置換原則
  4. Interface Segregation Principle:インターフェース分離の原則
  5. Dependency Inversion Principle:依存性逆転の原則

アメリカのロバート・C・マーティン (Robert C. Martin) によって2000年代初頭に書かれた著書「Agile Software Development: Principles, Patterns, and Practices」の中でSOLID原則を提唱されました。

ここからはそれぞれの役割をSwift(iOS)での例に例えながら見ていきたいと思います。またここではクラスを例に見ていきますが、原則に当てはめるべきはクラスだけでなく、メソッドやプロパティなども含みます。

Single Responsibility Principle:単一責任の原則

1つのクラスは単一の責務を持つべき

これはクラスに持たせるべき責務を考えるための原則です。ここで言う責務とはクラスが持つ役割や機能(API)のことであり、クラスに対して責務は1つであるべきだと提唱しています。

例えばユーザー情報を表すクラス(Userクラス)にユーザー情報を管理させる(ユーザーの追加など)のはこの原則に反しています。なのでユーザーを管理するためのUserManagerクラスなどを定義することでそれぞれの責務を明確に切り分けることが大切なようです。

class User {
    let id: Int 
    var name: String
    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
}  

class UserManager {
    private var users: [User] = []

    func addUser(_ user: User) {
        users.append(user)
    }

    func removeUser(_ user: User) {
        if let index = users.firstIndex(where: { $0.id == user.id }) {
            users.remove(at: index)
        }
    }
}

意識するべきは定義したクラス名以上の役割をクラスに持たせないようにすることだと思います。

Open-Closed Principle:開放・閉鎖の原則

拡張しやすく、変更しにくい設計にするべき

この原則はクラスの機能に対する原則です。クラスに対して新しい機能を追加することには開放的であり、機能自体を変更することには閉鎖的であるべきと提唱しています。

クラスの機能(メソッドなど)の動作自体を変更するとアプリ全体に影響を及ぼす可能性があり、改修する部分が膨大になってしまう可能性があります。機能を追加するだけであれば影響を与える範囲は少なくすみます。

extension UserManager {
    func updateUser(_ id: Int,name: String) {
        if let index = users.firstIndex(where: { $0.id == user.id }) {
            var user = users[userIndex]
            user.name = name
            users[userIndex] = user
        }
    }
}

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

サブクラスはスーパークラスを置き換えても問題無いようにするべき

この原則はクラスのスーパークラスとサブクラスの継承に関する原則です。サブクラスを利用している部分はスーパークラスでも正常に動作できるようにすることで、正しい継承(仕様)であることを保証させることごできると提唱しています。

ちなみにリスコフとはこの原則の提唱者であるアメリカのコンピュータ科学者、Barbara Liskov(バーバラ・リスコフ)の名前に由来しているようです。

スーパークラスとしてUserを定義しておき、継承したサブクラスを2つ用意します。そしてそれらを扱うUserManagerクラスはUser型を操作するクラスとします。

protocol User {
    var id: Int { get }
    var name: String { get set }
}

class RegularUser: User {
    var id: Int
    var name: String
    
    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
}

class PremiumUser: User {
    var id: Int
    var name: String
    var isPremium: Bool
    
    init(id: Int, name: String, isPremium: Bool) {
        self.id = id
        self.name = name
        self.isPremium = isPremium
    }
}

class UserManager {
      private var users: [User] = []

    func addUser(_ user: User) {
        users.append(user)
    }

    func removeUser(_ user: User) {
        if let index = users.firstIndex(where: { $0.id == user.id }) {
            users.remove(at: index)
        }
    }
}

これによりUserManagerクラスはRegularUser型もPremiumUser型も意識することなくUser型として操作することが可能になります。

let userManager = UserManager()

let user1 = RegularUser(id: 1, name: "Alice")
let user2 = PremiumUser(id: 2, name: "Bob", isPremium: true)

userManager.addUser(user: user1)
userManager.addUser(user: user2)

print(userManager.users)
// Output: [RegularUser(id: 1, name: "Alice"), PremiumUser(id: 2, name: "Bob", isPremium: true)]

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

使用しないインターフェース(メソッドやプロパティなど)を持つクラスは設計上の問題があるとされ分離するべき

この原則はクラスに持たせるべき役割を精査するための原則です。1つの大きなクラスが持つインタフェースを小さなクラスに分割することで必要となるインターフェースだけを保持したクラスになるようにするべきと提唱しています。

protocol UserManaging {
  func addUser(user: User)
  func removeUser(user: User)
}

protocol UserUpdating {
  func updateUser(user: User)
}

protocol UserRetrieving {
  func getUser(by id: Int) -> User?
}

class UserManager: UserManaging, UserUpdating, UserRetrieving {
  private var users: [User] = []

  func addUser(user: User) {
    users.append(user)
  }

  func removeUser(user: User) {
        if let index = users.firstIndex(where: { $0.id == user.id }) {
            users.remove(at: index)
        }
  }

  func updateUser(user: User) {
    if let index = users.firstIndex(where: { $0.id == user.id }) {
      users[index] = user
    }
  }

  func getUser(by id: Int) -> User? {
    return users.first { $0.id == id }
  }
}

細かく切り分けることで再利用性が向上するだけでなく、クラス間の依存性の減少や、テストやメンテナンスの容易性が上がりやすくなります。

Dependency Inversion Principle:依存性逆転の原則

上位クラスは下位クラスに依存してはならず、抽象に依存するべき

この原則はクラスが依存するべき先を示すための原則です。クラスAとクラスBがあるとき、この両者に依存関係を持たせるべきではなく、抽象(Swiftでいうプロトコルなど)として定義された型に対して依存させるべきと提唱しています。

これまでのコードにも既に登場してますが、クラスA変更した際に、クラスBまで変更するのは良くないので、両者が扱うべき抽象を用意しようねって感じですかね。

protocol UserProtocol {
    var id: Int { get set }
    var name: String { get set }
}

class User: UserProtocol {
    var id: Int
    var name: String
    var email: String
    
    init(id: Int, name: String, email: String) {
        self.id = id
        self.name = name
        self.email = email
    }
}

class UserManager {
    var users: [UserProtocol] = []
    
    func addUser(user: UserProtocol) {
        users.append(user)
    }
    
    func removeUser(user: UserProtocol) {
        if let index = users.firstIndex(where: { $0 === user }) {
            users.remove(at: index)
        }
    }
}

UserProtocolプロトコルを定義したことでUserManagerクラスは、UserProtocol型(抽象)に依存させることができ、UserProtocolを採用しているすべてのオブジェクトを追加/削除できるようになります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index