+2

[Translate] Clean code JS - SOLID (End)

SOLID

Nguyên lí đơn trách nhiệm (Single Responsibility Principle)

Như đã được nói đến trong cuốn Clean Code, "Chỉ có thể thay đổi một lớp vì một lí do duy nhất". Thật là hấp dẫn để nhồi nhét nhiều chức năng vào cho một lớp, giống như là khi bạn chỉ có thể lấy một chiếc vali cho chuyến bay vậy. Vấn đề là lớp của bạn sẽ không được hiểu gắn kết về mặt khái niệm của nó và sẽ có rất nhiều lí do để thay đổi. Việc làm giảm thiểu số lần bạn cần phải thay đổi một lớp là một việc quan trọng. Nó quan trọng bởi vì nếu có quá nhiều chức năng trong một lớp và bạn chỉ muốn thay đổi một chút xíu của lớp đó, thì có thể sẽ rất khó để hiểu được việc thay đổi đó sẽ ảnh hưởng đến những module khác trong codebase của bạn như thế nào.

Không tốt:

class UserSettings {
  constructor(user) {
    this.user = user;
  }

  changeSettings(settings) {
    if (this.verifyCredentials()) {
      // ...
    }
  }

  verifyCredentials() {
    // ...
  }
}

Tốt:

class UserAuth {
  constructor(user) {
    this.user = user;
  }

  verifyCredentials() {
    // ...
  }
}


class UserSettings {
  constructor(user) {
    this.user = user;
    this.auth = new UserAuth(user);
  }

  changeSettings(settings) {
    if (this.auth.verifyCredentials()) {
      // ...
    }
  }
}

Nguyên lí đóng mở (Open/Closed Principle)

Betrand Meyer đã nói "có thể thoải mái mở rộng một module, nhưng hạn chế sửa đổi bên trong module đó". Điều đó nghĩa là gì? Nguyên tắc này cơ bản nhấn mạnh rằng bạn phải cho phép người dùng thêm các chức năng mới mà không làm thay đổi các code đang có.

Không tốt:

class AjaxAdapter extends Adapter {
  constructor() {
    super();
    this.name = 'ajaxAdapter';
  }
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
    this.name = 'nodeAdapter';
  }
}

class HttpRequester {
  constructor(adapter) {
    this.adapter = adapter;
  }

  fetch(url) {
    if (this.adapter.name === 'ajaxAdapter') {
      return makeAjaxCall(url).then((response) => {
        // transform response and return
      });
    } else if (this.adapter.name === 'httpNodeAdapter') {
      return makeHttpCall(url).then((response) => {
        // transform response and return
      });
    }
  }
}

function makeAjaxCall(url) {
  // request and return promise
}

function makeHttpCall(url) {
  // request and return promise
}

Tốt:

class AjaxAdapter extends Adapter {
  constructor() {
    super();
    this.name = 'ajaxAdapter';
  }

  request(url) {
    // request and return promise
  }
}

class NodeAdapter extends Adapter {
  constructor() {
    super();
    this.name = 'nodeAdapter';
  }

  request(url) {
    // request and return promise
  }
}

class HttpRequester {
  constructor(adapter) {
    this.adapter = adapter;
  }

  fetch(url) {
    return this.adapter.request(url).then((response) => {
      // transform response and return
    });
  }
}

Nguyên lí thay thế Liskov (Liskov Substitution Principle)

Đây là một thuật ngữ đáng sợ cho một khái niệm rất đơn giản. Nó được định nghĩa một cách chính thức là: "Nếu S là một kiểu con của T, thì các đối tượng của kiểu T có thể được thay thế bằng các đối tượng của kiểu S (ví dụ các đối tượng của kiểu S có thể thay thế các đối tượng của kiểu T) mà không làm thay đổi bất kì thuộc tính mong muốn nào của chương trình đó (tính chính xác, thực hiện tác vụ, ..). Đó thậm chí còn là một định nghĩa đáng sợ hơn.

Sự giải thích tốt nhất cho nguyên lí này là, nếu bạn có một lớp cha và một lớp con, thì lớp cơ sở và lớp con có thể được sử dụng thay thế cho nhau mà không làm thay đổi tính đúng đắn của chương trình. Có thể vẫn còn hơi rối ở đây, vậy hãy xem cái ví dụ cổ điển hình vuông-hình chữ nhật (Square-Rectangle) dưới đây. Về mặt toán học, một hình vuông là một hình chữ nhật, tuy nhiên nếu bạn mô hình hoá điều này sử dụng quan hệ "is a" thông qua việc kế thừa, bạn sẽ nhanh chóng gặp phải rắc rối đấy.

Không tốt:

class Rectangle {
  constructor() {
    this.width = 0;
    this.height = 0;
  }

  setColor(color) {
    // ...
  }

  render(area) {
    // ...
  }

  setWidth(width) {
    this.width = width;
  }

