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 Hunt và Dave 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 name
và familyName
, thay vì tạo ra 1 stored property fullName
để ghép name
và familyName
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:
- Khi phải thay đổi function/class, liệu bạn có muốn tất cả những chỗ chúng được dùng cùng cập nhật?
- Hai đoạn code này giống nhau có phải là trùng hợp không? Có khả năng chúng không như vậy trong tương lai chứ?
- Bạn đã thực sự hiểu về yêu cầu dự án/tính năng để áp dụng DRY cho chúng chưa? Liệu có nên tạm thời để code lặp và đợi xem thế nào?
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”.