+3

Clean code #5: Viết các lớp (Class)

1. Khi nào tạo một Class:

Các class giống như tiêu đề trong một cuốn sách:

  • Tiêu đề trong sách cung cấp tóm tắt cấp cao về nội dung bên trong. Khi người đọc tìm kiếm một khái niệm cụ thể. Sau đó, tiêu đề này cung cấp một lộ trình.
  • Tiêu đề giúp người đọc dễ đọc lướt hơn và hiểu các đoạn văn bên trong dễ dàng hơn bằng cách cung cấp bản tóm tắt tổng quan về chủ đề đang được thảo luận.
  • Các Class được đặt tên hay và có mục tiêu rõ ràng sẽ cung cấp một hàng đợi suy nghĩ chặt chẽ cho mục đích cấp cao bên trong và do đó hỗ trợ người đọc.
  • Giống như một tiêu đề có các đoạn văn liên quan, một Class phải chứa nhiều method có liên quan chặt chẽ đến một trách nhiệm rõ ràng duy nhất.

Có một số lý do hợp lý để tạo một Class hoặc trích xuất (extract) một Class từ một Class hiện có.

a. Để mô hình hóa một đối tượng (To model an object)

Các đối tượng (object) này có thể mô hình hóa các khái niệm Cụ thể (Concrete) hoặc Trừu tượng (Abstract). Các Class đặc biệt hữu ích với các khái niệm trừu tượng vì chúng đặt tên và hành vi cụ thể cho các khái niệm này và do đó giúp chúng dễ tiếp cận hơn.

Nếu các method trong Class của bạn ít liên quan đến nhau thì đó là dấu hiệu cho thấy Class có tính gắn kết thấp (Low Cohesion).

b. Tính gắn kết thấp (Low Cohesion)

Các Class có tính gắn kết thấp nên được chia thành các Class riêng biệt với trách nhiệm cụ thể hơn

c. Để thúc đẩy tái sử dụng (Promote Reuse)

Ngay cả khi một đoạn code có thể là một phần của một Class lớn hơn, hãy đặt nó vào Class riêng của nó nếu nó hữu ích cho một chương trình (program) khác và do đó dễ làm việc hơn. Việc tạo một Class cũng hữu ích để giảm độ phức tạp.

Việc tạo một Class mới có nghĩa là người đọc không phải cân nhắc đến gánh nặng tinh thần của một vấn đề đã giải quyết. Class này tạo ra ẩn đi sự phức tạp và các hoạt động liên quan đến các method của class cũ, khuất tâm trí, và cho phép ai đó chỉ cần tin tưởng rằng các method đó vẫn sẽ hoạt động.

d. Để làm rõ các thông số (Clarify parameters)

  • Các Class có thể đơn giản hóa đáng kể việc truyền thông tin khi liên quan đến các cấu trúc dữ liệu phức tạp.
  • Các lớp học có thể chuyển đổi một nhóm các biến liên quan thành thứ gì đó cụ thể hơn, dễ lý giải hơn và hữu hình hơn.
  • Nếu bạn truyền cùng một variable cho nhiều function thì rất có thể các variable và function đó phải là một Class.

3. Cohesion (Sự gắn kết):

Chúng ta thường gặp phải vấn đề trong Class khi giao nhiệm vụ cho sai Class hoặc mong đợi một Class nào đó làm quá nhiều việc. Sự gắn kết (Cohesion) là thước đo mức độ liên quan chặt chẽ giữa các trách nhiệm của một Class.

Các Class có tính gắn kết cao có các chức năng liên quan chặt chẽ. Điều này giúp chúng dễ đọc hơn vì tên Class có cùng mục đích như tiêu đề trong sách. Sẽ dễ hiểu hơn về nội dung của một Class khi tiêu đề thực sự tóm tắt rõ ràng trọng tâm của Class ở cấp độ cao.

