/ #swift4 #ios 

Strong type và bài toán qui đổi tiền tệ

Strong type là gì? Tại sao lại cần?

Strong type là cách ta định nghĩa ra một kiểu dữ liệu mới, từ những dữ liệu có sẵn nhưng mang tính định danh cao hơn. Ở các ngôn ngữ khác, ví dụ như haskell, người ta hay gọi đấy là Phantom types (kiểu bóng ma)

Một ví dụ điển hình và đơn giản nhất:

Ta có một struct Bank để gửi tiền

struct Bank {
    let money: Double
    init(_ money: Double) {
        self.money = money
    }
}

Ngân hàng chỉ cho gửi VND, nhưng chưa có cơ chế check. Ta gửi VND bình thường theo cách sau

Bank(100) // VND

Nhưng một hôm, ta bận và nhờ bạn gửi hộ, bạn lại gửi $

Bank(100) // vẫn được chấp nhận

Vậy nên ta cần một kiểu dữ liệu mới để người dùng chỉ nhập được duy nhất VND. Đây là lúc Strong type phát huy tác dụng.

Cách thức tạo Strong type

VND, USD, AUD là đơn vị tiền tệ của mỗi quốc gia. Ta coi mỗi loại tiền là 1 Unit, Ta cần một cái gì đó để xác định 3 Unit trên cùng chung một nhóm. Thế nên ta định nghĩa 2 protocol: UnitFamily tạo nhóm, MyUnit để định nghĩa các đơn vị trong nhóm.

// Define UnitFamily to group all of units have the same family
protocol UnitFamily {
    associatedtype BaseUnit
}

// Define MyUnit, own unit will be based on MyUnit protocol
protocol MyUnit {
    associatedtype Family: UnitFamily
    static var symbol: String { get }
    static var converted: UnitConverter { get }
}

Ở đây ta để ý rằng UnitFamily có chứa 1 type BaseUnit nhưng không sử dụng đến, tại sao lại cần đến nó? Như ta biết trong thực tế, bất cứ công thức qui đổi nào đều cần dùng một đơn vị chuẩn. Giống như F = ma, m luôn là kg, mọi khối lượng đều phải qui chuẩn về kg để đảm bảo tính đúng đắn của công thức. .

Thế nên BaseUnit có thể hiểu là một Unit bất kì trong group được lấy ra làm chuẩn.

Ngoài ta trong MyUnit còn có đối tượng dùng để convert dữ liệu là UnitConverter. Đối tượng này có sẵn trong thư viện của táo khuyết và được giới thiệu trong phiên bản ios 10.

Tiếp theo ta cần tạo ra Strong type để không nhầm lẫn Unit này với Unit khác.

// Unit wrapper, this one help us convert data type easier
struct MyMeasurement<UnitType: MyUnit> {
    let value: Double
    init(_ value: Double) {
        self.value = value
    }
}

UnitType không được sờ đến trong MyMeasurement, tác dụng của nó là định danh, qua đó hình thành Strong type, cũng có thể gọi là Phantom Types. Ta đã có Strong type theo cách rất đơn giản phải không? Ta sẽ đi sâu vào hiệu quả của Strong type sau, trước hết ta nối tiếp đến vấn đề convert.

Để convert được giữa các đơn vị theo cách chung nhất thì ta tạo extension cho MyMeasurement và có sử dụng generic.

extension MyMeasurement {
    func converted<TargetUnit>(to: TargetUnit.Type) -> MyMeasurement<TargetUnit> where
        TargetUnit: MyUnit, UnitType.Family == TargetUnit.Family {
            let valueInBase = UnitType.converted.baseUnitValue(fromValue: self.value)
            let convertValue = TargetUnit.converted.value(fromBaseUnitValue: valueInBase)
            return MyMeasurement<TargetUnit>(convertValue)
    }
}

Bây giờ ta bắt đầu định nghĩa các loại tiền với kiểu dữ liệu Enum. Các Unit VND, USD, AUD đều chung 1 nhóm (Family) MyCurrency.

