Phần 1:https://techmaster.vn/posts/34609/xu-ly-code-lom-phan-1

Ở bài này chúng ta sẽ sử dụng các biện pháp nâng cao hơn để giải quyết mã lởm.Sử dụng Dependency injection, Strategy và factory design pattern

Đoạn mã sau khi chúng ta refactor sẽ như sau:

class DiscountManager
{
    private var _factory: PAccountDiscountCalculatorFactory!
    private var _loyaltyDiscountForCalculator: PLoyaltyDiscountCalculator!
    
    func init(factory:PAccountDiscountCalculatorFactory, loyaltyDiscountCalculator: PLoyaltyDiscountCalculator)
    {
        _factory = factory
        _loyaltyDiscountForCalculator = loyaltyDiscountCalculator
    }
    
    func applyDiscount(price: Double, accountStatus: AccountStatus, timeOfHavingAccountInYears: Int) -> Double
    {
        var priceAfterDiscount: Double = 0
        priceAfterDiscount = _factory.getAccountDiscountForCalculator(accountStatus: accountStatus).applyDiscount(price: price, timeOfHavingAccountInYears: timeOfHavingAccountInYears)
        priceAfterDiscount = _loyaltyDiscountCalculator.applyDiscount(price: priceAfterDiscount, timeOfHavingAccountInYears: timeOfHavingAccountInYears)
        return priceAfterDiscount;
    }
}

protocol PLoyaltyDiscountCalculator
{
    func applyDiscount(price: Double, timeOfHavingAccountInYears: Int) -> Double
}

class DefaultLoyaltyDiscountForCalculator: PLoyaltyDiscountCalculator
{
    func applyDiscount(price: Double, timeOfHavingAccountInYears: Int) -> Double {
        price.applyDiscountForTime(discountSize: price, timeHavingAccountInYears: timeOfHavingAccountInYears)
    }
}

protocol PAccountDiscountCalculator
{
    func applyDiscount(price: Double) -> Double
}

class NotRegisteredDiscountForCalculator: PAccountDiscountCalculator
{
    func applyDiscount(price: Double) -> Double {
        return price
    }
}
class SimpleCustomerDiscountForCalculator: PAccountDiscountCalculator
{
    func applyDiscount(price: Double) -> Double {
        return price - (Constants.DISCOUNT_SIMPLE_CUSTOMERS * price)
    }
}
class ValuableCustomerDiscountForCalculator: PAccountDiscountCalculator
{
    func applyDiscount(price: Double) -> Double {
        return price - (Constants.DISCOUNT_VALUABLE_CUSTOMERS * price)
    }
}
class MostValuableCustomerDiscountForCalculator: PAccountDiscountCalculator
{
    func applyDiscount(price: Double) -> Double {
        return price - (Constants.DISCOUNT_MOST_VALUABLE_CUSTOMERS * price)
    }
}

protocol PAccountDiscountCalculatorFactory
{
    func getAccountDiscountForCalculator(accountStatus: AccountStatus) -> PLoyaltyDiscountCalculator
}

class DefaultAccountDiscountForCalculatorFactory: PAccountDiscountCalculatorFactory
{
    func getAccountDiscountForCalculator(accountStatus: AccountStatus) -> PLoyaltyDiscountCalculator {
        var calculator: PAccountDiscountCalculator!
        switch (accountStatus)
        {
        case AccountStatus.NotRegistered:
            calculator = NotRegisteredDiscountForCalculator()
            break
        case AccountStatus.SimpleCustomer:
            calculator = SimpleCustomerDiscountForCalculator()
            break
        case AccountStatus.ValuableCustomer:
            calculator = ValuableCustomerDiscountForCalculator()
            break
        case AccountStatus.MostValuableCustomer:
            calculator = MostValuableCustomerDiscountForCalculator()
            break
        default:
            //throw error
        }
        return priceAfterDiscount
    }
}


Quay trở lại bàn về các đoạn mã ở bài trước, với class DiscountManager trong nó được viết các đoạn mã xử lý logic, đặt ra giả thiết giờ mình chỉ muốn unit test với phần ApplyDiscount đối với satus, thì nó không khả thi vì chúng ta sẽ cần test luôn cả discount for time.
Để giải quyết thì tôi đã tạo DefaultLoyaltyDiscountForCalculator class nó sẽ chứa logic của ApplyDiscountForTime vào hàm apply của protocol PLoyaltyDiscountCalculator, vậy khi chung ta muốn test DiscountManager class thì chúng ta dễ dàng mock một đối tượng được implement theo PLoyaltyDiscountCalculator thông qua phương thức khởi tạo của DiscountManager class(ở đây sử dụng Dependency Injection). Vì sử dụng abtraction nên chúng ta dễ dàng mock các đối tượng cũng như chỉnh logic mà k bị ảnh hưởng đến nhưng đối tượng khác.