Các Class có tính gắn kết cao có nhiều khả năng được tái sử dụng hơn vì logic bên trong có nhiều khả năng được phát hiện ngay từ đầu.

Khi các Class chứa nhiều nhóm method không liên quan thì khả năng các lập trình viên khác tìm thấy code là rất thấp. Do đó, họ có nhiều khả năng tái tạo lại Nguyên tắc bánh xe (reinvent to wheel) và phá vỡ bằng cách lặp lại (DRY Principle) cùng một logic theo cách riêng của họ trong một Class mới.

Các Class có tính gắn kết thấp thường có tên kém vì rất khó để đặt tên cho một lớp có quá nhiều trách nhiệm quan trọng. Các Class này thường được đặt tên quá chung chung, thu hút các lập trình viên lười biếng, không muốn suy nghĩ về nơi logic thực sự của họ thuộc về. Để tránh thu hút các lập trình viên lười biếng, điều quan trọng là phải giữ tên Class mang tính mô tả. Điều này giúp duy trì Cohesion ở mức cao. Nguyên tắc này có liên quan chặt chẽ đến Nguyên tắc trách nhiệm duy nhất (Single Responsibility Principle) của Bob Martin, trong đó nêu rằng mỗi mô-đun phải thực hiện một việc và thực hiện tốt một việc và chỉ có một lý do để thay đổi.

🔥 Để tránh tạo ra các Class có Độ gắn kết thấp, hãy chú ý một số điều sau đây:

Hãy chú ý đến những method không tương tác với phần còn lại của Class:

Trong các Class có tính gắn kết cao, các method và thuộc tính (properties) hoạt động cùng nhau để hoàn thành nhiệm vụ (task). Sự tương tác thường xuyên này là dấu hiệu cho thấy lớp có tính gắn kết.

Các Class có độ gắn kết thấp giống như một tập hợp các chức năng ngẫu nhiên, khiến chúng khó hiểu và khó bảo trì, đồng thời ít có khả năng được người khác sử dụng lại.

Tương tự như vậy, hãy chú ý đến các field chỉ được sử dụng ở một method. Các Field nên được nhiều method trong Class sử dụng và nếu không thì hãy xem xét filed hoặc tập hợp các filed có phải là dấu hiệu cho thấy chức năng liên quan nên được trích xuất sang một Class riêng biệt hay không.

Source control cũng có thể giúp chỉ ra các Class đang làm quá nhiều việc. Nếu bạn thấy một Class nhận được nhiều commit hơn mức trung bình thì có khả năng là ứng cử viên cho việc tái cấu trúc để tách các Class có Độ gắn kết cao hơn.

🔥 Hãy xem xét một ví dụ về Lớp có Độ gắn kết thấp:

Vehicle này cho phép bạn chỉnh sửa các tùy chọn vehicle, cập nhật giá (pricing), lên lịch bảo dưỡng (schedule maintenance), gửi lời nhắc bảo dưỡng (send maintenance reminders), chọn hình thức tài chính (select financing) và tính toán các khoản thanh toán hàng tháng (calculate monthly payments). Bây giờ, tất cả những mặt hàng này đều liên quan đến vehicle. Một Class xử lý những trách nhiệm khá tuyệt vọng này có khả năng có Sự gắn kết thấp vì các method xử lý bảo trì sẽ sử dụng một tập hợp các field rất khác so với các tùy chọn xe và tài chính và thanh toán sẽ đòi hỏi toán học phức tạp và logic kinh doanh hoàn toàn không liên quan đến nhiệm vụ đơn giản là chỉnh sửa tùy chọn xe.

Hãy cùng xem xét một khả năng tái cấu trúc:

Low High
Low_vehicle High_vehicle
Low_vehicle_1 High_vehicle_2

