+4

Clean code #3: Viết các điều kiện (Conditionals) (P2)

Cùng tiếp tục sau Clean code #3: Viết các điều kiện (Conditionals) (P1), chúng ta sẽ đến với Phần 2

7. Magic Numbers:

🔴 Dirty

if(age > 18) {
  // do something
}

if(status == 2) {
  // do something
}

🟢 Clean

const int teenager = 16;
if(age > teenager) {
  // do something
}

if(status == Status.Active) {
  // do something
}

❌ Với Ví dụ Dirty Numbers tự thân không có ngữ cảnh thì không truyền tải được bất kỳ ý nghĩa hay mục đích nào. Nó không truyền tải được nhiều thông tin. Người đọc buộc phải đi tìm kiếm ý nghĩa đằng sau những magic number này.

Cần tránh các magic number. Các con số thường không phải là công cụ phù hợp cho công việc trong câu điều kiện. Để loại bỏ magic number chúng ta có các cách sau:

  1. Thay vào đó, hãy cân nhắc đặt một hằng số có tên dễ hiểu để sử dụng trong câu điều kiện. (Ví dụ: teenager = 16 như ví dụ trên)
  2. Sử dụng enum. Ví dụ: Status.Active như ví dụ Clean trên

✅ Sử dụng các hằng số và enum này, thay vì magic number, giúp tránh lỗi đánh máy, vì ứng dụng sẽ không biên dịch nếu bạn mắc lỗi và bạn sẽ nhận được hỗ trợ Intellisense từ IDE của mình. Thêm vào đó, việc tìm kiếm (searching) tất cả các chỗ sử dụng hoặc tham chiếu của một hằng số hoặc enum nhất định rất dễ dàng. Nhưng lợi ích lớn nhất của việc loại bỏ magic number là nó làm rõ ý định của code.

8. Complex Conditionals:

🔴 Dirty

if(car.Year > 2020
  && (car.Make == "Mazda" || car.Make == "BMW")
  && car.Odometer > 100000
  && car.Vin.StartWith("V2") || car.Vin.StartWith("IA3")) {
  // do Something
}

Câu điều kiện trở nên mất kiểm soát. Chúng có xu hướng phát triển theo thời gian cho đến khi khó sử dụng và không rõ ràng. Có 2 cách tiếp cận đơn giản để quản lý sự phức tạp và làm rõ ý định, để chúng không gây ra sự nhầm lẫn như điều kiện cụ thể này.

a. Sử dụng các biến trung gian

🔴 Dirty

if(employee.Age > 55
  && employee.YearsEmployed > 10
  && employee.IsRetired == true) {
  // do Something
}

🟢 Clean

const int MinRetirementAge = 55;
const int MinPensionEmploymentYears = 10;

bool eligibleForPension = employee.Age > MinRetirementAge
                          && employee.YearsEmployed > MinPensionEmploymentYears
                          && employee.IsRetired;

Trong ví dụ Clean giúp chúng ta biết rằng người viết code đang cố gắng xác định xem nhân viên có đủ điều kiện tham gia chương trình lương hưu của công ty hay không. Bằng cách tạo các biến trung gian đơn giản, anh ấy đã làm rõ ý định của mình.

b. Đóng gói thông qua 1 function/method.

🔴 Dirty

// Check for valid file extensions. Confirm admin or active
if(fileExtension == "mp4" ||
   fileExtension == "mpg" ||
   fileExtension == "avi") &&
  (isAdmin || isActiveFile) {
  // do Something
}

Với ví dụ Dirty này, bạn có thể thấy người viết nhận ra rằng bài viết đang trở nên phức tạp và anh ấy đã chọn sử dụng comment để giúp làm rõ nghĩa cho code hơn.

=> Chúng ta chỉ nên sử dụng comment khi code không thể giải thích đầy đủ về chính nó.

Để sửa lại, chúng ta có thể sử dụng ngữ cảnh của comment để giúp chúng ta đặt tên cho function/method tốt hơn. Tên của function giúp làm rõ rằng mục đích là xác định xem yêu cầu tệp nhận được có hợp lệ hay không.