Để làm điều này thì chúng ta sẽ chuyển phần logic của phần tính toán giảm giá(Loyalty) cho 1 class khác, như vậy thì trong trường hợp chúng ta cần thay đổi logic thì chúng ta chỉ cần thay đổi DefaultLayaltyDiscountCalculator class, các đoạn mã khác sẽ không bị ảnh hưởng, giảm thiểu rủi ro và thời gian test.Ví dụ mình có DefaultLayaltyDiscountForCalculator1DefaultLayaltyDiscountForCalculator2,... khi cần thay đổi data mock mình cần thay đổi 1 trong các thằng đấy thôi.

DiscountManager class tôi đã chia nhỏ logic: 

priceAfterDiscount = _loyaltyDiscountForCalculator.applyDiscount(price: priceAfterDiscount, timeOfHavingAccountInYears: timeOfHavingAccountInYears)

Trong trường hợp tính toán giảm giá đối với trạng thái của tài khoản(account status) dĩ nhiên tôi sẽ phải thêm rất logic phức tạp, vậy nên ở đây tôi đã tách nhỏ ra với 2 phần như sau:
- sử lý logic lấy discount theo trạng thái của tài khoản
- chi tiết của thể phần tính toán

Đầu tiên tôi sẽ tách phần sử lý logic theo account status thành 1 class mới DefaultAccountDiscountForCalculatorFactory, nó đước sử dụng Factory design pattern và sử dụng protocol PAccountDiscountCalculatorFactory, class này chịu trách nghiệm chỉ rõ sẽ sử dụng logic nào cho từng discount và cuối cùng chúng ta sẽ inject nó vào DiscountManager class thông qua hàm khởi tạo.
Như dưới đây:

priceAfterDiscount = _factory.getAccountDiscountForCalculator(accountStatus: accountStatus).applyDiscount(price: price, timeOfHavingAccountInYears: timeOfHavingAccountInYears)


Ở đoạn mã trên đâu tiên nó sẽ tính toàn discount phụ thuộc vào status sau đó sẽ apply, như vậy phần logic đâu tiên đã được tách ra. 

Tiếp theo đến phần thứ 2:
Ở đây vì mỗi phần logic apply discount nó xử lý khác nhau, ở đây tôi đã sử dụng strategy desogm pattern vào đây:
Ở ví dụ của chúng ta sẽ có 2 class sau:
NotRegisteredDiscountForCalculator
SimpleCustomerDiscountForCalculator
ValuableCustomerDiscountForCalculator
MostValuableCustomerDiscounFortCalculator

Các class này đều được implement theo PAccountDiscountCalculator với logic xử lý khác nhau, điều này cho phép DiscountManager class không cần quan tâm phần xử lý discount này.Nhìn qua thì các bạn thấy việc tách như này sẽ làm phình to ra nhanh đúng không, nhưng ở đây chúng ta có thể thấy là toàn bộ code rất dễ đọc, tất cả class đều có 1 nhiệm vụ duy nhất:
1)DiscountManager: quản lý flow
2)DefaultLoyaltyDiscountCalculator: tính toán discount đối với (loyalty)
3)DefaultAccountDiscountCalculatorFactory: xử lý logic tính toàn với mỗi trạng thái phù hợp
4)NotRegisteredDiscountForCalculator, SimpleCustomerDiscountForCalculator, ValuableCustomerDiscountForCalculator, MostValuableCustomerDiscountForCalculator: tính toán đối với account status.
Bạn có thể tự compare code lúc mới bắt đầu và hiện tại để xem nó như nào nhé.

Tóm lại:
Vậy là 2 phần đâu đã kết thúc, ở đây vẫn còn 1 số vấn đề ở hàm getAccountDiscountForCalculator() với switch case hãy tưởng tưởng xem nếu sau này mở rộng thì nó sẽ gặp vấn đề nhưng việc đấy chúng ta sẽ giải quyết ở phần tiếp theo ở 2 phần đã qua mình đã cố gắng sử dụng 1 ví dụ hết sức đơn giản và các kỹ thuật cũng đơn giản để cho các bạn thấy các lỗi thông dụng khi lập trình và cách để xử lý nhưng đoạn mã lởm đấy, mục đích thì mình cũng đã nói rồi, có thể nhưng người đã viết ra đoạn mã đấy đọc được bài này thì sẽ không đồng tình thì thấy nó hơi trừu tượng cũng như phức tập, nhưng đối với 1 dự án dài hơi thì nó là 1 trong nhưng cách để giúp cải thiện thời gian lập trình và sửa đổi sau này.
Cảm ơn các bạn đã đọc!