Việc tái cấu trúc này của Vehicle Class chia nó thành 3 Class riêng biệt. Các Class này gắn kết hơn vì chúng thực hiện một tập hợp các chức năng có liên quan chặt chẽ. Vehicle Class hiện xử lý các trách nhiệm rất cụ thể đối với dữ liệu xe cốt lõi như chỉnh sửa tùy chọn xe và giá cả.

  • Vehicle Maintenance Class xử lý việc lên lịch bảo dưỡng cho xe và gửi lời nhắc liên quan đến bảo dưỡng.
  • Vehicle Finance Class xử lý việc trình bày các lựa chọn tài chính và tính toán các khoản thanh toán hàng tháng dựa trên các lựa chọn tài chính đã chọn.
  • Hãy tưởng tượng tôi cần thêm một tính năng mới để tài trợ cho một chiếc xe. Bằng cách đặt logic tài trợ vào một Class riêng biệt, giờ đây tôi có một Class rõ ràng và dễ tiếp cận hơn để xem xét và sửa đổi. Tôi cũng đã giảm thiểu rủi ro vì việc tôi thay đổi Vehicle Finance Class sẽ không ảnh hưởng đến bất kỳ chức năng nào trong Vehicle Maintenance or Vehicle Classes.
  • Ngoài ra, hãy nhớ rằng tính gắn kết ngày càng quan trọng khi Class phát triển và logic trong ứng dụng trở nên phức tạp hơn. Trong một hệ thống đơn giản, việc kết hợp những mối quan tâm này lại với nhau trong một Class duy nhất sẽ không nhất thiết gây cảm giác nặng nề. Nhưng hãy tưởng tượng nếu mỗi Class có tính gắn kết cao này ở bên phải chứa một nghìn dòng mã. Khi đó, ý tưởng được đặt cùng nhau sẽ nghe có vẻ cực kỳ đau đớn.
  • Các Class có tên quá chung chung và không mô tả được sẽ dẫn đến các Lớp Nam châm (Magnet Classes) và như chúng ta đã biết, nam châm có khả năng hút các vật thể (item). Tương tự như vậy, các Class có tên tệ như thế này sẽ thu hút các lập trình viên lười biếng.
  • Các lớp có tên như CommonUtility thường phát triển nhanh chóng thành hàng nghìn dòng mã không liên quan. Vì vậy khi bạn khởi tạo một Class, bạn phải có ý tưởng rõ ràng về Class đó là gì và Class đó làm gì chỉ từ tên của Class. Đây là lý do tại sao Class thường có tên danh từ. Tên càng ngắn gọn thì Class càng nhỏ.
  • Vì vậy, hãy bắt đầu bằng cách đặt tên tốt và phấn đấu cho Class Cohesion. Bạn có thể đo lường Class Cohesion một cách sơ bộ bằng cách xem xét có bao nhiêu method sử dụng biến thể hiện (instance variable) của Classes. Các biến thể hiện (instance variable) chỉ được sử dụng bởi một hoặc hai method là dấu hiệu cho thấy những method đó có thể được cấu trúc lại thành một Class khác.

4. When is A Class too Small?

Các nhà phát triển gần như luôn mắc lỗi khi tạo ra các Lớp có quá nhiều chức năng tuyệt vọng. Tuy nhiên, sau đây là một số dấu hiệu cho thấy một Class quá nhỏ:

  • Nếu hai Class phụ thuộc nhiều vào nhau và gọi phần lớn method của nhau thì chúng có thể là ứng cử viên để hợp nhất thành một Class duy nhất.
  • Nếu bạn thấy một Class phụ thuộc nhiều vào một Class khác thì có thể là dấu hiệu cho thấy hai Class đó thuộc về nhau.
  • Các Class quá nhỏ có thể khiến việc hiểu cách hệ thống gắn kết với nhau trở nên khó khăn. Có quá nhiều bộ phận chuyển động nhỏ. Điều này có thể dẫn đến các vấn đề khác như các Class liên kết chặt chẽ với nhau. Những vấn đề này hầu như không đáng kể trong thực tế. Các lập trình viên hiếm khi mắc phải lỗi này khi tạo ra các Class quá nhỏ và rất thường mắc phải lỗi tạo ra các Class nguyên khối với quá nhiều trách nhiệm.

