"Clean Code" không chỉ là một khái niệm mà còn là một triết lý mang tính cốt lõi, dẫn dắt các lập trình viên đến với việc viết mã nguồn hiệu quả, dễ hiểu, và dễ bảo trì. Nhưng điều gì thực sự tạo nên một đoạn code được coi là "sạch"? Tại sao việc viết code sạch lại quan trọng đến vậy, đặc biệt trong môi trường phát triển phần mềm ngày càng phức tạp hiện nay? Bài viết này sẽ giúp bạn khám phá ý nghĩa của Clean Code cùng những mẹo hữu ích để viết mã nguồn "sạch".

1. Viết "Clean Code" có nghĩa là gì và tại sao lập trình viên nên quan tâm?

Clean code (Code sạch) là thuật ngữ dùng để mô tả code dễ đọc, dễ hiểu và dễ duy trì. Clean code được viết theo cách đơn giản, ngắn gọn và biểu đạt rõ ràng. Nó tuân theo một tập hợp các quy ước, tiêu chuẩn và thực hành giúp code trở nên dễ đọc và dễ theo dõi.

Khi code dễ đọc và dễ hiểu, các lập trình viên sẽ dễ dàng làm việc trên codebase hơn. Điều này có thể dẫn đến tăng năng suất làm việc và giảm thiểu lỗi. Ngoài ra, khi code dễ duy trì, nó đảm bảo rằng code có thể được cải thiện và cập nhật theo thời gian. Điều này đặc biệt quan trọng đối với các dự án dài hạn, nơi codebase cần được duy trì và cập nhật trong nhiều năm tới.

2. Làm thế nào để đánh giá một codebase có sạch hay không?

Có rất nhiều cách để đánh giá xem một đoạn code có sạch hay không. Ví dụ như: đoạn code đó có được viết rõ ràng, mạch lạc không? Có tuân theo một khuôn mẫu nhất định không? Và tất cả các phần của đoạn code có được sắp xếp gọn gàng không?

Ngoài ra, việc review code cũng giúp chúng ta tìm ra những lỗi sai và đảm bảo rằng đoạn code đó được viết theo cách tốt nhất.

Việc kiểm tra (testing) xem đoạn code có hoạt động đúng như mong muốn không cũng rất quan trọng. Điều này giúp chúng ta phát hiện ra các lỗi sớm và đảm bảo chất lượng của phần mềm.

Có nhiều cách để viết code sạch, ví dụ như sử dụng các công cụ hỗ trợ, làm theo những quy tắc nhất định và xây dựng những thói quen tốt khi viết code. Nếu làm được điều này, các lập trình viên sẽ tạo ra được những đoạn code dễ đọc, dễ hiểu và dễ bảo trì.

Tuy nhiên, việc đánh giá code có sạch sẽ hay không đôi khi cũng phụ thuộc vào quan điểm của từng người và từng dự án. Có thể đoạn code mà người này thấy rất đẹp thì người khác lại không nghĩ vậy.

Dù vậy, vẫn có một số quy tắc chung mà chúng ta có thể áp dụng để viết code sạch hơn. Chúng ta sẽ cùng tìm hiểu về những quy tắc đó nhé.

3. Mẹo và quy ước để viết code sạch hơn

3.1. Hiệu quả, Hiệu suất và Đơn giản

Bất cứ khi nào bạn cần suy nghĩ về cách triển khai một tính năng mới vào codebase hiện có hoặc cách giải quyết một vấn đề cụ thể, hãy luôn ưu tiên ba điều đơn giản này.

a. Hiệu quả

Đầu tiên, code của chúng ta phải hiệu quả, nghĩa là nó phải làm đúng nhiệm vụ mà nó được viết. 

b. Hiệu suất

Thứ hai, một khi chúng ta biết code của mình làm đúng nhiệm vụ của nó, chúng ta nên kiểm tra xem hiệu suất mà nó mang lại. Chương trình có chạy bằng một lượng tài nguyên hợp lý về thời gian hay không? Có thể chạy nhanh hơn và với chiếm ít không gian hơn không?

Để mở rộng về hiệu suất, đây là hai ví dụ về một hàm tính tổng của tất cả các số trong một mảng.

 

// Inefficient Example
function sumArrayInefficient(array) {
  let sum = 0;
  for (let i = 0; i < array.length; i++) {
    sum += array[i];
  }
  return sum;
}

 

Hàm sumArrayInefficient sử dụng một vòng lặp for để duyệt qua từng phần tử trong mảng. Ở mỗi lần lặp, nó lấy giá trị của phần tử đó và cộng vào một biến sum (biến tổng). Sau khi kết thúc vòng lặp, biến sum sẽ chứa tổng của tất cả các số trong mảng.

Cách làm này hoạt động được nhưng không phải là cách tối ưu nhất. Khi xử lý các mảng lớn, cách này có thể làm cho chương trình chạy chậm. Vì vậy, chúng ta cần tìm cách khác hiệu quả hơn để tính tổng các số trong mảng.

 

// Efficient example
function sumArrayEfficient(array) {
  return array.reduce((a, b) => a + b, 0);
}

 

Việc triển khai hàm sumArrayEfficient này sử dụng phương thức reduce để tính tổng các phần tử của mảng. Phương thức reduce áp dụng một hàm cho từng phần tử của mảng và tích lũy kết quả. Trong trường hợp này, hàm chỉ thêm từng phần tử vào giá trị tích lũy, bắt đầu từ 0.

Giải pháp này hiệu quả hơn vì nó chỉ yêu cầu một lần lặp qua mảng và thực hiện phép tính tổng trên từng phần tử khi nó đi qua.

c. Sự đơn giản

Cuối cùng là sự đơn giản. Đây là điều khó đánh giá nhất vì nó mang tính chủ quan, tùy thuộc vào người đọc code. Nhưng một số hướng dẫn chúng ta có thể tuân theo là:

  • Bạn có thể dễ dàng hiểu chương trình đang làm gì ở mỗi dòng không?

  • Các hàm và biến có tên đại diện rõ ràng cho trách nhiệm của chúng không?

  • Code có được thụt lề đúng cách và có khoảng cách với cùng một định dạng trên toàn bộ codebase không?

  • Có tài liệu nào cho code không? Có sử dụng comment để giải thích các phần phức tạp của chương trình không?

  • Bạn có thể nhanh chóng xác định phần nào của codebase có các tính năng nhất định của chương trình không? Bạn có thể xóa/thêm các tính năng mới mà không cần sửa đổi nhiều phần khác của code không?

  • Code có theo cách tiếp cận mô-đun, với các tính năng khác nhau được tách riêng thành các thành phần không?

  • Code có được tái sử dụng khi có thể không?

  • Các quyết định về kiến ​​trúc, thiết kế và triển khai giống nhau được tuân theo như nhau trên toàn bộ codebase không?

3.2. Định dạng và cú pháp

Viết code sạch cần phải sử dụng định dạng và cú pháp nhất quán trong toàn bộ codebase. Điều này giúp code trở nên dễ đọc và dễ hiểu hơn.

Khi code được viết nhất quán, các lập trình viên có thể dễ dàng nhận ra các mẫu và hiểu cách code hoạt động, điều này giúp dễ dàng debug, bảo trì và cập nhật codebase theo thời gian. Sự nhất quán cũng giúp giảm lỗi, vì nó đảm bảo rằng tất cả các lập trình viên đều tuân theo cùng một tiêu chuẩn và quy ước.

Một số điều chúng ta nên nghĩ về định dạng và cú pháp là:

a. Thụt lề và khoảng cách

 

// bad indentation and spacing
const myFunc=(number1,number2)=>{
const result=number1+number2;
return result;
}

// good indentation and spacing
const myFunc = (number1, number2) => {
    const result = number1 + number2
    return result
}

 

Ở đây chúng ta có một ví dụ về cùng một hàm, một hàm được thực hiện mà không có thụt lề và khoảng cách, và hàm kia được giãn cách và thụt lề đúng cách. Chúng ta có thể thấy hàm thứ hai rõ ràng dễ đọc hơn rất nhiều.

b. Cú pháp nhất quán

 

// Arrow function, no colons, no return
const multiplyByTwo = number => number * 2

// Function, colons, return
function multiplyByThree(number) {
    return number * 3;
}

 

Một lần nữa, chúng ta có các hàm rất giống nhau được triển khai với cú pháp khác nhau. Cái đầu tiên là một hàm mũi tên (arrow function), không có dấu hai chấm và không có return, trong khi cái kia là một hàm thông thường sử dụng dấu hai chấm và một return.

Cả hai đều hoạt động và đều ổn, nhưng chúng ta nên cố gắng luôn sử dụng cùng một cú pháp cho các thao tác tương tự, vì nó trở nên đều hơn và dễ đọc hơn trong toàn bộ codebase.

Linterns và code formatters là những công cụ tuyệt vời mà các bạn có thể sử dụng trong các dự án để tự động hóa cú pháp và quy ước định dạng trong codebase. 

c. Quy ước về chữ viết nhất quán

 

// camelCase
const myName = 'John'
// PascalCase
const MyName = 'John'
// snake_case
const my_name = 'John'

 

Điều tương tự cũng áp dụng cho quy ước chữ viết mà chúng ta chọn. Tất cả đều hoạt động, nhưng các bạn nên cố gắng sử dụng nhất quán cùng một quy ước trong suốt dự án.

3.3. Đặt tên

Đặt tên cho các biến và hàm một cách rõ ràng và có tính mô tả là một khía cạnh quan trọng của việc viết code sạch. Điều này giúp cải thiện tính dễ đọc và khả năng bảo trì của codebase. Khi đặt tên phù hợp, các lập trình viên khác có thể nhanh chóng hiểu biến hoặc hàm đang làm gì và nó liên quan đến phần còn lại của code như thế nào.

Dưới đây là hai ví dụ trong JavaScript minh họa tầm quan trọng của việc đặt tên rõ ràng:

 

// Example 1: Poor Naming
function ab(a, b) {
  let x = 10;
  let y = a + b + x;
  console.log(y);
}

ab(5, 3);

 

Trong ví dụ này, chúng ta có một hàm nhận hai tham số, cộng chúng vào một giá trị cố định là 10 và ghi kết quả ra console. Tên hàm và tên biến được đặt chưa phù hợp và không cung cấp bất kỳ thông tin nào về chức năng của hàm hoặc những gì các biến đại diện.

 

// Example 1: Good Naming
function calculateTotalWithTax(basePrice, taxRate) {
  const BASE_TAX = 10;
  const totalWithTax = basePrice + (basePrice * (taxRate / 100)) + BASE_TAX;
  console.log(totalWithTax);
}

calculateTotalWithTax(50, 20);

 

Trong ví dụ này, chúng ta có một hàm tính tổng giá của một sản phẩm bao gồm thuế. Tên hàm và tên biến được đặt rõ ràng và cung cấp chức năng của hàm và những gì các biến đại diện.

Điều này làm cho code dễ đọc và dễ hiểu hơn, đặc biệt đối với các lập trình viên khác có thể làm việc với codebase trong tương lai.

3.4. Sự ngắn gọn và rõ ràng

Khi viết code, chúng ta luôn muốn code của mình vừa ngắn gọn, dễ nhìn lại vừa dễ hiểu. Ngắn gọn giúp cho code gọn gàng, dễ quản lý. Nhưng nếu quá ngắn gọn mà không chú ý đến rõ ràng, code sẽ trở nên khó đọc và dễ gây hiểu nhầm, nhất là khi có nhiều người cùng làm việc trên một dự án.

Dưới đây là hai ví dụ minh họa tầm quan trọng của sự ngắn gọn và rõ ràng:

 

// Example 1: Concise function
const countVowels = s => (s.match(/[aeiou]/gi) || []).length;
console.log(countVowels("hello world"));

 

Ví dụ này dùng một cách viết hàm rất ngắn gọn (hàm mũi tên) kết hợp với một công cụ tìm kiếm văn bản đặc biệt (biểu thức chính quy) để đếm xem trong một đoạn văn bản có bao nhiêu chữ cái nguyên âm (a, e, i, o, u).

Mặc dù cách viết này rất ngắn gọn và tiện lợi, nhưng có thể sẽ hơi khó hiểu với những người chưa quen với cách sử dụng công cụ tìm kiếm văn bản này (biểu thức chính quy). Nói cách khác, nếu bạn chưa biết về biểu thức chính quy thì sẽ khó hiểu đoạn code này hoạt động như thế nào.

 

// Example 2: More verbose and clearer function
function countVowels(s) {
  const vowelRegex = /[aeiou]/gi;
  const matches = s.match(vowelRegex) || [];
  return matches.length;
}

console.log(countVowels("hello world"));

 

Ví dụ này sử dụng một hàm truyền thống và biểu thức chính quy để đếm số lượng nguyên âm trong một chuỗi cho trước, nhưng thực hiện theo cách rõ ràng và dễ hiểu. Tên hàm tên biến được mô tả và mẫu biểu thức chính quy được lưu trữ trong một biến có tên rõ ràng. Điều này giúp dev dễ dàng xem chức năng là gì và cách thức hoạt động của nó.

Bằng cách sử dụng tên hàm &  biến mô tả và sử dụng định dạng &  nhận xét mã rõ ràng, dễ đọc, bạn có thể viết code sạch và ngắn gọn dễ hiểu.

3.5. Khả năng tái sử dụng

Khả năng tái sử dụng code là một khái niệm cơ bản trong kỹ thuật phần mềm, đề cập đến khả năng sử dụng code nhiều lần mà không cần sửa đổi.

Tầm quan trọng của việc tái sử dụng code nằm ở việc nó có thể cải thiện đáng kể hiệu quả và năng suất của phát triển phần mềm bằng cách giảm lượng code cần viết và kiểm thử.

Bằng cách tái sử dụng code hiện có, các lập trình viên có thể tiết kiệm thời gian và công sức, cải thiện chất lượng và tính nhất quán của code, đồng thời giảm thiểu rủi ro về lỗi. Code có thể tái sử dụng cũng cho phép có nhiều kiến ​​trúc phần mềm mô-đun và có thể mở rộng hơn, giúp dễ dàng bảo trì và cập nhật cơ sở mã theo thời gian.

Khả năng tái sử dụng code là một điều rất quan trọng trong lập trình. Nó giống như việc bạn có một bộ đồ chơi Lego. Thay vì phải tạo ra từng viên gạch nhỏ một mỗi khi muốn xây một tòa nhà mới, bạn có thể tận dụng những viên gạch đã có sẵn để lắp ráp thành những công trình khác nhau. 

 

// Example 1: No re-usability
function calculateCircleArea(radius) {
  const PI = 3.14;
  return PI * radius * radius;
}

function calculateRectangleArea(length, width) {
  return length * width;
}

function calculateTriangleArea(base, height) {
  return (base * height) / 2;
}

const circleArea = calculateCircleArea(5);
const rectangleArea = calculateRectangleArea(4, 6);
const triangleArea = calculateTriangleArea(3, 7);

console.log(circleArea, rectangleArea, triangleArea);

 

Ví dụ này tạo ra ba công thức toán học, mỗi công thức tính diện tích của một hình khác nhau: hình tròn, hình chữ nhật và hình tam giác. Mặc dù mỗi công thức làm một việc riêng biệt, nhưng chúng đều được viết riêng lẻ, không thể dùng chung cho các tình huống khác.

Hơn nữa, cách viết này còn có một vấn đề nữa là giá trị số Pi (3.14...) được viết mặc định vào trong công thức. Nếu sau này muốn thay đổi giá trị này thì phải sửa lại ở tất cả các chỗ đã viết số Pi, rất dễ gây ra lỗi.

Cuối cùng, cách viết này còn lặp lại những đoạn code giống nhau nhiều lần, khiến cho chương trình trở nên dài dòng và khó đọc.

 

// Example 2: Implementing re-usability
function calculateArea(shape, ...args) {
  if (shape === 'circle') {
    const [radius] = args;
    const PI = 3.14;
    return PI * radius * radius;
  } else if (shape === 'rectangle') {
    const [length, width] = args;
    return length * width;
  } else if (shape === 'triangle') {
    const [base, height] = args;
    return (base * height) / 2;
  } else {
    throw new Error(`Shape "${shape}" not supported.`);
  }
}

const circleArea = calculateArea('circle', 5);
const rectangleArea = calculateArea('rectangle', 4, 6);
const triangleArea = calculateArea('triangle', 3, 7);

console.log(circleArea, rectangleArea, triangleArea);

 

Thay vì viết nhiều công thức tính diện tích riêng lẻ cho từng hình, ta có thể tạo một hàm duy nhất để tính diện tích của nhiều hình khác nhau. Hàm này sẽ tự động chọn cách tính phù hợp dựa vào loại hình mà bạn nhập vào. Cách này giúp code gọn gàng, dễ hiểu và dễ mở rộng hơn rất nhiều.

Như ở ví dụ trên, thay vì viết riêng hàm tính diện tích hình tròn, hình chữ nhật, ta chỉ cần một hàm calculateArea duy nhất, nhập vào hình dạng và các thông số cần thiết để cho ra kết quả mà ta cần.

3.6. Luồng thực thi rõ ràng

Để viết code sạch, điều quan trọng là phải có một luồng thực thi rõ ràng. Tưởng tượng như một công thức nấu ăn, nếu các bước được liệt kê một cách mạch lạc, bạn sẽ dễ dàng làm theo và cho ra được món ăn ngon. Tương tự, nếu code được viết theo một trình tự logic, người đọc sẽ dễ dàng nắm bắt ý tưởng và tìm ra lỗi nếu có.

Ngược lại, spaghetti code là một thuật ngữ dùng để chỉ những đoạn code rối rắm, khó theo dõi, giống như một bát mì Ý bị vướng vào nhau. Những đoạn code này thường rất dài, các phần khác nhau đan xen vào nhau một cách phức tạp, không có một cấu trúc rõ ràng. Điều này khiến cho việc tìm hiểu, sửa chữa hoặc mở rộng mã trở nên rất khó khăn.

Để bạn hình dung rõ hơn, sau đây là hai ví dụ về đoạn code JavaScript thực hiện cùng một việc, nhưng một đoạn viết theo cách rõ ràng và đoạn còn lại là spaghetti code.

 

// Example 1: Clear flow of execution
function calculateDiscount(price, discountPercentage) {
  const discountAmount = price * (discountPercentage / 100);
  const discountedPrice = price - discountAmount;
  return discountedPrice;
}

const originalPrice = 100;
const discountPercentage = 20;
const finalPrice = calculateDiscount(originalPrice, discountPercentage);

console.log(finalPrice);

// Example 2: Spaghetti code
const originalPrice = 100;
const discountPercentage = 20;

let discountedPrice;
let discountAmount;
if (originalPrice && discountPercentage) {
  discountAmount = originalPrice * (discountPercentage / 100);
  discountedPrice = originalPrice - discountAmount;
}

if (discountedPrice) {
  console.log(discountedPrice);
}

 

Như bạn có thể thấy, ví dụ 1 tuân theo một cấu trúc rõ ràng và hợp lý, với một hàm nhận các tham số cần thiết và trả về kết quả đã tính toán. Mặt khác, ví dụ 2 phức tạp hơn nhiều, với các biến được khai báo bên ngoài tất cả các hàm và nhiều câu lệnh if được sử dụng để kiểm tra xem khối mã đã thực thi thành công hay chưa.

3.7. Nguyên tắc đơn nhiệm (Single Responsibility Principle)

Nguyên tắc đơn nhiệm (SRP) là một nguyên tắc trong phát triển phần mềm quy định rằng mỗi lớp hoặc mô-đun chỉ nên có một lý do để thay đổi, hoặc nói cách khác, mỗi thực thể trong codebase nên có một trách nhiệm duy nhất. Nguyên tắc này giúp tạo ra code dễ hiểu, dễ bảo trì và dễ mở rộng.

Bằng cách áp dụng SRP, bạn có thể tạo ra code dễ kiểm thử, tái sử dụng và tái cấu trúc hơn, vì mỗi mô-đun chỉ xử lý một trách nhiệm duy nhất. Điều này làm giảm các tác dụng phụ hoặc phụ thuộc có thể khiến code khó làm việc hơn.

 

// Example 1: Withouth SRP
function processOrder(order) {
  // validate order
  if (order.items.length === 0) {
    console.log("Error: Order has no items");
    return;
  }
  
  // calculate total
  let total = 0;
  order.items.forEach(item => {
    total += item.price * item.quantity;
  });
  
  // apply discounts
  if (order.customer === "vip") {
    total *= 0.9;
  }
  
  // save order
  const db = new Database();
  db.connect();
  db.saveOrder(order, total);
}

 

Trong ví dụ này, hàm processOrder xử lý một số trách nhiệm: xác thực đơn hàng, tính tổng, áp dụng giảm giá và lưu đơn hàng vào cơ sở dữ liệu. Điều này làm cho hàm dài và khó hiểu, và bất kỳ thay đổi nào đối với một trách nhiệm có thể ảnh hưởng đến những trách nhiệm khác, khiến việc bảo trì khó khăn hơn.

 

// Example 2: With SRP
class OrderProcessor {
  constructor(order) {
    this.order = order;
  }
  
  validate() {
    if (this.order.items.length === 0) {
      console.log("Error: Order has no items");
      return false;
    }
    return true;
  }
  
  calculateTotal() {
    let total = 0;
    this.order.items.forEach(item => {
      total += item.price * item.quantity;
    });
    return total;
  }
  
  applyDiscounts(total) {
    if (this.order.customer === "vip") {
      total *= 0.9;
    }
    return total;
  }
}

class OrderSaver {
  constructor(order, total) {
    this.order = order;
    this.total = total;
  }
  
  save() {
    const db = new Database();
    db.connect();
    db.saveOrder(this.order, this.total);
  }
}

const order = new Order();
const processor = new OrderProcessor(order);

if (processor.validate()) {
  const total = processor.calculateTotal();
  const totalWithDiscounts = processor.applyDiscounts(total);
  const saver = new OrderSaver(order, totalWithDiscounts);
  saver.save();
}

 

Trong ví dụ này, hàm processOrder đã được chia thành hai lớp tuân theo SRP: OrderProcessor OrderSaver.

Lớp OrderProcessor xử lý trách nhiệm xác thực đơn hàng, tính tổng và áp dụng giảm giá, trong khi lớp OrderSaver xử lý trách nhiệm lưu đơn hàng vào cơ sở dữ liệu.

