Static factory methods in Swift

Trong quá trình lập trình, hầu hết đối tượng (views, controller,...) đều có một số thiết lập cần thiết trước khi chúng được sử dụng, và có khi các đối tượng đó có chung 1 thiết lập được sử dụng ở nhiều nơi khác nhau. Trong những trường hợp như vậy chúng ta thường để những thiết lập như vậy trong các hàm setupViews(), configureViewController() của những class con của UIView, UIViewController,...nơi chúng trực tiếp được sử dụng.

Và thông thường, với những thiết lập được đặt ở nhiều nơi như vậy, chúng ta sẽ khó tránh khỏi việc code bị trùng lặp hay sẽ làm cho class của chúng ta trở nên dài hơn, đọc code sẽ vất vả hơn. Vậy bài viết này sẽ giới thiệu với các bạn một cách tiếp cận khác để giải quyết những vấn đề này, giúp cho việc viết code của chúng ta trở nên đơn giản, gọn gàng hơn bằng cách sử dụng static factory methods.

Views

Một trong những đối tượng phổ biến mà chúng ta thường xuyên phải thiết lập khi viết UI code đó chính là views. Cả UIKit trên iOS và AppKit trên Mac đều cung cấp cho chúng ta tất cả các hàm khởi tạo cần thiết để tạo ra giao diện người dùng, tuy nhiên với giao diện gốc chúng ta chưa thể sử dụng được luôn mà cần phải tùy chỉnh để phù hợp với thiết kế và bố cục mà chúng ta mong muốn. Hầu hết các lập trình viên sẽ chọn cách để tất cả những thiết lập cho views đó vào trong hàm khởi tạo, như sau:

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

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

Dĩ nhiên, cách tiếp cận này không có gì sai cả, tuy nhiên chúng ta sẽ cần phải tạo ra khá nhiều các class khác nhau hoặc kế thừa với nhiều lớp để tạo ra những View chỉ với một chút sai khác so với view ban đầu (giống như: TitleLabel, SubtitleLabel, FeaturedTitleLabel,...).

Trong khi tính kế thừa là một tính chất quan trọng của Lập trình hướng đối tượng, chúng thường dễ nhầm lẫn giữa custom setup và custom behavior. Sự thật là chúng ta không tạo ra 1 behavior mới cho UILabel bên trên, chúng ta chỉ tạo ra 1 cấu hình mới cho UILabel. Vậy câu hỏi đặt ra là liệu việc tạo ra 1 subclass có thực sự là 1 giải pháp tốt cho trường hợp trên.

Chúng ta hãy thử sử dụng static factory method để giải quyết trường hợp trên xem sao, đơn giản như tên gọi của nó, nó là một hàm tĩnh với đầu ra một đối tượng UILabel mà chúng ta mong muốn:

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
    }
}

Cách tiếp cận này giúp chúng ta tránh việc phải tạo ra quá nhiều các subclass khác nhau mà đơn giản chỉ để tạo ra các views khác nhau. Thậm chí chúng ta có thể set private để áp dụng extension riêng cho một file cụ thể như sau:

// We'll only use this in a single view controller, so we'll scope
// it as private (for now) as to not add this functionality to
// UIButton globally in our app.
private extension UIButton {
    static func makeForBuying() -> UIButton {
        let button = UIButton()
        ...
        return button
    }
}

Với việc sử dụng static factory method, chúng ta sẽ thấy những dòng code khởi tạo sẽ trở nên gọn gàng hơn rất nhiều:

class ProductViewController {
    private lazy var titleLabel = UILabel.makeForTitle()
    private lazy var buyButton = UIButton.makeForBuying()
}

Thậm chí nếu chúng ta muốn những dòng code khởi tạo trông tối giản hơn nữa, chúng ta có thể thay vì viết methods, thì sẽ viết dạng computed properties, như sau:

extension UILabel {
    static var title: UILabel {
        let label = UILabel()
        ...
        return label
    }
}

class ProductViewController {
    private lazy var titleLabel = UILabel.title
    private lazy var buyButton = UIButton.buy
}

Tuy nhiên, việc sử dụng static computed properties sẽ có hạn chế là chúng ta không thể truyền thêm tham số như methods, nên chúng thường được sử dụng trong các trường hợp đơn giản.

View controllers

Bên cạnh views thì view controllers cũng là những đối tượng thường xuyên tạo ra những subclass khác nhau, mà thông thường việc tạo ra chúng là không thể tránh khỏi, vì ngoài việc thiết lập custom views, chúng cũng thường có behavior khác nhau giữa các subclasses. Tuy nhiên, cách tiếp cận với static factory method cũng sẽ giúp ích rất nhiều cho việc viết code đơn giản hơn.

Đặc biệt khi sử dụng child view controllers, chúng ta thường kết thúc với một nhóm view controllers mà chỉ để hiển thị một trạng thái nhất định - thay vì có nhiều logic trong đó. Đối với những view controllers này, việc chuyển thiết lập của chúng sang static factory API có thể là một giải pháp khá hay. Dưới đây, chúng ta sẽ sử dụng phương pháp đó để triển khai computed property trả về 1 loading view controller mà chúng ta sẽ sử dụng để thể hiện trạng thái loading:

extension UIViewController {
    static var loading: UIViewController {
        let viewController = UIViewController()

        let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
        indicator.translatesAutoresizingMaskIntoConstraints = false
        indicator.startAnimating()
        viewController.view.addSubview(indicator)

        NSLayoutConstraint.activate([
            indicator.centerXAnchor.constraint(
                equalTo: viewController.view.centerXAnchor
            ),
            indicator.centerYAnchor.constraint(
                equalTo: viewController.view.centerYAnchor
            )
        ])

        return viewController
    }
}

Như bạn có thể thấy ở trên, chúng ta thậm chí có thể thiết lập Auto Layout bên trong các thuộc tính hoặc hàm tĩnh của chúng ta. Đây là một tình huống trong đó tính chất khai báo của Auto Layout thực sự có ích - chúng ta có thể chỉ định tất cả constraints trực tiếp, mà không cần phải override lên bất kỳ phương thức nào hoặc respond to any calls. Giống như khi sử dụng với view, phương pháp tiếp cận factory cung cấp cho chúng ta các API để gọi rất đẹp và sạch sẽ:

class ProductListViewController: UIViewController {
    func loadProducts() {
        let loadingVC = add(.loading)

        productLoader.loadProducts { [weak self] result in
            loadingVC.remove()
            self?.handle(result)
        }
    }
}

Kết luận

Sử dụng các static factory methods and properties để thực hiện thiết lập các đối tượng có thể là cách tuyệt vời để tách biệt setup code khỏi logic, để đọc code dễ dàng, ngắn gọn và dễ test hơn. Trong khi subclassing vẫn là 1 công cụ quan trọng - đặc biệt là khi chúng ta muốn thêm những logic mới vào 1 Type - thì việc tránh subclasses chỉ để thực hiện 1 vài thiết lập sẽ giúp cho code base dễ dàng điều hướng hơn, ít class thừa hơn, dễ dàng maintain hơn.

Hy vọng với cách tiếp cận trên, bạn sẽ cảm thấy dễ dàng hơn trong việc viết code, giúp code của chúng ta càng thêm trong sáng. Bài viết được dịch từ blog swiftbysundell, bạn có thể tham khảo bài viết gốc ở link bên dưới:

Bài gốc: https://www.swiftbysundell.com/posts/static-factory-methods-in-swift