5. Primitive Obsession

Các lập trình viên làm hướng đối tượng có thể dễ dàng chuyển các phần dữ liệu liên quan rời rạc mà thực ra phải được đóng gói trong một Class. Ví dụ Dirty này phá vỡ Rule of Seven khi có nhiều hơn 7 parameters. Tất cả các tham số (parameter) này rõ ràng là propertie của User nên cần phải truyền một đối tượng (object) người dùng vào method này.

🔴 Dirty

void SaveUser(User user, bool sendEmail, int emailFormat, bool printReport, bool sendBill) {
  // do something
}

🟢 Clean

void SaveUser(User user) {
 // do something
}

Bây giờ, có một sự đánh đổi cần thực hiện ở đây. Nếu method chỉ sử dụng một hoặc hai tham số (parameter) từ User thì việc truyền các thuộc tính (propertie) cần thiết cụ thể có thể giúp làm rõ các phụ thuộc dữ liệu chính xác và mục đích của method này là thể hiện những gì cần thiết mà không cần xem nội dung method. Phiên bản Clean gom các mục dữ liệu liên quan vào một Class. Có nhiều lợi ích cho cách tiếp cận này:

  • Nó giúp người đọc hình dung được code và mục đích bằng cách đặt tên dễ hiểu cho nhóm parameter.
  • Nó lấy một thứ gì đó ngầm định, trong trường hợp này là một danh sách dài các parameter, và làm cho nó rõ ràng hơn bằng cách đặt cho nhóm một cái tên có thể lý giải được.
  • Properties có thể đóng gói logic kinh doanh và các quy tắc cho các giá trị của chúng để tránh đưa code tùy ý vào bất cứ nơi nào các nguyên hàm này được sử dụng.
  • Nếu sau này có thêm một property khác vào đối tượng User cần được method này tận dụng thì method signature đó sẽ không bị ảnh hưởng. (Bảo trì hỗ trợ)
  • Bạn có thể dễ dàng tìm thấy tất cả các tham chiếu (reference) đến đối tượng User trong code. Khi bạn truyền xung quanh một tập hợp các nguyên thủy rời rạc, việc tìm tất cả các tham chiếu đến các nguyên thủy đó trong dữ liệu User liên quan có thể trở thành một nhiệm vụ khó khăn.

=> Vì vậy, hãy nhớ rằng nếu bạn đang truyền một tập hợp các mục dữ liệu có liên quan thì đó là dấu hiệu cho thấy đã đến lúc tạo một Class.

6. Principle of Proximity

Người đọc thường đọc từ trên xuống dưới. Do đó, việc giữ các hành động liên quan lại với nhau thực sự hữu ích. Việc đọc một method và thấy nó gọi một method khác là điều rất bình thường. Vì vậy, nếu method tiếp theo được gọi là method bên dưới thì công việc của người đọc rất dễ dàng. Ngược lại sẽ khó đọc và làm việc với Class hơn khi bạn phải cuộn lên và xuống hoặc sử dụng phím tắt để nhảy qua lại trong khi đọc các lệnh gọi function/method.

Ví dụ: Đoạn mã Class này được trình bày theo thứ tự từ trên xuống dưới

private void ValidateRegistration()
{
    ValidateData();

    if (!SpeakerMeetsOurRequirements())
    {
        throw new SpeakerDoesntMeetRequirementsException("This speaker doesn't meet our standards.");
    }

    ApproveSessions();
}

