随時更新します
既存のプロジェクトにすぐに導入できるちょっとしたリファクタリングのアイディアを集めてみました。
こうしたアイディアを積極的に取り入れて、シンプルで美しいコードを目指しましょう。
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
ビューとモデルと切り離して、ビューの再利用性を高めようという話。
例えば、ユーザのプロフィールを表示するカスタムセルを作る時、configure(user: User)
のように特定のモデルを受け取ってセルの設定を行うような関数を定義してしまうと、そのセルは特定のモデル専用になってしまう。
そうではなく、セルとモデルを受け取ってセルの設定を行うUserTableViewCellConfigurator
クラスという間の層を用意することで、セルとモデルを分離し、セルの再利用性を高めようというアイディア。
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
タプルの活用法をいくつか紹介してくれている。
一つだけ具体例を紹介(少しアレンジしました)。
例えば、ユーザ登録を行う関数に渡すパラメータの数は多くなりがちなので、これをタプルとして渡せるようにしてあげる。
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
データソースとデリゲートの実装を別のクラスに切り離す方法をチュートリアル形式で紹介してくれている。
これは自分にとっては非常に大きな発見で、真っ先に自分のプロジェクトに導入した。
データソースとデリゲートを別クラスに実装するのはとても簡単で、例えば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
テストしづらいコードをリファクタリングして、テストしやすいコードにしていく方法をチュートリアル形式で紹介してくれている。
題材となるクラスはこちら。
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)
}
}