Điều này làm cho code dễ hiểu, kiểm tra và bảo trì hơn, vì mỗi lớp có một trách nhiệm rõ ràng và có thể được sửa đổi hoặc thay thế mà không ảnh hưởng đến những lớp khác.

3.8. Nguồn dữ liệu tin cậy duy nhất (Single Source of Truth)

Có một nguồn dữ liệu tin cậy duy nhất có nghĩa là chỉ có một nơi mà một mẫu dữ liệu hoặc cấu hình cụ thể được lưu trữ trong codebase và bất kỳ tham chiếu nào khác đến nó trong code đều tham chiếu lại nguồn đó. Điều này quan trọng vì nó đảm bảo dữ liệu nhất quán và tránh trùng lặp.

Dưới đây là một ví dụ để minh họa khái niệm. Giả sử chúng ta có một ứng dụng cần hiển thị điều kiện thời tiết hiện tại trong một thành phố. Chúng ta có thể thực hiện tính năng này theo hai cách khác nhau:

 

// Option 2: "Single source of truth"

// file 1: weatherAPI.js
const apiKey = '12345abcde';

function getCurrentWeather(city) {
  return fetch(`https://api.weather.com/conditions/v1/${city}?apiKey=${apiKey}`)
    .then(response => response.json());
}

export { getCurrentWeather, apiKey };


// file 2: weatherComponent.js
import { getCurrentWeather } from './weatherAPI';

function displayCurrentWeather(city) {
  getCurrentWeather(city)
    .then(weatherData => {
      // display weatherData on the UI
    });
}

 

Trong tùy chọn này, khóa API được sao chép trong hai tệp khác nhau, khiến việc duy trì và cập nhật trở nên khó khăn hơn. Nếu chúng ta cần thay đổi khóa API, chúng ta phải nhớ cập nhật khóa ở cả hai nơi.

 

// Option 1: No "single source of truth"

// file 1: weatherAPI.js
const apiKey = '12345abcde';

function getCurrentWeather(city) {
  return fetch(`https://api.weather.com/conditions/v1/${city}?apiKey=${apiKey}`)
    .then(response => response.json());
}

// file 2: weatherComponent.js
const apiKey = '12345abcde';

function displayCurrentWeather(city) {
  getCurrentWeather(city)
    .then(weatherData => {
      // display weatherData on the UI
    });
}

 

Trong tùy chọn này, khóa API được lưu trữ ở một nơi (trong tệp weatherAPI.js) và được xuất để các mô-đun khác sử dụng. Điều này đảm bảo rằng chỉ có một nguồn dữ liệu cho khóa API giúp tránh trùng lặp và thiếu sự nhất quán.

Nếu bạn cần cập nhật khóa API, bạn có thể thực hiện ở một nơi và tất cả các mô-đun khác sử dụng khóa đó sẽ tự động nhận được giá trị cập nhật.

3.9. Chỉ tiết lộ và sử dụng dữ liệu cần thiết

Một nguyên tắc quan trọng của việc viết code sạch là chỉ tiết lộ và sử dụng thông tin cần thiết cho một nhiệm vụ cụ thể. Điều này giúp giảm độ phức tạp, tăng hiệu suất và tránh lỗi có thể phát sinh từ việc sử dụng dữ liệu không cần thiết.

Khi dữ liệu không cần thiết được tiết lộ hoặc sử dụng, nó có thể dẫn đến các vấn đề về hiệu suất và làm cho code khó hiểu và khó bảo trì hơn.

Giả sử bạn có một đối tượng với nhiều thuộc tính, nhưng bạn chỉ cần sử dụng một vài trong số chúng. Một cách để làm điều này là tham chiếu đối tượng và các thuộc tính cụ thể mỗi khi bạn cần chúng. Nhưng việc này có thể trở nên dài dòng và dễ xảy ra lỗi, đặc biệt nếu đối tượng được lồng sâu. Một giải pháp khả thi hơn và hiệu quả hơn sẽ là sử dụng giải cấu trúc đối tượng để chỉ tiết lộ và sử dụng thông tin bạn cần.

 

// Original object
const user = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  age: 25,
  address: {
    street: '123 Main St',
    city: 'Anytown',
    state: 'CA',
    zip: '12345'
  }
};

// Only expose and consume the name and email properties
const { name, email } = user;

console.log(name); // 'Alice'
console.log(email); // 'alice@example.com'

 

3.10. Mô-đun hóa

Mô đun hóa là một khái niệm thiết yếu trong việc viết code sạch. Nó đề cập đến việc phân chia code lớn, phức tạp thành các mô-đun hoặc hàm nhỏ hơn, dễ quản lý hơn. 

