Linh Tạ

Bạn đang hiểu sai về nguyên tắc DRY

Một trong những nguyên tắc đảm bảo chất lượng code đầu tiên chúng ta được học là tránh lặp code. Nguyên tắc này thường được biết đến qua thuật ngữ DRY - Don’t Repeat Yourself. Chúng ta được hứa hẹn rằng việc loại bỏ code lặp sẽ giúp codebase sạch, dễ thay đổi, và ít bug hơn, tuy nhiên điều này không phải lúc nào cũng đúng và thậm chí còn phản tác dụng. Trong bài viết ngày hôm nay, hãy cùng mình tìm hiểu bản chất và cách phù hợp nhất để áp dụng DRY.

Vấn đề của việc abstract code

Chắc hẳn bạn đã từng cố loại bỏ những đoạn code giống nhau bằng việc tạo ra một abstraction để nhóm chúng lại, ví dụ như dùng function, hoặc một class cha để subclass. Bạn tự hào về thành quả của mình vì giờ đây code đã gọn và dễ thay đổi hơn rất nhiều - chỉ cần update ở một chỗ là tất cả những nơi sử dụng abstraction đó đều sẽ được cập nhật theo 🤔.

Theo thời gian, yêu cầu dự án thay đổi, đột nhiên bạn thấy rằng vẫn tính năng/giao diện đấy nhưng được dùng ở màn A hơi khác với màn B và C một chút. Bạn quyết định truyền thêm cờ vào function và sử dụng if - else để phân biệt giữa A và B, C. Không lâu sau, bạn lại nhận ra mình chưa hiểu kỹ tính năng của màn C và phải cập nhật function cho phù hợp hơn bằng cách gắn thêm một biến cờ khác…

Thời gian cứ thế trôi, bạn sang project mới, Quân được giao trọng trách maintain codebase hiện giờ. Quân bị tester bắn bug tính năng đó ở màn D. Nhưng giờ đây, thay vì sửa lại toàn bộ function, Quân chỉ dám gắn thêm cờ dành riêng cho D vì không biết thay đổi của mình có tạo bug ở các màn khác không. Hơn nữa, do khách hàng đang ép deadline nên cũng không còn đủ thời gian để tìm hiểu 😥.

Từ một mục đích rất tốt ban đầu là code sạch và tránh trùng lặp, function/class bạn tạo ra đã bó buộc tất cả những nơi nó được dùng với nhau - A không thể thay đổi mà không ảnh hưởng đến B, C, D và ngược lại. Không chỉ vậy, theo thời gian, càng được sử dụng ở nhiều nơi, càng khó biết được việc update function/class đó có tác động đến những phần liên quan không. Điều này vô hình chung khiến bạn cũng như anh em trong team sợ thay đổi hoặc ngại tách function theo hướng tốt hơn. Đây chính là tác hại lớn nhất của việc tách code quá sớm và tách code dựa theo sự giống nhau về mặt hình thức.

Trên thực tế, việc abstract code dựa theo hình thức và khi chưa hiểu rõ requirement không đem lại ích lợi mà chỉ làm tăng độ phức tạp của codebase, khiến những phần đáng lẽ không liên quan tới nhau bị phụ thuộc và khó tiến hoá độc lập khi yêu cầu dự án thay đổi.

Mình không phủ nhận tầm quan trọng của việc tránh lặp code, điều mình muốn nhấn mạnh là bất kỳ kĩ thuật hay nguyên tắc nào đều có giá của nó. Đối với abstract code, cái giá phải trả chính là sự ràng buộc. Để hạn chế tối đa điểm yếu này, chúng ta phải hiểu rõ khi nào nên và không nên áp dụng nó. Chỉ vì hai đoạn code trông giống nhau không đồng nghĩa với việc chúng nên được abstract vào cùng một function hoặc class.

Cái giá của lặp code rẻ hơn việc abstract sai rất nhiều - Sandi Metz


Bản chất của DRY

Tránh lặp code theo hình thức không phải là DRY. Theo Andy HuntDave Thomas, 2 cha đẻ của tên gọi DRY, bản chất của nguyên tắc này là abstract code dựa vào sự giống nhau về mặt kiến thức và mục đích

DRY is about the duplication of knowledge, of intent. It’s about expressing the same thing in two different places, possibly in two totally different ways

Dịch: DRY nói về sự lặp về mặt kiến thức và mục đích. Nó nói về việc (tránh) diễn tả một thứ ở 2 nơi khác nhau, bằng 2 cách hoàn toàn khác nhau

Nói cách khác, DRY khuyên bạn nên nhóm những đoạn code cùng diễn tả một business requirement - yêu cầu, kiến thức, nghiệp vụ, hoặc tính năng chung của dự án.

✅ Nếu bạn làm 1 app thương mại điện tử, thì rất có thể bạn nên tạo một function tính tổng tiền dựa theo sản phẩm và phần trăm giảm giá của chúng thay vì lặp code ở nhiều function nhỏ lẻ tại nhiều màn khác nhau.

✅ Nếu bạn model User với 2 trường namefamilyName, thay vì tạo ra 1 stored property fullName để ghép namefamilyName thì bạn nên sử dụng computed property để tránh lặp data.


struct User {
    var name: String
    var familyName: String

    //❌ Lặp data vì fullName được ghép từ name và familyName
    var fullName: String

