Decorator pattern là một mẫu thiết kế cấu trúc cho phép mở rộng chức năng của một thứ mà không thay đổi mã nguồn của nó.
Decorator Pattern sử dụng sự kết hợp (composition) để đạt được mục tiêu này thay vì sử dụng kế thừa để mở rộng chức năng.
Mở rộng chức năng động: cho phép mở rộng chức năng của đối tượng mà không cần thay đổi mã nguồn của lớp đó.
Tuân thủ nguyên tắc mở/đóng (Open/Closed Principle): Đối tượng có thể mở rộng chức năng của mình mà không thay đổi mã nguồn ban đầu.
Hợp nhất nhiều mẫu: Có nhiều mẫu khác nhau có thể được kết hợp để thêm nhiều tính năng khác nhau vào đối tượng.
Tính linh hoạt cao: có thể được thêm, thay đổi hoặc loại bỏ một cách dễ dàng mà không ảnh hưởng đến các đối tượng khác.
// Đối tượng cơ bản class Coffee { cost() { return 5; } } // Decorator cơ bản class MilkDecorator { constructor(coffee) { this.coffee = coffee; } cost() { return this.coffee.cost() + 2; } } class SugarDecorator { constructor(coffee) { this.coffee = coffee; } cost() { return this.coffee.cost() + 1; } } // Sử dụng Decorator let myCoffee = new Coffee(); console.log("Chi phí ban đầu: " + myCoffee.cost() + " VND"); // 5 VND myCoffee = new MilkDecorator(myCoffee); console.log("Chi phí sau khi thêm sữa: " + myCoffee.cost() + " VND"); // 7 VND myCoffee = new SugarDecorator(myCoffee); console.log("Chi phí sau khi thêm đường: " + myCoffee.cost() + " VND"); // 8 VND
Đối tượng cơ bản —Coffee— cơ bản cung cấp phương thức chi phí trả về giá cơ bản của cà phê.
Decorator (Milk và Sugar): Các lớp trang trí này bổ sung chức năng cho lớp cơ bản.
Mỗi lớp này đều bao gồm một đối tượng của lớp cơ bản và ghi đè phương thức chi phí để thêm chi phí cho thành phần mới được tạo ra.
Sử dụng trang trí:
Đầu tiên, tạo một đối tượng Coffee.
Sau đó, để tăng thêm sữa, bọc nó bằng MilkDecorator.
Cuối cùng, để tiết kiệm chi phí đường, bọc nó bằng SugarDecorator.
Trong JavaScript, đặc biệt là trong TypeScript, bạn có thể áp dụng bằng các bằng cú pháp @decorator.
Đây là một số ví dụ sử dụng cú pháp @Decorator:
Lưu ý một vài trình duyệt không hỗ trợ nên phải dùng cứu pháp cũ
Ví dụ 1: Logging
function log(target, name, descriptor) { const original = descriptor.value; descriptor.value = function(...args) { console.log(`Calling ${name} with`, args); let result = original.apply(this, args); console.log(`Result: ${result}`); return result; }; return descriptor; } class MathOperations { @log add(a, b) { return a + b; } } const math = new MathOperations(); math.add(2, 3); // Logs: Calling add with [2, 3] and Result: 5
Ví dụ 2: Deprecated
function deprecated(target, name, descriptor) { const original = descriptor.value; descriptor.value = function(...args) { console.warn(`${name} is deprecated`); return original.apply(this, args); }; return descriptor; } class Example { @deprecated oldMethod() { console.log('This is an old method'); } } const example = new Example(); example.oldMethod(); // Logs: oldMethod is deprecated and This is an old method
Ví dụ 3: Timing
function timing(target, name, descriptor) { const original = descriptor.value; descriptor.value = function(...args) { console.time(name); const result = original.apply(this, args); console.timeEnd(name); return result; }; return descriptor; } class Processor { @timing process() { for (let i = 0; i < 1000000; i++) {} // some time-consuming task return 'Done'; } } const processor = new Processor(); processor.process(); // Logs the time taken to execute the process method
Ví dụ 4: Read-Only
function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor; } class Car { @readonly brand() { return 'Toyota'; } } const myCar = new Car(); myCar.brand = function() { return 'Honda'; }; // Throws an error console.log(myCar.brand()); // 'Toyota'
Ví dụ 5: Validate Input
function validate(target, name, descriptor) { const original = descriptor.value; descriptor.value = function(...args) { if (args.some(arg => arg < 0)) { throw new Error('Negative values are not allowed'); } return original.apply(this, args); }; return descriptor; } class Calculator { @validate multiply(a, b) { return a * b; } } const calculator = new Calculator(); console.log(calculator.multiply(2, 3)); // 6 // calculator.multiply(-1, 2); // Throws an error
Ví dụ 6: Autobind
function autobind(target, name, descriptor) { const original = descriptor.value; descriptor.value = function(...args) { return original.apply(this, args); }; return { configurable: true, get() { const boundFn = original.bind(this); Object.defineProperty(this, name, { value: boundFn, configurable: true, writable: true }); return boundFn; } }; } class Person { constructor(name) { this.name = name; } @autobind greet() { console.log(`Hello, my name is ${this.name}`); } } const john = new Person('John'); const greet = john.greet; greet(); // 'Hello, my name is John'
Ví dụ 7: Singleton
function singleton(target) { let instance; const newConstructor = function(...args) { if (!instance) { instance = new target(...args); } return instance; }; newConstructor.prototype = target.prototype; return newConstructor; } @singleton class Database { constructor() { console.log('New Database instance created'); } connect() { console.log('Connected to the database'); } } const db1 = new Database(); const db2 = new Database(); console.log(db1 === db2); // true
Ví dụ 8: Retry
function retry(retries) { return function(target, name, descriptor) { const original = descriptor.value; descriptor.value = async function(...args) { for (let i = 0; i < retries; i++) { try { return await original.apply(this, args); } catch (e) { console.warn(`Retrying ${name}... (${i + 1})`); } } throw new Error(`${name} failed after ${retries} retries`); }; return descriptor; }; } class Network { @retry(3) async fetchData() { // Simulate network request if (Math.random() > 0.7) { return 'Data fetched successfully'; } else { throw new Error('Network error'); } } } const network = new Network(); network.fetchData().then(console.log).catch(console.error);
Ví dụ 9: Memoize
function memoize(target, name, descriptor) { const original = descriptor.value; const cache = new Map(); descriptor.value = function(...args) { const key = JSON.stringify(args); if (!cache.has(key)) { const result = original.apply(this, args); cache.set(key, result); } return cache.get(key); }; return descriptor; } class Fibonacci { @memoize calculate(n) { if (n <= 1) { return n; } return this.calculate(n - 1) + this.calculate(n - 2); } } const fib = new Fibonacci(); console.log(fib.calculate(40)); // Computes quickly due to memoization
Ví dụ 10: Throttle
function throttle(limit) { let inThrottle; return function(target, name, descriptor) { const original = descriptor.value; descriptor.value = function(...args) { if (!inThrottle) { original.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; return descriptor; }; } class Events { @throttle(2000) onScroll() { console.log('Scroll event triggered'); } } const events = new Events(); window.addEventListener('scroll', () => events.onScroll());
Các ví dụ trên minh họa cách sử dụng cú pháp @decorator để thêm nhiều tính năng vào các phương thức JavaScript.
Bằng cách tách biệt các thành phần chức năng khác nhau, Decorator giúp mã nguồn trở nên ngắn gọn và dễ hiểu hơn.
// Middleware decorator function middleware(target, key, descriptor) { const originalMethod = descriptor.value; descriptor.value = async function(...args) { // Code để xác thực và kiểm tra quyền truy cập console.log("Middleware executed"); return await originalMethod.apply(this, args); }; return descriptor; } class UserController { @middleware @authenticate getUser(userId: string) { console.log(`Retrieving user with id ${userId}`); // Code để lấy thông tin người dùng từ cơ sở dữ liệu return { id: userId, name: "John Doe" }; } } // Authentication decorator function authenticate(target, key, descriptor) { const originalMethod = descriptor.value; descriptor.value = async function(...args) { // Code để xác thực người dùng console.log("Authentication executed"); return await originalMethod.apply(this, args); }; return descriptor; } const userController = new UserController(); userController.getUser("123"); // Logs: Authentication executed, Middleware executed, Retrieving user with id 123
Sử dụng cho phương thức getUser: @middleware và @authenticate.
Các decorator sẽ được thực thi theo thứ tự được đặt ra trong mã nguồn khi phương thức getUser được gọi.
Decorator @authenticate được thực thi trước.
Sau đó, decorator @middleware được thực thi sau.
Mỗi trang trí thêm một lớp logic trước hoặc sau khi phương thức chính thức được gọi.
Trong trường hợp này, nó là lớp xác thực và kiểm tra quyền truy cập.
Lưu ý một vài trình duyệt “không hỗ trợ“
Ba tham số được gửi đến Log:
Target: đề cập đến lớp chứa phương thức trang trí.
Key: Tên của phương thức.
Descriptor: Một đối tượng mô tả thuộc tính hoặc phương thức trang trí của một đối tượng.
Trình biên dịch sẽ gọi hàm trang trí phù hợp khi chương trình chạy và gặp cú pháp @decoratorName.
Các tham số của phần tử được trang trí, chẳng hạn như lớp, phương thức hoặc thuộc tính, sẽ được hàm trang trí thu thập và sử dụng thông tin này để tạo ra bất kỳ logic nào.
Hàm trang trí có khả năng thay đổi hoặc mở rộng phần tử mà nó trang trí bằng cách thay đổi descriptor.
Điều này có thể bao gồm thêm logic vào phương thức, thay đổi cách hoạt động của lớp, v.
v.
Cuối cùng, hàm trả về một đối tượng mới chứa các sửa đổi hoặc descriptor sửa đổi.
Đây là một ví dụ khác về cách sử dụng @decorator trong TypeScript để giúp xác thực trong một ứng dụng web trở nên đơn giản hơn.
Giả sử bạn đang phát triển một ứng dụng web và muốn người dùng được xác thực trước khi họ có thể truy cập vào một controller cụ thể.
Để thực hiện điều này, bạn có thể sử dụng decorator.
// Middleware decorator function authenticate(target, key, descriptor) { const originalMethod = descriptor.value; descriptor.value = function(...args) { // Kiểm tra xem người dùng đã đăng nhập chưa if (!isLoggedIn()) { throw new Error("Bạn cần đăng nhập để truy cập vào phương thức này."); } return originalMethod.apply(this, args); }; return descriptor; } // Định nghĩa một đối tượng đại diện cho việc xác thực function isLoggedIn() { // Trả về true nếu người dùng đã đăng nhập, ngược lại trả về false return true; // Giả sử người dùng đã đăng nhập } class UserController { @authenticate getUser(userId: string) { // Code để lấy thông tin người dùng từ cơ sở dữ liệu return { id: userId, name: "John Doe" }; } } const userController = new UserController(); console.log(userController.getUser("123")); // Nếu đăng nhập thành công, sẽ lấy thông tin người dùng. Ngược lại, sẽ ném ra một lỗi.
Một decorator được sử dụng cho phương thức getUser là @authenticate.
Decorator @authenticate sẽ được thực thi trước khi gọi phương thức getUser.
Decorator @authenticate xác minh liệu người dùng đã đăng nhập hay không.
Nếu không, nó sẽ xác định một sai lầm.
Trong trường hợp người dùng đã đăng nhập, phương thức getUser sẽ được sử dụng thông thường để thu thập thông tin người dùng.
Nếu không, nó sẽ không được hoàn thành và một lỗi sẽ được xử lý.
#Mtips5s #Contact
Fanpage: https://www.facebook.com/mtipscoder
Group trao đổi, chia sẻ: https://www.facebook.com/groups/mtipscoder
Website: https://mtips5s.com
Youtube: https://mtips5s.com
Twitter(X): @takagiks99
Instagram: @khuongkara
Threads: @khuongkara
Google Maps: @khuongkara
#Base Code #Souce Code
Npm: @tools.mtips5s.com
Bộ công cụ My Self: @github
Npm: @npm
Docker: @docker
Chúc các bạn thành công!
Leave A Comment