private void ValidateData()
{
    if (string.IsNullOrEmpty(FirstName)) throw new ArgumentNullException("First Name is required.");
    if (string.IsNullOrEmpty(LastName)) throw new ArgumentNullException("Last Name is required.");
    if (string.IsNullOrEmpty(Email)) throw new ArgumentNullException("Email is required.");
    if (Sessions.Count == 0) throw new ArgumentException("Can't register speaker with no sessions to present.");
}

private bool SpeakerMeetsOurRequirements()
{
    return IsExceptionalOnPaper() || !ObviousRedFlags();
}

Lưu ý rằng khi bạn đọc lệnh gọi để xác thực dữ liệu, bạn có thể liếc xuống và thấy rằng đó là method tiếp theo trong Class. Cuộc gọi tiếp theo trong ValidateRegistrationSpeakerMeetsOurRequirements. Bạn sẽ nhận thấy lại rằng nó được đặt ngay bên dưới ValidateData. Vì vậy, nó vẫn gần một cách thuận tiện.

Lưu ý: Đây là một quy tắc không chặt chẽ vì các method thường được gọi nhiều lần từ nhiều nơi khác nhau và bạn chỉ có thể đặt một method/function ở một vị trí. Tuy nhiên, nếu có thể, sẽ hữu ích cho người đọc nếu sắp xếp các method tương tác hoặc có liên quan chặt chẽ với nhau ở gần nhau trong Class.

7. Outline Rule

Một dàn ý được viết tốt có nhiều Class. Nó định nghĩa các khái niệm cấp cao và đi sâu vào các điểm cụ thể được nêu trong từng khái niệm. Hoặc nếu bạn nghĩ về một cuốn sách được viết hay. Nếu bạn đang lật giở một chương không quen thuộc để tìm kiếm một ý tưởng cụ thể ở cấp độ cao thì việc có tiêu đề phần sẽ rất hữu ích. Sau đó, bạn có thể dễ dàng quét một khái niệm high-level cụ thể. Các Class sử dụng Outline Rule cũng mang lại cho người đọc lợi ích tương tự.

Outline Rule làm tăng signal to noise của code bằng cách cung cấp cho người đọc nhiều lớp trừu tượng (abstraction). Các Class này được cung cấp bằng cách tạo ra các method được đặt tên tốt mô tả các bước cấp cao liên quan đến một quy trình. Mỗi high level methods này chuyển giao cho các lower level method để thực hiện công việc cuối cùng.

Chapter Title Class
Chapter Class

Với phong cách này, bạn có thể có một số method thực chất chỉ là danh sách các lệnh gọi method liên quan và điều đó không sao cả. Nó chuyển đổi thứ gì đó ngầm hiểu và có khả năng gây nhầm lẫn thành một cấu trúc có tên dễ hiểu, dễ hiểu hơn và truyền đạt ý định tốt hơn. Bằng cách đó, bạn xây dựng một Outline vào mã của mình, cho phép người đọc lý giải về code của bạn ở mức trừu tượng và hữu ích nhất cho mục đích của họ. Ví dụ như thế này:

Typical Class Strive for this
Typical_Class Strive

Bạn có thể thấy tổng quan cấp cao về 3 method liên quan. Nếu bạn muốn tổng quan về những gì method 1 đang thực hiện, bạn có thể xem lại tên của 3 method con của nó. Nếu bạn quan tâm đến chức năng được nêu trong Method 1a, bạn thậm chí có thể thấy rằng ở cấp độ cao (high level), nó bao gồm các bước được nêu trong 1ai1aii.

Khi một Class được thiết kế để tôn trọng Outline Rule, bạn có thể dễ dàng xem xét chức năng của Class đó và lớp trừu tượng (layer of abstraction) hữu ích nhất cho mục đích của mình, đồng thời đào sâu vào mức độ chi tiết phù hợp khi cần.

Cảm ơn bạn đã đọc hết bài viết. Nếu thấy hay hãy để lại mình 1 vote và hẹn gặp lại trong bài viết tiếp theo! 🚀


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí