post Image
すぐに使える!Swiftリファクタリングアイディア集

:star2:随時更新します:star2:

既存のプロジェクトにすぐに導入できるちょっとしたリファクタリングのアイディアを集めてみました。
こうしたアイディアを積極的に取り入れて、シンプルで美しいコードを目指しましょう。

How to slim down your viewDidLoad() method

https://clean-swift.com/how-to-slim-down-your-viewdidload-method/

ネットワーク通信やビューの設定等、多くの初期化処理で膨らんだviewDidLoad()メソッドをスリムにしようという話。

override func viewDidLoad() {
    super.viewDidLoad()

    // 各ビューの設定など...

    // APIからデータを取得
    let request = ListOrders.FetchOrders.Request()    
    interactor?.fetchOrders(request: request)

    // 他の何か...
}

このようなviewDidLoad()に対しては、まず意味のあるまとまりごとにプライベート関数化する。
こうすることで、あとでviewDidLoad()を見たときに、各関数の詳細を見なくても名前を見ればどんなことをしてるかがわかる。

override func viewDidLoad() {
    super.viewDidLoad()

    setupViews()
    fetchOrders()
    setupOther()
}

private func setupViews() {...}

private func fetchOrders() {
   let request = ListOrders.FetchOrders.Request()    
    interactor?.fetchOrders(request: request)
}

private func setupOther() {...}

Preventing views from being model aware in Swift

https://www.swiftbysundell.com/posts/preventing-views-from-being-model-aware-in-swift

ビューとモデルと切り離して、ビューの再利用性を高めようという話。
例えば、ユーザのプロフィールを表示するカスタムセルを作る時、configure(user: User)のように特定のモデルを受け取ってセルの設定を行うような関数を定義してしまうと、そのセルは特定のモデル専用になってしまう。
そうではなく、セルとモデルを受け取ってセルの設定を行うUserTableViewCellConfiguratorクラスという間の層を用意することで、セルとモデルを分離し、セルの再利用性を高めようというアイディア。

Static factory methods in Swift

https://www.swiftbysundell.com/posts/static-factory-methods-in-swift

UILabelのフォントを大きくしたり、UIButtonの色を変えたりといった初期化処理はこれらのサブクラスのイニシャライザで行うのが一般的である。
この記事では、サブクラスを作るのではなくstatic factoryメソッドを使うというアプローチを紹介している。

以下のように、フォントや文字色をカスタマイズした新しいUILabelクラスを作成するというのは一般的であるが、このやり方だと例えばフォントの大きさだけを変えた同じようなクラス(SubTitleLabelとか)をたくさん作ることになってしまう。

class TitleLabel: UILabel {
    override init(frame: CGRect) {
        super.init(frame: frame)

        font = .boldSystemFont(ofSize: 24)
        textColor = .darkGray
        adjustsFontSizeToFitWidth = true
        minimumScaleFactor = 0.75
    }
}

代わりに、UILabelクラスのエクステンションとしてstatic factoryメソッドを実装する。
これによってTitleLabelやSubTitleLabelなどのサブクラスが乱立することがなくなる。

extension UILabel {
    static func makeForTitle() -> UILabel {
        let label = UILabel()
        label.font = .boldSystemFont(ofSize: 24)
        label.textColor = .darkGray
        label.adjustsFontSizeToFitWidth = true
        label.minimumScaleFactor = 0.75
        return label
    }

    static func makeForSubtitle() -> UILabel {...}
    static func makeForFeaturedTitle() -> UILabel {...}
}

エクステンションであることのメリットとして、スコープを決められることが挙げられる。
例えば1つの画面でしか使わないようなラベルのstatic factoryメソッドを、その画面のViewControllerと同じファイルでprivateなエクステンションとして定義してあげることができる。

なお、static factoryメソッドではなくstatic getterプロパティとして定義することで、よりシンプルにできる。

この記事では他にも、static factoryメソッドを使ってViewControllerやテストスタブを作成することについても取り上げている。

Using tuples as lightweight types in Swift

https://www.swiftbysundell.com/posts/using-tuples-as-lightweight-types-in-swift

タプルの活用法をいくつか紹介してくれている。

