Pure functions in Swift
Bài đăng này đã không được cập nhật trong 6 năm
- 
Pure functionlà một trong những khái niệm cốt lõi của việc lập trình cho phép hầu hết các ngôn ngữ lập trình có thể hỗ trợ các biểu mẫu dưới dạng function hoặc các chương trình con(subroutines).
- 
1 function được gọi là pure functionkhi mà nó không gây ra các hiệu ứng phụ và không chịu tác động bởi các trạng thái bên ngoài(external state). Ý tưởng cơ bản là một pure function sẽ luôn luôn cho ra cácoutputgiống nhau cho cácinputkhác nhau - mặc cho nó được gọi đến bao nhiêu lần.
1/ Purifying functions:
- Hãy bắt đầu với một ví dụ mà function có khả năng trở thành pure functionngoại từ việc nó không đảm bảo không gây ra hiệu ứng phụ nào:
extension String {
    mutating func addSuffixIfNeeded(_ suffix: String) {
        guard !hasSuffix(suffix) else {
            return
        }
        append(suffix)
    }
}
- Trường hợp ở trên thì function là mutatingcó vẻ không phải là vấn đề to tát gì vìStringlàvalue typenên chúng ta có thể coi nó nhưmutable values:
var fileName = contentName
fileName.addSuffixIfNeeded(".md")
try save(content, inFileNamed: fileName)
- Thay vì rút gọn function bằng cách returnmột giá trịStringmới thay vì thay đổi 1 giá trị đã được gọi đến trước đó:
extension String {
    func addingSuffixIfNeeded(_ suffix: String) -> String {
        guard !hasSuffix(suffix) else {
            return self
        }
        return appending(suffix)
    }
}
- Chỉ là một thay đổi nhỏ từ đoạn code trên nhưng nó có thể hạn chế tối thiểu các statecó thể thay đổi, có thể coi là bản nâng cấp nhỏ của đoạn code trên:
let fileName = contentName.addingSuffixIfNeeded(".md")
try save(content, inFileNamed: fileName)
- Một điều khác có thể cản trở function có thể là một pure functionlà nếu nó phụ thuộc vào một vài mẫu bên ngoài, cácstatethay đổi. VD: nếu chúng ta xây dựng một màn hình login của app và chúng ta muốn hiển thịerror messagetrong trường hợp lặp đi lặp lại lỗi login. Function có thể trông như sau:
extension LoginController {
    func makeFailureHelpText() -> String {
        guard numberOfAttempts < 3 else {
            return "Still can't log you in. Forgot your password?"
        }
        return "Invalid username/password. Please try again."
    }
}
- Function bên trên có vẻ phụ thuộc vào property numberOfAttemptscủaviewcontroller. Chúng ta có thể tạm coi nó làpure functionngoại trừ việc nó có thể cho ra các giá trị khác nhau phụ thuộc vào biến thay đổi:
- Chúng ta sẽ thay đổi nhỏ để fix đoạn code trên:
extension LoginController {
    func makeFailureHelpText(numberOfAttempts: Int) -> String {
        guard numberOfAttempts < 3 else {
            return "Still can't log you in. Forgot your password?"
        }
        return "Invalid username/password. Please try again."
    }
}
- Một lợi ích to lớn của pure function là nó thực dễ để test - chúng ta có thể dễ dàng xác nhận là nó cho ra đúng output với bất kì inputnào. Ví dụ dưới dây sẽ cho chúng ta có thể dễ test các function trên của chúng ta khi truyền vào cácnumberOfAttemptskhác nhau:
class LoginControllerTests: XCTestCase {
    func testHelpTextForFailedLogin() {
        let controller = LoginController()
        XCTAssertEqual(
            controller.makeFailureHelpText(numberOfAttempts: 0),
            "Invalid username/password. Please try again."
        )
        XCTAssertEqual(
            controller.makeFailureHelpText(numberOfAttempts: 3),
            "Still can't log you in. Forgot your password?"
        )
    }
}
2 / Enforcing purity:
- Nhờ vào pure-functioncó nhiều lợi ích mà trong quá trình coding chúng ta có thể sử dụng một số trick để biết chính xác khi nào function nào là pure khi mà hầu hết code của chúng ta viết đều phụ thuộc vào rất nhiều state khác nhau:
- Tuy nhiên ở một mức độ nào đó, thì pure-function bó buộc chúng ta vào các value type. Nếu các value không thể thay đổi chính nó hoặc không có cácpropertyở ngoàimutating-function, nó vẫn đảm bảo cho logic của chúng ta thay vì sử dụngpure-function.
- VD là chúng ta sẽ có logic cgho việc tính toàn tổng tiền mua danh sách các sản phẩm, sử dụng structchỉ bao gồm cácletproperty:
struct PriceCalculator {
    let shippingCosts: ShippingCostDirectory
    let currency: Currency
    func calculateTotalPrice(for products: [Product],
                             shippingTo region: Region) -> Cost {
        let productCost: Cost = products.reduce(0) { cost, product in
            return cost + product.price
        }
        let shippingCost = shippingCosts.shippingCost(
            forRegion: region
        )
        let totalCost = productCost + shippingCost
        return totalCost.convert(to: currency)
    }
}
3 / Purifying refactors:
- Hãy cùng xem ví dụ chúng ta xử lý tap của next-buttonvới các bài đọc trênReaderViewController. Phụ thuộc vàoviewcontrollerđang cóstatenào mà chúng ta có thể hiển thị bài đọc tiếp theo cho người dùng, show ra các mã promotion hoặc tắt đi bài đọc hiện tại:
private extension ReaderViewController {
    @objc func nextButtonTapped() {
        guard !articles.isEmpty else {
            return didFinishArticles()
        }
        let vc = ArticleViewController()
        vc.article = articles.removeFirst()
        present(vc)
    }
    func didFinishArticles() {
        guard !promotions.isEmpty else {
            return dismiss()
        }
        let vc = PromotionViewController()
        vc.promotions = promotions
        vc.delegate = self
        present(vc)
    }
}
- Bên trên không phải là "bad code", nó có vẻ dễ đọc và nó thậm chí chia nhỏ thành 2 function riêng biệt để nó dễ đọc và dễ review. Ngoại trừ phía trên nextButtonTappedfunction không phải là pure, nó khó có thể test.
- Logic như trên có vẻ dễ gây ra vấn đề "Massive View Controller" khi mà view controller đảm nhiểm quá nhiều vai trò lẫn logic phức tạp.
- Thay vì chia nhỏ logic trên vào trong pure function, chúng ta có 1 nơi chỉ chứa logic của button đó. Bằng cách đó chúng ta có thể trình bày logic của chúng ta như mộtpure functiontừstatecho đến outcome và sử dụngstatic function.:
struct ReaderNextButtonLogic {
    enum Outcome {
        case present(UIViewController, remainingArticles: [Article])
        case dismiss
    }
    static func outcome(
        forArticles articles: [Article],
        promotions: [Promotion],
        promotionDelegate: PromotionDelegate?
    ) -> Outcome {
        guard !articles.isEmpty else {
            guard !promotions.isEmpty else {
                return .dismiss
            }
            let vc = PromotionViewController()
            vc.promotions = promotions
            vc.delegate = promotionDelegate
            return .present(vc, remainingArticles: [])
        }
        var remainingArticles = articles
        let vc = ArticleViewController()
        vc.article = remainingArticles.removeFirst()
        return .present(vc, remainingArticles: remainingArticles)
    }
}
- Điều gì thực sự quan trọng ở đoạn code trên thì đó là nó thực hiện đầy đủ các thứ mà viewcontrollertrước đó đã làm ngoại trừ là nó thay đổi các trạng thái.
private extension ReaderViewController {
    @objc func nextButtonTapped() {
        let outcome = ReaderNextButtonLogic.outcome(
            forArticles: articles,
            promotions: promotions,
            promotionDelegate: self
        )
        switch outcome {
        case .present(let vc, let remainingArticles):
            articles = remainingArticles
            present(vc)
        case .dismiss:
            dismiss()
        }
    }
}
- Chúng ta còn cần tinh chỉnh một chút cho đoạn code trên nhưng đoạn code trên đã có thể dễ dàng test hơn và cho outcome đúng khi button được tap:
class ReaderNextButtonLogicTests: XCTestCase {
    func testNextArticleOutcome() {
        let articles = [Article.stub(), Article.stub()]
        let outcome = ReaderNextButtonLogic.outcome(
            forArticles: articles,
            promotions: [],
            promotionDelegate: nil
        )
        guard case .present(let vc, let remaining) = outcome else {
            return XCTFail("Invalid outcome: \(outcome)")
        }
        XCTAssertTrue(vc is ArticleViewController)
        XCTAssertEqual(remaining, [articles[1]])
    }
}
- Vẫn còn rất nhiều cách để có thể đóng gói logic như trên bằng việc sử dụng logic-controllers hay view-models. Chỉ với việc di chuyển logic chúng ta đã làm cho công việc test trở nên dễ dàng hơn so với thay đổi cả cấu trúc code hoặc kỹ thuật.
All rights reserved
 
  
 