    //✅
    var fullName: String  { return "\(name) \(familyName)" }
}


✅ Nếu bạn thấy các đoạn code chia sẻ một logic nhất định nhưng khác nhau ở một vài điểm thì hãy DRY logic đó ra thành một pure function có thể tái sử dụng thay vì DRY tất cả đoạn code đấy.

❌ Đừng subclass view controller chỉ vì UI hai màn trông giống nhau. Bạn đã cân nhắc việc tạo custom view và reuse nó chưa? Nếu chúng giống nhau về tính năng nhưng khác giao diện, liệu bạn có thể tách tính năng đó thành một object riêng không?

❌ Đừng DRY chỉ vì code trùng 100%. Hãy chắc chắn rằng chúng có cùng mục đích. Hai nghiệp vụ thoạt nhìn giống nhau nhưng có khả năng không cùng mục đích.


//❌ Không nên dùng if-else để nhóm những đoạn code giống nhau 99%
//nhưng lại khác nhau ở một điểm nào đó
//vì rất có thể chúng không phục vụ cùng mục đích
func getShippingFee(order: Order, isFast: Bool) -> Double {
    //...
    if isFast {
        //...logic giao hàng nhanh...
    } else {
        //...logic giao hàng thường...
    }
    
    return fee
}

//✅ Nên tách ra thành những function riêng biệt - ĐƠN MỤC ĐÍCH.
//Các function đơn lẻ và chuyên tâm cho 1 mục đích
//dễ thay đổi và dễ test hơn một function phức tạp rất nhiều
func getFastShippingFee(order: Order) -> Double
func getNormalShippingFee(order: Order) -> Double


❌ Bạn đã từng dùng chung view hoặc view controller và gắn cờ để bật tắt UI hoặc tính năng chứ? Nếu if-else xuất hiện khắp nơi thì khả năng cao bạn nên tách chúng ra thành các view/view controller khác nhau đấy.

❌ Bạn đã bao giờ dùng chung model cho nhiều API khác nhau và rồi phải thêm các biến optional vì không phải màn nào cũng có chung thuộc tính? Có thể đây là dấu hiệu của việc DRY data không đúng.


//❌ Không nên dùng chung struct/class
//để biểu thị những object có chung một vài property
//nhưng thực ra lại khác nhau về bản chất.
//Việc thêm biến optional cho những giá trị chỉ tồn tại
//cho một loại nhất định khiến code bẩn, khó hiểu và khó dùng hơn
//khi luôn phải check/unwrap optional
struct User {
    var name: String
    var address: String
    var age: Int
    
    //❌ Dấu hiệu áp dụng DRY sai cách
    var isVip: Bool
    var vipDuration: Date?
    var vipAbilities: [Ability] = []
    var vipPreference: Preference = .unknown
}

//✅
//Nếu cần thiết có thế sinh ra một protocol để nhóm User và VIPUser
//tuy nhiên protocol cũng là một abstraction
//vậy nên bạn hãy cân nhắc nghiệp vụ thật kĩ trước khi dùng
struct User {
    var name: String
    var address: String
    var age: Int
}

struct VIPUser {
    var name: String
    var address: String
    var age: Int
    
    //không còn nỗi lo optional 
    //bởi luôn chắc chắn rằng VIP sẽ có vipDuration
    var vipDuration: Date

    //không còn phải đau đầu suy nghĩ về giá trị mặc định
    //bởi luôn lấy theo API
    var vipAbilities: [Ability]
    var vipPreference: Preference
}


Lưu ý

Trước khi DRY code hãy luôn tự hỏi:

Giống như mọi nguyên tắc khác, tất cả những thứ mình nêu trên chỉ mang tính tương đối. Bạn không nên áp dụng một cách mù quáng mà phải cân nhắc kĩ càng dựa theo hoàn cảnh và yêu cầu dự án. Hãy luôn nhớ rằng lặp code không phải lúc nào cũng xấu. Thà lặp code vài lần và đợi chắc chắn rằng đã đến lúc abstract chứ đừng abstract quá sớm và mạo hiểm ràng buộc code với nhau.

Trong trường hợp bạn thực sự muốn DRY code, ưu tiên sử dụng function và protocol thay vì subclass - 99% số lần, subclass luôn là câu trả lời sai.

Tổng kết

DRY giúp việc thay đổi business requirement trở nên dễ dàng và tránh nỗi lo bỏ lọt code. Nếu 2 đoạn code được tạo ra có chung mục đích và bắt buộc phải tiến hoá cùng nhau thì áp dụng DRY là điều nên làm. Tuy nhiên, sử dụng DRY không đúng chỗ hoặc abstract quá sớm và cố reuse code là một trong những lý do lớn nhất khiến code trở nên phức tạp và khó thay đổi.

Mong rằng bài viết này sẽ có ích với bạn. Đừng sợ nếu những gì mình nêu trên còn quá mơ hồ, DRY là một nguyên tắc mở và phụ thuộc rất nhiều vào hoàn cảnh. Chỉ cần bạn hiểu bản chất của DRY và cân nhắc thật kĩ trước khi sử dụng abstraction thì theo thời gian bạn sẽ học được cách chọn thời điểm abstract code. Và đừng quên rằng “cái giá của lặp code rẻ hơn việc abstract sai rất nhiều”.