EcmaScript Decorators

1-Ifm00n-npUdYWTDbZag3rQ.png

Decorators là một khái niệm khá phổ biến đối với các ngôn ngữ lập trình: chúng ta có attributes trong C#, trong Java người ta gọi là annotations, còn trong Python thì gọi là decorators. Trong JavaScript cũng có khái niệm này và nó khá giống với decorators trong Python, có lẽ đó là lý do tại sao cái tên decorators trong JavaScript được lấy tên tương tự và có các tính năng tương đương.

1. Mẫu Decorator là gì?

Decorator là tên được sử dụng cho mẫu thiết kế (Design pattern) phần mềm, được dùng để thay đổi hành vi, chức năng của hàm hoặc lớp mà không cần phải thay đổi code của hàm hoặc lớp.

Decorator được dùng để làm gì?

Tại sao ta cần Decorator?

  • Cho phép tái sử dụng code.
  • Mở rộng các hàm, hoặc lớp mà không cần phải thay đổi code có sẵn nên không cần test lại.

Decorator trong Python

Trong Python, decorators cung cấp một cú pháp vô cùng đơn giản để gọi các hàm ở mức cao hơn. Một Python Decorator là một hàm mà gọi một hàm khác, kế thừa những hành vi của hàm đó mà không sửa đổi hàm đó. Decorator đơn giản nhất trong Python có dạng:

@decorator_name
def myfunc():
    pass

Kí tự @ chỉ ra rằng ta đang sử dụng một decorator với decorator_name chỉ tới một hàm bằng tên của hàm đó. Decorator của chúng ta nhận một đối số (là tên của hàm được decorate) và trả về một hàm tương đương với hàm đã được thêm vào decorator.

Decorators trong ES5 và ES6

Trong ES5, việc implement một mẫu decorator khá bình thường, không thực sự hữu ích. Trong ES6, trong khi các class hỗ trợ mở rộng, ta cần điều gì đó tốt hơn khi mà các class cần được chia sẻ các hành vi, chức năng đơn lẻ, cần một cái gì đó để giải quyết vấn đề phân phối.

Yehuda Katz cho ra decorator để cho phép chú thích, sửa đổi các lớp JavaScript, các thuộc tính tại thời điểm thiết kế trong khi vẫn giữ được cũ pháp đã khai báo.

Decorators trong ES7

Một ES7 decorator là một các diễn tả để trả về một hàm và có thể lấy được đối tượng, tên và các mô tả thuộc tính. Ta có thể làm được điều đó bằng các đặt kí tự @ ở trước tên decorator và đặt cái mà ta muốn decorate. Decorator có thể định nghĩa classproperty.

Decorator với property

class Cow {
  moo() {
    return `${this.name} says Moooo`;
  }
}

Việc định nghĩa hàm này tương đương với một Cow.prototype như sau:

Object.defineProperty(Cow.prototype, "moo", {
  value: specifiedFunction,
  enumerable: false,
  configurable: true,
  writable: true
});

Giả sử ta muốn cho một thuộc tính hoặc phương thức không thể bị ghi. Một decorator được đặt trước thuộc tính hoặc phương thức đó. Ta có thể định nghĩa một decorator @readonly:

function readonly(target, key, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

và ta thêm decorator đó vào trước phương thức moo:

class Cow {
  @readonly
  moo() {
    return `${this.name} says Moooo`;
  }
}

Và giờ, phương thức moo không thể ghi được nữa. Ta có thể kiểm tra điều này bằng cách:

var cow = new Cow();
cow.moo = () => {console.log("I want grass.")}

Một exception sẽ được raise lên: Attempted to assign to readonly property

Trước khi chuyển đến Decorator in classes, ta xem qua những thư viện về decorators, một trong những thư viện đó là core-decorators của Jay Phelps https://github.com/jayphelps/core-decorators.js.

Ngoài decorator @readonly ta đã thử định nghĩa ở trên, có những decorator khác như @deprecate, nó sẽ gọi console.warn() với một deprecation message được in ra khi hàm mà được decorate bởi @deprecate:

import {deprecate} from "core-decorators";

class Dog {
  @deprecate
  bag() {}

  say() {}
}

let dog = Dog.new();

dog.bag();
// DEPRECATION Dog#bag: This function will be removed in future versions.

dog.say();
// Working with no warning

Decorator với class

Trong trường hợp này, với mỗi đặc tả thì có một đối tượng được khởi tạo. Ví dụ, với class XMen, ta có thể định nghĩa một decorator @xmen:

function xmen(target) {
  target.isXMen = true;
}

@xmen
class Wolverine() {}

console.log(Wolverine.isXMen);
// true

Ta có thể mở rộng, bằng cách cho phép thêm các đối số cho hàm decorator:

function xmen(isXMen) {
  return function(target) {
    target.isXMen = isXMen;
  }
}

@xmen(true)
class Wolverine() {}
console.log(Wolverine.isXMen);
// true

@xmen(false)
class Wolverine() {}
console.log(Wolverine.isXMen);
// false

ES7 Decorators có thể chạy trên thuộc tính hoặc class. Chúng tự động lấy các các thuộc tính và đối tượng được truyền. Việc có quyền truy cập vào các mô tả của lớp hay thuộc tính cho phép decorator có thể thay đổi thuộc tính để sử dụng hàm getter.

Sử dụng Decorators thông qua Babel

Nếu sử dụng Babel CLI, ta có thể thêm Decorators theo cách:

$ babel --optional es7.decorators

Ngoài ra, ta có thể chuyển đổi sử dụng transformer:

babel.transform("code", {optional: ["es7.decorators"]});

Tóm tắt

ES7 decorators hữu ích cho việc định nghĩa decoration, kiểm tra kiểu. Về lâu dài, nó có thể được chứng minh rằng rất hữu ích cho việc phân tích thời gian compile, kiểm tra kiểu hay autocompletion.

Nó không khác nhiều so với mẫu decorators trong OOP cơ bản, nơi các mẫu cho phép đối tượng có thể được bổ sung thêm các hành vi mà không ảnh hưởng đến các đối tượng khác mà cùng class.

Nguồn tham khảo

All Rights Reserved