// Group
enum MyCurrency: UnitFamily {
    typealias BaseUnit = VND
}

// Unit
enum VND: MyUnit {
    typealias Family = MyCurrency
    static var symbol: String {
        return "VND"
    }
    
    // chuẩn không cần chỉnh
    static var converted: UnitConverter = UnitConverterLinear(coefficient: 1)
}

enum USD: MyUnit {
    typealias Family = MyCurrency
    static var symbol: String {
        return "$"
    }
    
    static var converted: UnitConverter = UnitConverterLinear(coefficient: 1*22.5)
}

// more ...

Ta lấy VND để làm chuẩn cho việc qui đổi, thế nên ta khởi tạo đối tượng UnitConverter trong enum VND như sau

static var converted: UnitConverter = UnitConverterLinear(coefficient: 1)

Các đơn vị khác nhân theo tỉ giá đối với VND.

OK, mọi thứ đã được định nghĩa một cách ổn thỏa. Bây giờ sếp trả lương cho ta theo 2 đợt, đợt 1 lấy vnd, đợt 2 lấy dollar Úc, ta muốn qui đổi ra VND để tiêu cho dễ.

let vnd = MyMeasurement<VND>(20)
let aud = MyMeasurement<AUD>(10)

let salaryValue = vnd.value + aud.converted(to: VND.self).value
let salary = MyMeasurement<VND>(salaryValue)

Ta phải qua ngân hàng để chuyển đổi đồng dollar Úc qua VND nhưng điều đó quá bất tiện, ta yêu cầu ngân hàng tạo phương thức qui đổi tiền tệ cho mình. Tất nhiên là ngân hàng đáp ứng

func + <Unit1,Unit2>(lhs: MyMeasurement<Unit1>, rhs: MyMeasurement<Unit2>) -> MyMeasurement<VND>
    where
    Unit1: MyUnit
    , Unit2: MyUnit
    , Unit1.Family == Unit2.Family
    , Unit1.Family == VND.Family {
        let lhsToVND = Unit1.converted.baseUnitValue(fromValue: lhs.value)
        let rhsToVND = Unit2.converted.baseUnitValue(fromValue: rhs.value)
        return MyMeasurement<VND>(lhsToVND + rhsToVND)
}

Giờ ta chỉ cần thực hiện lệnh qua phần mềm để tự động chuyển về VND, nhẹ hơn 1 dòng :v

let vnd = MyMeasurement<VND>(20)
let aud = MyMeasurement<AUD>(10)
let newSalary = vnd + aud

OK giờ ta có lương, ta muốn tiết kiệm nên làm sổ tiết kiệm VND. Ta bận nên nhờ bạn đến gửi hộ (như lần trước), bạn mang theo $ nhưng lần này ngân hàng nâng cấp hệ thống kiểm tra đầu vào nên bạn không gửi tùy tiện được. Bank cảnh báo lỗi luôn

image

struct Bank {
    let money: MyMeasurement<VND>
    init(_ money: MyMeasurement<VND>) {
        self.money = money
    }
}

Để ý rằng money: Double được thay thế bằng MyMeasurement là Strong type

Nhưng về sau ta không muốn ra tận ngân hàng để gửi tiền nữa, muốn gửi trực tuyến. Ngân hàng lại cải tiến đáp ứng khách hàng bằng cách sử dụng tool ExpressibleByIntegerLiteralExpressibleByFloatLiteral mà Apple cung cấp.

extension MyMeasurement: ExpressibleByIntegerLiteral {
    init(integerLiteral value: IntegerLiteralType) {
        self.value = Double(value)
    }
}

extension MyMeasurement: ExpressibleByFloatLiteral {
    init(floatLiteral value: FloatLiteralType) {
        self.value = Double(value)
    }
}

Bây giờ thì ta chỉ cần thao tác

Bank(5.2) // ngân hàng vẫn check format VND

Mọi người có thể lấy full Source code ở đây.

Tham khảo: Measurement ở blog https://oleb.net/

Author

viethq

VietHQ - Tìm đơn giản trong phức tạp