[Swift] 堅牢で崩れにくいコードを実現する

はじめに

開発規模が大きくなってくると、自然と開発に関わるデベロッパーの人数も増えていき、コードも複雑化していきます。この記事ではSwiftでできないことを利用して、開発規模が大きくなっても崩れにくいコードをデザインする方法についてサンプルコードを用いて説明します。

実行環境

この記事は以下の動作環境で動作確認しています。

  • swift (5.2)

swiftでできない2つのことを利用

associatedtype

  • genericなprotocolを作れる
protocol Sequence {
    associatedtype Element
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
    func makeIterator() -> Iterator
 }
  • contrete type(class, struct, enum)に準拠させる際に実際の型を指定して使える
  • generic functionで型を指定して使える

associatedtypeを持つprotocolをpropertyの型として利用することができない

extension

  • protocolやcontrete type(class, struct, enum)の機能を拡張できる

extensionブロックの中でstored propertyを持つことができない(黒魔術を使わない限り)

実装例

依存用と公開用のprotocolを分ける

二つのprotocolを用意する

  • 何に依存しているかを明確にする
protocol Repository {
    var remoteStore: StoreRemoteStore! { get }
    var memoryStore: StoreMemoryStore! { get }
}
  • 何を公開しているかを明確にする
protocol StoreRepository {
    func find(id: Int, completion: (Store) -> Void)
}
  • 実装
struct StoreRepositoryImpl: Repository, StoreRepository {
    let remoteStore: StoreRemoteStore!
    let memoryStore: StoreMemoryStore!

    func find(id: Int, completion: (Store) -> Void) {
        fatalError("need to implement")
    }
}
  • 使い所
import UIKit

final class ViewController: UIViewController {

    private let storeRepo: StoreRepository = StoreRepositoryImpl()

    override func viewDidLoad() {
        super.viewDidLoad()

        // StoreRepositoryの持つfindしかアクセスできない
        storeRepo.find(id: 1) { (store) in
            print(store)
        }
    }
}
  • 問題点
import UIKit

final class ViewController: UIViewController {

    private let storeRepo: Repository = StoreRepositoryImpl() // 依存用プロトコルがpropertyの型として利用されてしまう

    override func viewDidLoad() {
        super.viewDidLoad()

        storeRepo.remoteStore.find(id: 1) { (store) in
            print(store)
        }
    }
}

依存用のprotocolをpropertyとして持てなくする

associatedtypeを使って依存protocolを作る

  • 依存用protocol (blueprint)
protocol Repository {
    associatedtype RemoteStore
    associatedtype MemoryStore

    var remoteStore: RemoteStore! { get }
    var memoryStore: MemoryStore! { get }
}
  • 公開用protocol (interface)
protocol StoreRepository {
    func find(id: Int, completion: (Store) -> Void)
}
  • 実装
struct StoreRepositoryImpl: Repository, StoreRepository {
    typealias RemoteStore = StoreRemoteStore
    typealias MemoryStore = StoreMemoryStore

    let remoteStore: RemoteStore!
    let memoryStore: MemoryStore!

    func find(id: Int, completion: (Store) -> Void) {
        fatalError("need to implement")
    }
}
  • 使い所
final class ViewController: UIViewController {

    private let storeRepo: StoreRepository = StoreRepositoryImpl()

    private let storeRepo2: Repository = StoreRepositoryImpl() // これができなくなる

    override func viewDidLoad() {
        super.viewDidLoad()

        storeRepo.find(id: 1) { (store) in
            print(store)
        }
    }
}

余計なpropertyを生やさせない

extensionを利用する

  • 依存protocol (blueprint)
protocol Repository {
    associatedtype RemoteStore
    associatedtype MemoryStore

    var remoteStore: RemoteStore! { get }
    var memoryStore: MemoryStore! { get }
}
  • 公開protocol (interface)
protocol StoreRepository {
    func find(id: Int, completion: (Store) -> Void)
}
protocol UserRepository {
    func findAll(completion: ([User]) -> Void)
}
  • 依存protocolの実装
struct RepositoryImpl<R, M>: Repository {
    let remoteStore: R
    let memoryStore: M
}
  • 公開protocolの実装
extension RepositoryImpl: UserRepository where R: UserRemoteStore, M: UserMemoryStore {
  func findAll(completion: ([User]) -> Void) {
    fatalError("need to implement")
  }
}

extension RepositoryImpl: StoreRepository where R: StoreRemoteStore, M: StoreMemoryStore {
  func find(id: Int, completion: (Store) -> Void) {
    fatalError("need to implement")
  }
}
  • 使い所
final class ViewController: UIViewController {

    private let userRepo: UserRepository = RepositoryImpl<UserRemoteStoreImpl, UserMemoryStoreImpl>()
    private let storeRepo: StoreRepository = RepositoryImpl<StoreRemoteStoreImpl, StoreMemoryStoreImpl>()

    override func viewDidLoad() {
        super.viewDidLoad()

        userRepo.fetchAll { (users) in
            print(users)
        }

        storeRepo.find(id: 1) { (store) in
            print(store)
        }
    }
}

おわりに

インスタンスをpropertyとして持つことが前提となりますが、
使う側からは依存用protocolのpropertyにアクセスすることができず、
また実装クラスが依存するproperty以外の余計なpropertyを増やすことができなくなります。(大勢で作業する上で好き勝手されない)

※簡易的な実装例のため省略していますが、余計なpropertyを生やさせないの例だと依存protocolの実装をpropertyとして利用できるため、Default Initializerを利用せず、依存protocolの実装クラスを利用するクラスから参照できないようにするといったアプローチが必要になります。