Sử dụng mô đun hóa mang lại một số lợi ích như:

  • Tái sử dụng: Các mô-đun có thể được tái sử dụng trong các phần khác nhau của ứng dụng hoặc trong các ứng dụng khác, tiết kiệm thời gian và công sức trong quá trình phát triển.

  • Tính đóng gói (Encapsulation): Các mô-đun cho phép bạn ẩn các chi tiết bên trong của một hàm hoặc đối tượng, chỉ tiết lộ giao diện cần thiết cho bên ngoài. Điều này giúp giảm sự kết hợp giữa các phần khác nhau của code và cải thiện chất lượng tổng thể của code.

  • Khả năng mở rộng: Bằng cách chia nhỏ code lớn thành các phần nhỏ hơn, bạn có thể dễ dàng thêm hoặc xóa chức năng mà không ảnh hưởng đến toàn bộ codebase.

Sau đây là một ví dụ về một đoạn code thực hiện một tác vụ đơn giản không sử dụng mô-đun hóa.

 

// Without modularization
function calculatePrice(quantity, price, tax) {
  let subtotal = quantity * price;
  let total = subtotal + (subtotal * tax);
  return total;
}

// Without modularization
let quantity = parseInt(prompt("Enter quantity: "));
let price = parseFloat(prompt("Enter price: "));
let tax = parseFloat(prompt("Enter tax rate: "));

let total = calculatePrice(quantity, price, tax);
console.log("Total: $" + total.toFixed(2));

 

Trong ví dụ trên, hàm calculatePrice được sử dụng để tính tổng giá của một mặt hàng dựa trên số lượng, giá và thuế của nó. Tuy nhiên, hàm này không được mô đun hóa và được kết hợp chặt chẽ với input và output của người dùng. Nó có thể khiến việc kiểm tra và bảo trì trở nên khó khăn.

Bây giờ, hãy xem một ví dụ về cùng một code sử dụng mô đun hóa:

 

// With modularization
function calculateSubtotal(quantity, price) {
  return quantity * price;
}

function calculateTotal(subtotal, tax) {
  return subtotal + (subtotal * tax);
}

// With modularization
let quantity = parseInt(prompt("Enter quantity: "));
let price = parseFloat(prompt("Enter price: "));
let tax = parseFloat(prompt("Enter tax rate: "));

let subtotal = calculateSubtotal(quantity, price);
let total = calculateTotal(subtotal, tax);
console.log("Total: $" + total.toFixed(2));

 

Trong ví dụ trên, hàm calculatePrice đã được chia nhỏ thành hai hàm nhỏ hơn: calculateSubtotal calculateTotal. Các hàm này hiện đã được mô đun hóa và chịu trách nhiệm tính tổng phụ và tổng, tương ứng. Điều này làm cho code dễ hiểu, kiểm tra và bảo trì hơn, đồng thời nó có thể tái sử dụng trong các phần khác của ứng dụng.

Ngoài việc chia nhỏ các hàm, mô đun hóa còn có thể áp dụng cho cả các tệp mã. Thay vì viết tất cả code vào một tệp duy nhất, chúng ta chia thành nhiều tệp nhỏ hơn. Mỗi tệp sẽ chứa một phần chức năng riêng biệt. Điều này giúp cho dự án trở nên có tổ chức, dễ quản lý hơn và cũng giúp nhiều người cùng làm việc trên một dự án lớn.

3.11. Cấu trúc thư mục

Chọn một cấu trúc thư mục tốt là một phần quan trọng của việc viết code sạch. Khi một dự án có cấu trúc thư mục rõ ràng, các lập trình viên sẽ dễ dàng tìm thấy đoạn code cần sửa chữa hoặc muốn thêm tính năng mới. Điều này giúp tiết kiệm thời gian và giảm thiểu lỗi.

Ngược lại, nếu cấu trúc thư mục lộn xộn, các tệp được đặt lung tung, bạn sẽ rất khó tìm thấy những gì mình cần và dễ mắc lỗi khi sửa đổi code.

Dưới đây là các ví dụ về một cấu trúc thư mục tốt và xấu bằng cách sử dụng một dự án React làm ví dụ:

 

// Bad folder structure
my-app/
├── App.js
├── index.js
├── components/
│   ├── Button.js
│   ├── Card.js
│   └── Navbar.js
├── containers/
│   ├── Home.js
│   ├── Login.js
│   └── Profile.js
├── pages/
│   ├── Home.js
│   ├── Login.js
│   └── Profile.js
└── utilities/
    ├── api.js
    └── helpers.js

 

