Default arguments trong Swift.
Bài đăng này đã không được cập nhật trong 5 năm
- Những đặc điểm của thiết kế chặt chẽ, mạnh mẽ linh hoạt của APIlà nhẹ nhàng mà dễ sử dụng đang là yếu tố quyết định để người dùng có thể tuỳ chỉnh. Mặc khác thì chúng ta càng thêmconfigcấu hình nhiều thìAPIcàng trở nên linh hoạt hơn nhưng đồng thời nó cũng trở nên phức tạp hơn và khó hiểu hơn.
- Điều đó khiến default argumentstrở nên hữu dụng trongSwiftbằng việc cho phép chúng ta thêm các mặc định sẵn có một cách chắc chắn và trực quan hơn. Chúng ta sẽ đạt được sự cân bằng tốt giữa tính linh hoạt và dễ sử dụng. Chúng ta sẽ cùng xem xét những ví dụ dưới đây:
1/ Lựa chọn phương án tối ưu dễ dàng:
- Một trong những phương án quan trọng mà dự án có thể thực hiện việc bảo trì dễ dàng hơn(khi mà dự án tăng về kích thước, vể cả codelẫndeveloper) là đảm bảo thực hiện các phương áncodetối ưu đúng đắn nhất, dễ dàng nhất. Các chức năng phổ biến sẽ phải hạn chế được triển khai lại nhiều lần bởi cácdeveloperkhác nhau bằng việc triển khai có sự trừu tượng hoá, dễ sử dụng lại.
extension UIResponder {
    // Here we use a default argument to define what we want
    // our unified, default animation duration to be:
    func animate(withDuration duration: TimeInterval = 0.3,
                 animations: @escaping () -> Void) {
        UIView.animate(withDuration: duration,
                       animations: animations)
    }
}
- Với đoạn codetrên, hầu hết cácanimationsẽ trở nên giống như sau:
animate {
    button.frame.size = CGSize(width: 100, height: 100)
}
- Điều đó khá tốt, dễ đọc và vì chúng ta chỉ một một sourceduy nhất trong quá trình cácanimationmặc định chạy. Tuy nhiên có một điều quan trọng không kém là mặc định mới của chúng ta dễ dàng bịoverridelại:
animate(withDuration: 2) {
    button.frame.size = CGSize(width: 100, height: 100)
}
- Bên cạnh việc cung cấp các chuẩn hoá các giá trị khác nhau trên source code, chúng ta có thể thiết kếAPIđể có thể mở rộng, băng việc thêm các trường hợp phức tạp và cácconfigcó thể mà không cần yêu cầu tất cả người dùngAPIsử dụng tất cả nhữngconfig, trường hợp phức tạp đó.
- Ví dụ như cách chúng ta có thể mở rộng API animationđể hỗ trợ những trường hợp sử dụng khác mà vẫn sử dụng những mặc định đơn giản có thể:
extension UIResponder {
    func animate(withDuration duration: TimeInterval = 0.3,
                 delay: TimeInterval = 0,
                 options: UIView.AnimationOptions = .curveEaseInOut,
                 animations: @escaping () -> Void) {
        UIView.animate(withDuration: duration,
                       delay: delay,
                       options: options,
                       animations: animations)
    }
}
2/ Tầm quan trọng của việc định danh rõ ràng:
- Khi quyết định những giá trị nào trở thành mặc định, điều quan trọng là xem xét một mặc định cụ thể có thể trở nên trực quan với người dùng APIcủa chúng ta hay không. Những giá trị mặc định tốt nhất là những giá trị được định danh rõ ràng tránh gây cho chúng ta những hiểu lầm là lỗi do API gây ra những lỗi không mong đợi.
- Ví dụ, giả sử chúng ta khai báo chúng ta đã viết 1 hàm để lưu giá trị trong databasevà chúng ta cho phép người dùngAPIquyết định cách xử lý những xung đột khi có một giá trị tương tự tồn tại trongdatabase:
enum ConflictResolution {
    case overwriteExisting
    case stopIfExisting
    case askUser
}
func store<T: Storable>(
    _ value: T,
    conflictResolution: ConflictResolution = .stopIfExisting
) throws {
    ...
}
- 
Điều trên trông có vẻ tốt nhưng khi chúng ta suy nghĩ về nó, nó không thực sự rõ ràng khi chúng ta gọi chức năng không xác định rõ ràng ConflicResolutionsẽ hiển thị không giá trị nào được lưu nếudatabasecủa chúng ta không tồn tại 1 giá trị. Đơn giản với việc gọitry store(value), chúng ta mong muốn một giá trị thực sự được lưu giữ nhưng đồng thời chúng ta muốn ghi đè lên giá trị mặc định hiện tại -> điều đó gây mất dữ liệu không mong muốn.
- 
Trong tình huống này, khi không tìm thấy giá trị mặc định rõ ràng thì chúng ta nên xác định chức năng riêng nếu chúng ta muốn cung cấp API.
func storeIfNeeded<T: Storable>(_ value: T) throws {
    try store(value, conflictResolution: .stopIfExisting)
}
3/ Thêm các ràng buộc:###
- 
Các đối số mặc định có thể cho phép trang bị thêm một typehoặcfuncvới các ràng buộc thêmdependency injection. Như chúng ta đã biết vềdependency injectionthì thay vì phụ thuộc vàosingleton, chúng ta có cách để viết code với cấu trúc tốt hơn và có thể kiểm tra. Tuy nhiên hoàn toàn chỉnh sửa code để giới thiệu các ràng buộc là một nhiệm vụ lớn nhưng nhờdefault argumentthì việc đó được thực hiện từng bước một.
- 
Nếu như chúng ta sử dụng FileLoaderclass cũng như việc sử dụngCachenhưsingleton. Điều đó mang lại cho chúng ta chút lợi ích trong việc đơn giản hoá việc khởi tạoFileLoadertừ bất kì đâ mà không cần quan tâm đến các ràng buộc những sẽ gây khó khăn cho việc unit test cũng như việc đánh giá loại ràng buộc nào.
- 
Tin tốt là bằng việc biến đổi đơn giản chúng ta có thể dễ sử dụng các singleton:
class FileLoader {
    private let fileManager: FileManager
    private let cache: Cache
    init(fileManager: FileManager = .default,
         cache: Cache = .shared) {
        self.fileManager = fileManager
        self.cache = cache
    }
}
- Vì chúng ta đã truyền tham số cho các ràng buộc của các trình tải tập tin nên chúng ta có thể dễ dàng kiểm tra:
let loader = FileLoader(cache: .autoEmptyingForTests)
4/ Sử dụng giá trị liên kết trong enum:
- Ví dụ chúng ta đang xây dựng một libsử dụng XML. Với XML như một cây với dạng các dât sẽ được sử dụngnodevới biến thể hữu hạn, chúng ta có thể chọn cácmodelsử dụngXMLNodenhư sau:
enum XMLNode {
    // A standard element, which can contain child elements:
    case element(
        name: String,
        attributes: [Attribute],
        children: [XMLNode]
    )
    // A "void" element that closes itself, and can't have children:
    case voidElement(
        name: String,
        attributes: [Attribute]
    )
    // An inline piece of text, defined as a child node:
    case text(String)
}
- Trước Swift 5.1việc sử dụng cách trên đi kèm với sự rủi ro lớn là không có đối số mặc định nào có thể được xác định.
let emptyItems = XMLNode.element(
    name: "items",
    attributes: [],
    children: []
)
- Mặc dù chúng ta có thể giới thiệu XMLNodeđể mở rộngAPIvới việc điển các giá trị mặc định trống. Chúng ta có thể xác định các đối số mặc định cho các giá trịenumliên kết:
enum XMLNode {
    case element(
        name: String,
        attributes: [Attribute] = [],
        children: [XMLNode] = []
    )
    case voidElement(
        name: String,
        attributes: [Attribute] = []
    )
    case text(String)
}
- Với sự thay đổi trên, API của XMLNodetrở nên linh động hơn nhiều và chúng ta có thể xác định các loạinodechỉ với việc sử dụng loạienumtrên.
let emptyItems = XMLNode.element(name: "items")
let link = XMLNode.element(name: "link", children: [.text(url)])
let metadata = XMLNode.voidElement(name: "meta", attributes: metadataAttributes)
All rights reserved
 
  
 