🟢 Clean

if(ValidFileRequest(fileExtension, isActiveFile, isAdmin)) {
  // do Something
}

bool ValidFileRequest(String fileExtension, bool isActiveFile, bool isAdmin) {
  return (fileExtension == "mp4" ||
     fileExtension == "mpg" ||
     fileExtension == "avi") &&
    (isAdmin || isActiveFile);
}

Với ví dụ Clean trên, dòng trả về bây giờ mô tả rõ ràng ý định của chúng ta và đọc giống như lời nói. Đó là validFileRequest nếu nó là validFileType và người dùng được phép xem tệp, rất rõ ràng.

Và bạn hãy nghĩ về trải nghiệm của người đọc, nếu người đọc bắt gặp một dòng duy nhất nói rằng, nếu validFileRequest, toàn bộ phần thân (phần code) của hàm của có thể bị bỏ qua hoàn toàn bởi lập trình viên bảo trì, nếu lỗi mà họ đang fix hoàn toàn không liên quan đến phần này. Đó là một dòng duy nhất mà họ có thể bỏ qua và tiếp tục.

Nhưng chúng ta có thể làm gọn hơn ví dụ Clean ở trên. Hãy cùng xem một phiên bản khác:

🟢 Clean

if(ValidFileRequest(fileExtension, isActiveFile, isAdmin)) {
  // do Something
}

bool ValidFileRequest(String fileExtension, bool isActiveFile, bool isAdmin) {
  return validFileType && userIsAllowedToViewFile;
}

bool validFileType(String fileExtension) {
    return fileExtension == "mp4" ||
     fileExtension == "mpg" ||
     fileExtension == "avi";
}

bool userIsAllowedToViewFile(bool isActiveFile, bool isAdmin) {
    return isAdmin || isActiveFile;
}

Có thể nói method này thực hiện hai việc. Nó xác thực rằng phần mở rộng tệp được yêu cầu nằm trong danh sách phần mở rộng tệp được chấp nhận của chúng tôi và cũng kiểm tra xem người dùng có quyền xem tệp hay không. Đó là hai phần logic riêng biệt có thể được phân tích lại thành các phương pháp riêng biệt, sao cho mỗi phương pháp có một trách nhiệm rõ ràng nếu muốn.

9. Favor Polymorphism over Enums for Behavior:

🔴 Dirty

void LoginUser(User user) {
  switch(User.status) {
    case Status.active:
        // logic for active users
        break;
    case Status.inActive:
        // logic for user InActive users
        break;
    case Status.locked:
        // logic for user Lock users
        break;            
  }
}

🟢 Clean

void LoginUser(User user) {
  user.Login();
}

abstract class User {
  final String name;
  final Status status;
  final int accountBalance;

  abstract void Login();
}

class ActiveUser extend User {
  @override
  void Login(){
    // Active User logic here
  }
}

class InActiveUser extend User {
  @override
  void Login(){
    // InActive User logic here
  }
}

class LockedUser extend User {
  @override
  void Login(){
    // Locked User logic here
  }
}

Enumconstants là hữu ích cho các conditional logic. Tuy nhiên, khi chuyển đổi logic được sử dụng để cung cấp hành vi khác biệt đáng kể, việc sử dụng các câu lệnh switchenum có thể dẫn đến các câu lệnh switch dư thừa và mã khó đọc. Thay vào đó, đa hình thường có thể cung cấp giải pháp có khả năng mở rộng hơn. Điều này đặc biệt đúng khi bạn nhận thấy câu lệnh switch xuất hiện nhiều lần trong mã của mình.

Trong ví dụ trên, có nhiều logic khác nhau xảy ra khi đăng nhập dựa trên trạng thái (status) của User. Hãy tưởng tượng có nhiều điểm trong class User chứa cùng câu lệnh switch này, kiểm tra status của người dùng và chạy logic phức tạp dựa trên status của User đó. Sử dụng đa hình, chúng ta có thể chuyển giao các chi tiết như vậy cho class User. Với cấu trúc này, mỗi lớp biết cách xử lý chính nó, do đó không còn cần thiết phải chuyển đổi ở nhiều nơi trong mã của chúng ta nữa và logic cho từng User được đóng gói hoàn toàn trong các lớp cụ thể này. Điều này thực sự giúp bạn quản lý sự phức tạp trong các phần riêng lẻ này. Và nó giúp loại bỏ những switch thừa mà bạn có thể thấy trong toàn bộ mã của mình.