  setHeight(height) {
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Square extends Rectangle {
  setWidth(width) {
    this.width = width;
    this.height = width;
  }

  setHeight(height) {
    this.width = height;
    this.height = height;
  }
}

function renderLargeRectangles(rectangles) {
  rectangles.forEach((rectangle) => {
    rectangle.setWidth(4);
    rectangle.setHeight(5);
    const area = rectangle.getArea(); // BAD: Will return 25 for Square. Should be 20.
    rectangle.render(area);
  });
}

const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

Tốt:

class Shape {
  setColor(color) {
    // ...
  }

  render(area) {
    // ...
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  getArea() {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(length) {
    super();
    this.length = length;
  }

  getArea() {
    return this.length * this.length;
  }
}

function renderLargeShapes(shapes) {
  shapes.forEach((shape) => {
      const area = shape.getArea();
      shape.render(area);
    });
  }

const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);

Nguyên lí phân tách interface (Interface Segregation Principle)

JavaScript không có interface vì vậy nguyên lí này không áp dụng một cách chặt chẽ như các nguyên lí khác. Tuy nhiên, nó cũng quan trọng và liên quan ngay cả với hệ thống thiếu định kiểu của JavaScript.

Nguyên lí phân tách interface nhấn mạnh rằng "Người dùng không nên bị bắt buộc phải phụ thuộc vào các interfaces mà họ không sử dụng." Interface là những ràng buộc ẩn trong JavaScript bởi vì duck typing.

Một ví dụ tốt để minh hoạ cho nguyên lí này trong JavaScript là các lớp mà yêu cầu cài đặt các đối tượng lớn. Việc không yêu cầu người dùng thiết lập một số lượng lớn các tuỳ chọn là một ích lợi, bởi vì đa số thời gian họ không cần tất cả các cài đặt. Làm cho chúng trở thành tuỳ chọn giúp tránh được việc có một "fat interface".

Không tốt:

class DOMTraverser {
  constructor(settings) {
    this.settings = settings;
    this.setup();
  }

  setup() {
    this.rootNode = this.settings.rootNode;
    this.animationModule.setup();
  }

  traverse() {
    // ...
  }
}

const $ = new DOMTraverser({
  rootNode: document.getElementsByTagName('body'),
  animationModule() {} // Most of the time, we won't need to animate when traversing.
  // ...
});

Tốt:

class DOMTraverser {
  constructor(settings) {
    this.settings = settings;
    this.options = settings.options;
    this.setup();
  }

  setup() {
    this.rootNode = this.settings.rootNode;
    this.setupOptions();
  }

  setupOptions() {
    if (this.options.animationModule) {
      // ...
    }
  }

  traverse() {
    // ...
  }
}

const $ = new DOMTraverser({
  rootNode: document.getElementsByTagName('body'),
  options: {
    animationModule() {}
  }
});

Nguyên lí đảo ngược dependency (Dependency Inversion Principle)

Nguyên lí này khẳng định hai điều cần thiết sau:

  1. Nhưng module cấp cao không nên phụ thuộc vào những module cấp thấp. Cả hai nên phụ thuộc vào abstraction.
  2. Abstraction (interface) không nên phụ thuộc vào chi tiết, mà ngược lại.

Điều này có thể khó hiểu lúc ban đầu, nhưng nếu bạn đã từng làm việc với Angular.js, bạn đã thấy một sự hiện thực của nguyên lí này trong dạng của Dependency Injection (DI). Khi chúng không phải là các khái niệm giống nhau, DIP giữ cho module cấp cao không biết chi tiết các module cấp thấp của nó và thiết lập chúng. Có thể đạt được điều này thông qua DI. Một lợi ích to lớn của DIP là nó làm giảm sự phụ thuộc lẫn nhau giữa các module. Sự phụ thuộc lẫn nhau là một kiểu mẫu không tốt, vì nó làm cho việc tái cấu trúc code trở nên khó khăn.

Như đã khẳng định ở trước, JavaScript không có interface vì vậy các abstraction mà bị phụ thuộc là những ràng buộc ẩn. Đó là để nói, các phương thức và thuộc tính mà một đối tượng/lớp làm phơi bày đối tượng/lớp khác. Trong ví dụ bên dưới, sự ràng buộc ẩn là bất cứ module Request cho một InventoryRequester sẽ có một phương thức requestItems.

Không tốt:

class InventoryRequester {
  constructor() {
    this.REQ_METHODS = ['HTTP'];
  }

  requestItem(item) {
    // ...
  }
}

class InventoryTracker {
  constructor(items) {
    this.items = items;

    // Không tốt: chúng ta đã tạo một phụ thuộc vào một hiện thực của một request cụ thể
    // Chúng ta nên có những requestItems phụ thuộc vào một phương thức request `request`
    this.requester = new InventoryRequester();
  }

  requestItems() {
    this.items.forEach((item) => {
      this.requester.requestItem(item);
    });
  }
}

const inventoryTracker = new InventoryTracker(['apples', 'bananas']);
inventoryTracker.requestItems();

Tốt:

class InventoryTracker {
  constructor(items, requester) {
    this.items = items;
    this.requester = requester;
  }

  requestItems() {
    this.items.forEach((item) => {
      this.requester.requestItem(item);
    });
  }
}

class InventoryRequesterV1 {
  constructor() {
    this.REQ_METHODS = ['HTTP'];
  }

  requestItem(item) {
    // ...
  }
}

class InventoryRequesterV2 {
  constructor() {
    this.REQ_METHODS = ['WS'];
  }

  requestItem(item) {
    // ...
  }
}

// Bằng cách xây dựng các phụ thuộc ở ngoài và thêm chúng vào, chúng ta có thể
// dễ dàng thay thế module request bằng một module mới lạ sử dụng WebSockets.
const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
inventoryTracker.requestItems();


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í