一つだけ具体例を紹介(少しアレンジしました)。
例えば、ユーザ登録を行う関数に渡すパラメータの数は多くなりがちなので、これをタプルとして渡せるようにしてあげる。

class API {
    typealias CreateUserParam = (email: String, password: String, name: String)

    func createUser(path: String, param: CreateUserParam) {
        request(path, param.email, param.password, param.name)
    }
}

パラメータとそれ以外の区別がつけやすく、呼び出し側のコードがすっきりする。

呼び出し側
let api = API()
let userParam = ("a@example.com", "passw0rd", "TestUser")
// let userParam = (email: "a@example.com", password: "passw0rd", name: "TestUser") ラベルはつけてもOK
api.createUser(path: "/users", param: userParam)

将来的にパラメータが増えた時、関数のシグネチャが変わらないので呼び出し側コードの変更が楽になるというメリットもある。

他にも、タプルの比較に==が使えることや、クロージャの引数がタプルであることを利用した実装方法なども紹介されている。

How to move data sources and delegates out of your view controllers

https://www.hackingwithswift.com/articles/86/how-to-move-data-sources-and-delegates-out-of-your-view-controllers

データソースとデリゲートの実装を別のクラスに切り離す方法をチュートリアル形式で紹介してくれている。
これは自分にとっては非常に大きな発見で、真っ先に自分のプロジェクトに導入した。

データソースとデリゲートを別クラスに実装するのはとても簡単で、例えばUITableViewDataSourceプロトコルに準拠したクラスを作るには以下のようにする。

一部を抜粋
class ObjectDataSource: NSObject, UITableViewDataSource {
    var objects = [Any]()

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return objects.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

        let object = objects[indexPath.row] as! NSDate
        cell.textLabel!.text = object.description
        return cell
    }
}

あとはTableViewを持つViewController側で、このクラスのインスタンスをデータソースに指定するだけ。

一部を抜粋
class ViewController: UITableViewController {
    var dataSource = ObjectDataSource()

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.dataSource = dataSource
        ...

別クラスに切り出すことで、ViewControllerの実装が軽くなるだけでなく、テストも書きやすくなる。

Refactoring Swift code for testability

https://www.swiftbysundell.com/posts/refactoring-swift-code-for-testability

テストしづらいコードをリファクタリングして、テストしやすいコードにしていく方法をチュートリアル形式で紹介してくれている。

題材となるクラスはこちら。

class ShoppingCart {
    static let shared = ShoppingCart()

    private var products = [Product]()
    private var coupon: Coupon?

    func add(_ product: Product) {
        products.append(product)
    }

    func apply(_ coupon: Coupon) {
        self.coupon = coupon
    }

    func startCheckout() {
        var finalPrice = products.reduce(0) { price, product in
            return price + product.cost
        }

        if let coupon = coupon {
            let multiplier = coupon.discountPercentage / 100
            let discount = Double(finalPrice) * multiplier
            finalPrice -= Int(discount)
        }

        App.router.openCheckoutPage(forProducts: products, finalPrice: finalPrice)
    }
}

このクラスは以下の問題がある。

  • 金額の計算結果がテストできない
  • グローバルなAPI(App.router)をメソッド内部で直接呼び出しているので、これをモック化できない

こうした問題に対して、

  • 金額の計算ロジックを抽出して純粋関数にする
  • App.routerのメソッドをプロトコルに抽出し、App.routerを外部からインジェクションできるようにする

というアプローチでテストしやすいクラスにしていく。
個人的に、後者はとても興味深い。

最終的にはこんなクラスになる。

class ShoppingCart {
    private var products = [Product]()
    private var coupon: Coupon?

    func add(_ product: Product) {
        products.append(product)
    }

    func apply(_ coupon: Coupon) {
        self.coupon = coupon
    }

    private let checkoutPageOpener: CheckoutPageOpener

    init(checkoutPageOpener: CheckoutPageOpener = App.router) {
        self.checkoutPageOpener = checkoutPageOpener
    }

    func startCheckout() {
        let finalPrice = PriceCalculator.calculateFinalPrice(for: products, applying: coupon)

        checkoutPageOpener.openCheckoutPage(forProducts: products, finalPrice: finalPrice)
    }
}

『 Swift 』Article List