10. Be declarative if possible:

🔴 Dirty

List<User> matchingUsers = new List<User>();

forEach(var user in users) {
  if(user.AccountBalance < minimumAccountBalance
       && user.Status == Status.Active) {
    matchingUsers.add(user);
  }
}

return matchingUsers;

Đoạn mã trên chỉ đơn giản là lặp qua danh sách người dùng để tìm những người phù hợp với một số tiêu chí cụ thể. Ví dụ ở trên yêu cầu năm dòng và việc tạo thủ công một cấu trúc lặp để lặp lại tất cả User. Hãy xem xét Clean version

🟢 Clean

return users
       .Where(u => user.AccountBalance < minimumAccountBalance)
       .Where(u => u.Status == Status.Active);

Trong hai dòng mã khai báo rất rõ ràng, chúng ta đã hoàn thành cùng một nhiệm vụ. Nếu chúng ta chỉ đọc to tác dụng của từng đoạn trích, sự khác biệt sẽ rất rõ ràng.

Dirty version: Nó có nghĩa là lặp qua từng user trong users. Và nếu Số dư tài khoản nhỏ hơn mức tối thiểu và trạng thái của người dùng là đang hoạt động, thì Thêm người dùng vào danh sách người dùng phù hợp. Sau đó trả về danh sách người dùng phù hợp

Clean version: Trả về những User có số dư tài khoản nhỏ hơn mức tối thiểu và có trạng thái hoạt động.

Compare Dirty and Clean: Với phiên bản Clean, Phiên bản này đơn giản, diễn đạt và đọc rất giống với lời nói. Vì vậy, nếu ngôn ngữ của bạn có loại công cụ cho phép bạn khai báo nhiều như vậy, hãy sử dụng nó để tiết kiệm thời gian gõ phím và người đọc cũng không phải đọc quá nhiều mã. Và sau đó, bạn cũng ít có khả năng viết ra lỗi hơn, vì bạn đang tuyên bố về những gì bạn muốn thay vì viết mã để có được những gì bạn muốn. Vì vậy, hãy nêu rõ ràng nếu có thể.

11. Prioritize short conditions:

Vì não chúng ta theo tự nhiên thích những cái gì nhanh, ngắn gọn, đơn giản và gặp khó khăn, sợ hãi với những gì dài dòng, phức tạp. Vì vậy khi viết code với các biểu thức điều kiện, bạn hãy cố gắng đem các biểu thức điều kiện ngắn gọn lên trước các biểu thức điều kiện phức tạp hoặc có logic xử lí dài dòng, để người đọc đọc trước. Hãy cùng xem xét ví dụ bên dưới:

🔴 Dirty

if(userStatus == UserStatus.loggedIn) {
    LocalDB.saveToken();
    AppState.isUserLoggedIn = true;
    RepositoryUtils.setAccessTokenToHeader();
    HomeScreen.pushReplace();
} else if (userStatus == UserStatus.nonLogin) {
    AppState.isUserLoggedIn = false;
     NonUserScreen.pushReplace();
} else if(userStatus == UserStatus.tutorial) {
    AppState.isUserLoggedIn = false;
     TutorialScreen.push();
}

Ở ví dụ Dirty trên, người đọc phải nhìn khá nhiều code. Trong trường hợp lập trình viên khác vào tìm nguyên nhân để fix bug cho trường hợp User bị freeze app khi vào màn hình Tutorial hướng dẫn user cài app (UserStatus.tutorial), thì khi đọc qua code chỗ này, tầm mắt họ phải đọc lướt qua các trường hợp bên trên và vô tình keep lại trong não những dòng code ở các trường hợp bên trên. Lúc này những suy nghĩ lộn xộn bắt đầu lé lên buộc lập trình viên đó phải focus vào dù họ không muốn. Đồng thời code chỗ này cũng dính chùm với nhau, và rất dài cũng tạo cảm giác hoang mang sợ hãi cho người đọc khi đọc vào. Hãy cùng xem xét phiên bản Clean:

🟢 Clean

if(userStatus == UserStatus.tutorial) {
    AppState.isUserLoggedIn = false;
    return;
}

if (userStatus == UserStatus.nonLogin) {
    AppState.isUserLoggedIn = false;
     NonUserScreen.pushReplace();
     return;
}

if(userStatus == UserStatus.loggedIn) {
    LocalDB.saveToken();
    AppState.isUserLoggedIn = true;
    RepositoryUtils.setAccessTokenToHeader();
    HomeScreen.pushReplace();
    return;
}

Việc phân biệt các trường hợp và kết thúc sớm những logic của từng trường hợp, đồng thời phân tách nhau bằng các khoảng trắng làm cho cho thẫm mỹ hơn, dễ nhìn hơn. Khi 1 lập trình viên fix bug trường hợp bug UserStatus.tutorial ở trên, anh ta nhìn vào đoạn code này, tâm trí anh ta lập tức focus vào trường hợp UserStatus.tutorial, và 1 cái thở phào nhẹ nhõm khi code logic trường hợp này khá ngắn, đồng thời có câu lệnh return thoát khỏi hàm đang chạy, và anh ta không cần phải cố gắng đọc các dòng code bên dưới.

Viêc đưa các trường hợp có xử lí logic ngắn lên trên cũng giúp cho việc hiểu các trường hợp logic dễ dàng hơn. Đứng dưới góc độ người đọc, anh ta sẽ thở phào nhẹ nhõm khi đã đọc xong 1 trường hợp logic ngắn và có thể tạm quên các logic đó đi để tiếp tục đọc tiếp các trường hợp dài hơn bên dưới, việc tăng số lượng dòng code từ thấp đến cao, từ ít lên nhiều tạo tâm lí nhẹ nhàng hơn khi đọc, giúp anh ta hoàn thành sớm việc hiểu logic của method/function.

Còn nếu bạn chuyển các điều kiện có các xử lí logic dài hơn lên trên như ví dụ Dirty thì sao, tâm lí ban đầu của người đọc là anh ta sẽ cảm thấy ngán. Sau đó cố gắng đọc cho hết những xử lí logic của trường hợp dài đó, nếu não cảm thấy mệt, có thể anh ta sẽ làm 1 tách cafe thư giãn và quay lại sau đó để tiếp tục đọc tiếp. Và điều gì xảy ra khi anh ta đọc xong các xử lí logic của điều kiện dài đó, anh ta sẽ tiếp tục đọc các điều kiện bên dưới và luôn có 1 câu hỏi thì thầm trong đầu "điều kiện bên trên đã xử lí mấy logic này chưa ta?". Rồi thỉnh thoảng anh ta lại quay lại đọc các logic của điều kiện trên (Có thể bạn đã gặp trường hợp tương tự như thế này rồi).

Vì vậy, việc đưa các điều kiện có logic ngắn gọn lên trên và kết thúc sớm function/method thực sự có thể góp phần giảm thiểu việc đọc code, và giúp đỡ việc đọc hiểu code rất nhiều.

Lưu ý: Nếu xảy ra trường hợp điều kiện ngắn vi phạm phần 4. Positive Conditionals, thì bạn hãy ưu tiên xử lí Positive Conditionals. Hãy xem ví dụ sau:

🔴 Dirty

void handleUserValid(List<User> users) {
    if(users.isNotEmpty) {
        sendEmailToUsers(users);
        sendSmsToUsers(users);
        addVoucherForUsers(users);
        sendVoucherNotificationToUsers(users);
    } else {
        sendCampaignNotificationToAllUsers();
    }
}

Nếu sử dụng phương pháp ở phần 11. Prioritize short conditions này chúng ta sẽ chuyển code thành

void handleUserValid(List<User> users) {
    if (!users.isNotEmpty) {
        sendCampaignNotificationToAllUsers();
        return;
    }
    
     sendEmailToUsers(users);
     sendSmsToUsers(users);
     addVoucherForUsers(users);
     sendVoucherNotificationToUsers(users);
}

Nhưng code trên thậm chí còn gây khó đọc và khó hiểu hơn cho người đọc. Vì code đã vi phạm tính chất 4. Positive Conditionals. Ở đây hoặc là bạn giữ nguyên code như cũ, hoặc bạn tìm phương pháp thay thế để chỉnh sửa cho phù hợp như ví dụ bên dưới:

🟢 Clean

void handleUserValid(List<User> users) {
    if(users.isEmpty) {
        sendCampaignNotificationToAllUsers();
        return;
    }
    
     sendEmailToUsers(users);
     sendSmsToUsers(users);
     addVoucherForUsers(users);
     sendVoucherNotificationToUsers(users);
}

Ở ví dụ này chúng ta thay thế câu lệnh !users.isNotEmpty thành users.isEmpty đúng với bản chất của nó, và code cũng đã clear hơn.

12. Table Driven Methods:

🔴 Dirty

if(goldPrice < 20) {
  return 3.4560;
} else if (goldPrice < 30) {
  return 4.1950;
} else if(goldPrice < 40) {
  return 4.7638;
} else if(goldPrice < 50) {
  return 5.1625;
}

Hãy tưởng tượng chúng ta đang cố gắng xác định mức lạm phát dựa trên giá vàng. Một cách để thực hiện điều này là sử dụng câu lệnh if dài và đặt tất cả các mức giá trực tiếp trong code. Đây được gọi là Hardcoding. Hardcoding không tệ khi đó là một lượng nhỏ dữ liệu không thay đổi. Nhưng trong trường hợp này, bạn có thể tưởng tượng rằng giá vàng sẽ thay đổi thường xuyên và do đó cần phải thay đổi mã của chúng ta. Bạn cũng có thể tưởng tượng được điều này có thể nhanh chóng trở thành một tập hợp logic rất phức tạp và thay đổi liên tục cho một ứng dụng xem giá cả thực tế như thế nào. Trong những trường hợp này, logic thường không nằm trong code. Thay vào đó, nó nằm trong cơ sở dữ liệu (database). Sau đây là bảng cơ sở dữ liệu có thể thay thế câu lệnh if phức tạp được mã hóa cứng trên phiên bản Dirty.

🟢 Clean

InflationRate table

InflationRateId MaximumPriceGold Rate
1 20 3.4660
2 30 4.2050
3 40 4.7638
4 50 5.1625

Sau khi bảng được điền dữ liệu chính xác, bạn chỉ cần truy vấn bảng trong mã của mình.

return Repository.GetInflationRate(goldPrice);

Vì vậy, không chỉ mã của bạn sạch hơn nhiều mà bất cứ khi nào dữ liệu thay đổi, bạn không phải phát hành phiên bản mới của ứng dụng. Bạn chỉ cần cập nhật một hàng trong cơ sở dữ liệu.

✅ Mã sạch sẽ tóm tắt những thứ thay đổi và tránh Hardcoding các giá trị động. Có nhiều nơi mà phương pháp dựa trên bảng có ý nghĩa, bao gồm cấu trúc giá và các quy tắc kinh doanh động phức tạp. Kỹ thuật này có thể làm cho ứng dụng của bạn cực kỳ linh hoạt mà không cần nhiều mã. Và đôi khi, sẽ hữu ích khi nghĩ về logic của bạn không gì khác ngoài một tập hợp dữ liệu tra cứu phức tạp.

Tóm lại: Phương pháp tiếp cận theo bảng rất tuyệt vời cho logic động và giúp tránh Hardcoding dữ liệu vào ứng dụng của chúng ta. Nó cũng giúp chúng ta tránh viết và duy trì các cấu trúc dữ liệu phức tạp. Và cuối cùng, nó có thể dễ dàng thay đổi mà không cần thay lớp phủ hoặc phương pháp triển khai ứng dụng.

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í