Trong ví dụ này, cấu trúc dự án được tổ chức xung quanh các loại tệp, chẳng hạn như components, containers, và pages.

Nhưng cách tiếp cận này có thể dẫn đến nhầm lẫn và trùng lặp, vì không rõ tệp nào thuộc về đâu. Ví dụ, thành phần Home có trong cả thư mục container và pages. Nó cũng có thể gây khó khăn cho việc tìm và sửa đổi mã, vì các lập trình viên có thể cần điều hướng nhiều thư mục để tìm code họ cần.

 

// Good folder structure
my-app/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.js
│   │   │   ├── Button.module.css
│   │   │   └── index.js
│   │   ├── Card/
│   │   │   ├── Card.js
│   │   │   ├── Card.module.css
│   │   │   └── index.js
│   │   └── Navbar/
│   │       ├── Navbar.js
│   │       ├── Navbar.module.css
│   │       └── index.js
│   ├── pages/
│   │   ├── Home/
│   │   │   ├── Home.js
│   │   │   ├── Home.module.css
│   │   │   └── index.js
│   │   ├── Login/
│   │   │   ├── Login.js
│   │   │   ├── Login.module.css
│   │   │   └── index.js
│   │   └── Profile/
│   │       ├── Profile.js
│   │       ├── Profile.module.css
│   │       └── index.js
│   ├── utils/
│   │   ├── api.js
│   │   └── helpers.js
│   ├── App.js
│   └── index.js
└── public/
    ├── index.html
    └── favicon.ico

 

Trong ví dụ này, cấu trúc dự án được tổ chức xung quanh các tính năng, chẳng hạn như components, pages và utils. Mỗi tính năng có thư mục riêng, chứa tất cả các tệp liên quan đến tính năng đó.

Cách tiếp cận này giúp dễ dàng tìm thấy và sửa đổi code, vì tất cả các tệp liên quan đến một tính năng được đặt trong cùng một thư mục. Nó cũng giúp giảm trùng lặp mã và độ phức tạp, vì các tính năng được tách ra và các tệp liên quan của chúng được tổ chức cùng nhau. Tóm lại, một cấu trúc thư mục tốt nên được tổ chức xung quanh các tính năng, không phải các loại tệp. 

3.12. Documentation

Tài liệu (Documentation) như một bản hướng dẫn chi tiết đi kèm với một chương trình máy tính. Nó giúp người lập trình, cả người tạo ra chương trình và những người khác muốn hiểu cách nó hoạt động, có thể nắm bắt được ý tưởng và logic đằng sau từng dòng code.

Tại sao tài liệu lại quan trọng?

  • Hiểu rõ hơn: Khi bạn quay lại xem lại code của mình sau một thời gian, hoặc khi người khác muốn sửa chữa hoặc bổ sung tính năng mới, tài liệu sẽ giúp họ nhanh chóng hiểu được đoạn code đó đang làm gì.

  • Dễ bảo trì: Nếu code được giải thích rõ ràng, việc tìm và sửa lỗi, hoặc nâng cấp chương trình sẽ trở nên dễ dàng hơn rất nhiều.

  • Làm việc nhóm hiệu quả: Khi nhiều người cùng làm việc trên một dự án, tài liệu sẽ giúp mọi người hiểu rõ vai trò của từng phần code và tránh những hiểu lầm không đáng có.

Các cách viết tài liệu:

  • Bình luận: Bạn có thể viết những ghi chú giải thích ngay bên cạnh đoạn code. 

  • Tài liệu nội dòng (Inline Documentation): Đây là những đoạn văn bản mô tả chi tiết về một hàm, một lớp hoặc một đoạn code nào đó. Thông thường, chúng được viết theo một định dạng nhất định để các công cụ có thể tự động tạo ra tài liệu cho toàn bộ dự án.

  • Công cụ hỗ trợ: Có nhiều công cụ như JSDoc, TypeScript giúp bạn tự động tạo ra tài liệu từ code của mình.

Lời kết

VietnamWorks inTECH hy vọng, từ những chia sẻ trên đây các bạn đã phần nào hiểu rõ hơn về clean code và cách để viết code sạch. Nếu thấy bài viết hữu ích, đừng quên chia sẻ cho bạn bè cùng biết nhé.

Nguồn: freecodecamp

VietnamWorks inTECH

TẠO TÀI KHOẢN MỚI: XEM FULL “1 TÁCH CODEFEE” - NHẬN SLOT TƯ VẤN CV TỪ CHUYÊN GIA - CƠ HỘI RINH VỀ VOUCHER 200K