Hướng dẫn design pattern javascript es6 - mô hình thiết kế javascript es6

Khi bắt đầu một project mới, điều cần làm trước tiên thay vì lao ngay vào việc code là xác định mục đich và phạm vi của project, sau đó là liệt kê ra các tính năng hay spec. Và nếu project phức tạp thì việc nên làm tiếp theo là chọn design pattern phù hợp hơn là bắt đầu code. Nhắc lại một chút về design pattern

Design Pattern là gì?

Trong thiết kế phần mềm, design pattern là các phương pháp để giải quyết các vấn đề thường gặp mà có thể tái sử dụng được. Design pattern có thể coi là các best practice được các lập trình viên kinh nghiệm khuyên dùng hoặc mĩ miều hơn là các khuôn mẫu lập trình. Các framework, library mà chúng ta sử dụng hàng ngày cũng được xây dựng từ một tập hợp các design pattern.

Tại sao sử dụng Design Pattern?

Nhiều lập trình viên nghĩ design pattern là không cần thiết hoặc họ không biết cách để áp dụng chúng một cách phù hợp. Nhưng sử dụng một design pattern thích hợp có thể giúp bạn viết code đẹp hơn, dễ hiểu hơn, và tất nhiên sẽ dễ maintain hơn nhiều.

Quan trọng hơn, design pattern như là tiếng nói chung của các lập trình viên. Người khác có thể hiểu ngay được mục đích của bạn khi đọc code (design pattern) bạn viết.

Ví dụ như nếu bạn sử dụng decorator pattern trong project, người khác có thể hiểu ngay rằng đoạn code đó là để "trang trí", mở rộng chức năng cho các core feature. Vậy nen họ có thể tập trung hơn vào giải quyết các vấn đề nghiệp vụ thay vì cố gắng nghiền ngẫm xem đoạn code làm gì.

Bây giờ chúng ta đã biết design pattern là gì và sự quan trọng của chúng. Hãy cùng tìm hiểu sâu hơn về các pattern thường dùng trong JavaScript

Module Pattern

module.publicMethod(); // prints 'Hello World'
2 là một đoạn code độc lập mà chúng ta có thể chỉnh sửa mà không làm ảnh hưởng đến các phần khác của code.
module.publicMethod(); // prints 'Hello World'
2 cũng cho phép chúng ta tránh lạm dụng namespace bằng cách cho phép tạo các scope riêng biệt cho các biến. Chúng ta cũng có thể tái sử dụng
module.publicMethod(); // prints 'Hello World'
4 trong các project khác vì bản chất của
module.publicMethod(); // prints 'Hello World'
4 là tách biệt, không phụ thuộc vào các phần code khác.

Trong các ứng dụng JavaScript hiện nay,

module.publicMethod(); // prints 'Hello World'
2 là một phần không thể thiếu.
module.publicMethod(); // prints 'Hello World'
2 giúp code sạch hơn, tách biệt và có tổ chức hơn.

Không giống như những ngôn ngữ lập trình khác, JavaScript không có các phương thức để định nghĩa biến public, private (hay còn gọi là access modifier). Vậy nên

module.publicMethod(); // prints 'Hello World'
8 được sử dụng để giả lập tính chất đóng gói của hướng đối tượng.

Pattern này thường sử dụng IIFE (immediately-invoked function expression), closures và function scope để giả lập concept này. Ví dụ:

const myModule = (function() {
    const privateVariable = 'Hello World';
    
    function privateMethod() {
        console.log(privateVariable)
    }
    
    return {
        publicMethod: function() {
            privateMethod();
        }
    }
})();

myModule.publicMethod();

Bằng cách sử dụng IIFE, đoạn code trên được thực thi ngay lập tức, và trả về một object để gán vào biến

module.publicMethod(); // prints 'Hello World'
9. Nhờ có closure, object trả về vẫn có thể truy cập vào các hàm và biến được định nghĩa bên trong IIFE ngay cả khi IIFE đã thực thi xong.

Vậy nên các biến và hàm định nghĩa trong IIFE được giấu đi khỏi outer scope và từ đó trở nên private với biến

module.publicMethod(); // prints 'Hello World'
9

Sau khi thực thi xong, biến

module.publicMethod(); // prints 'Hello World'
9 sẽ có dạng như sau:

   const myModule = {
       publicMethod: function() {
           privateMethod();
       }
   };

Khi đó chúng ta có thể gọi publicMethod(), phương thức này sẽ gọi tới privateMethod()

module.publicMethod(); // prints 'Hello World'

Revealing Module Pattern

const my RevealingModule = (function() {
    let privateVar = 'Peter';
    const publicVar = 'Hello World';
    
    function privateFunction() {
        console.log('Name: ' + privateVar);
    }
    
    function publicSetName(name) {
        privateVar = name;
    }
    
    function publicGetName() {
        privateFunction();
    }
    
  /** reveal methods and variables by assigning them to object properties */
    return {
        setName: publicSetName,
        greeting: publicVar,
        getName: publicGetNAme
    };
})();

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
2 có thể coi là phiên bản cải tiến của Module Pattern. Vấn đề của
const my RevealingModule = (function() {
    let privateVar = 'Peter';
    const publicVar = 'Hello World';
    
    function privateFunction() {
        console.log('Name: ' + privateVar);
    }
    
    function publicSetName(name) {
        privateVar = name;
    }
    
    function publicGetName() {
        privateFunction();
    }
    
  /** reveal methods and variables by assigning them to object properties */
    return {
        setName: publicSetName,
        greeting: publicVar,
        getName: publicGetNAme
    };
})();

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
3 là chúng ta phải tạo các public function chỉ để gọi tới các private function và variable.

Trong pattern này, chúng ta map các thuộc tính của object trả về với các private function mà chúng ta muốn public. Đó cũng chính là lí do nó được gọi là

const my RevealingModule = (function() {
    let privateVar = 'Peter';
    const publicVar = 'Hello World';
    
    function privateFunction() {
        console.log('Name: ' + privateVar);
    }
    
    function publicSetName(name) {
        privateVar = name;
    }
    
    function publicGetName() {
        privateFunction();
    }
    
  /** reveal methods and variables by assigning them to object properties */
    return {
        setName: publicSetName,
        greeting: publicVar,
        getName: publicGetNAme
    };
})();

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
2. Ví dụ:

const my RevealingModule = (function() {
    let privateVar = 'Peter';
    const publicVar = 'Hello World';
    
    function privateFunction() {
        console.log('Name: ' + privateVar);
    }
    
    function publicSetName(name) {
        privateVar = name;
    }
    
    function publicGetName() {
        privateFunction();
    }
    
  /** reveal methods and variables by assigning them to object properties */
    return {
        setName: publicSetName,
        greeting: publicVar,
        getName: publicGetNAme
    };
})();

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();

Pattern này làm cho code dễ đọc hiểu hơn, sau khi thực thi

const my RevealingModule = (function() {
    let privateVar = 'Peter';
    const publicVar = 'Hello World';
    
    function privateFunction() {
        console.log('Name: ' + privateVar);
    }
    
    function publicSetName(name) {
        privateVar = name;
    }
    
    function publicGetName() {
        privateFunction();
    }
    
  /** reveal methods and variables by assigning them to object properties */
    return {
        setName: publicSetName,
        greeting: publicVar,
        getName: publicGetNAme
    };
})();

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
5 sẽ có dạng như sau:

const myRevealingModule = {
    setName: publicSetName.
    greeting: publicVar,
    getName: publicGetName
};

Chúng ta có thể gọi

const my RevealingModule = (function() {
    let privateVar = 'Peter';
    const publicVar = 'Hello World';
    
    function privateFunction() {
        console.log('Name: ' + privateVar);
    }
    
    function publicSetName(name) {
        privateVar = name;
    }
    
    function publicGetName() {
        privateFunction();
    }
    
  /** reveal methods and variables by assigning them to object properties */
    return {
        setName: publicSetName,
        greeting: publicVar,
        getName: publicGetNAme
    };
})();

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
6, tham chiếu tới hàm nội tại
const my RevealingModule = (function() {
    let privateVar = 'Peter';
    const publicVar = 'Hello World';
    
    function privateFunction() {
        console.log('Name: ' + privateVar);
    }
    
    function publicSetName(name) {
        privateVar = name;
    }
    
    function publicGetName() {
        privateFunction();
    }
    
  /** reveal methods and variables by assigning them to object properties */
    return {
        setName: publicSetName,
        greeting: publicVar,
        getName: publicGetNAme
    };
})();

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
7 và
const my RevealingModule = (function() {
    let privateVar = 'Peter';
    const publicVar = 'Hello World';
    
    function privateFunction() {
        console.log('Name: ' + privateVar);
    }
    
    function publicSetName(name) {
        privateVar = name;
    }
    
    function publicGetName() {
        privateFunction();
    }
    
  /** reveal methods and variables by assigning them to object properties */
    return {
        setName: publicSetName,
        greeting: publicVar,
        getName: publicGetNAme
    };
})();

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
8, tham chiếu tới hàm nội tại
const my RevealingModule = (function() {
    let privateVar = 'Peter';
    const publicVar = 'Hello World';
    
    function privateFunction() {
        console.log('Name: ' + privateVar);
    }
    
    function publicSetName(name) {
        privateVar = name;
    }
    
    function publicGetName() {
        privateFunction();
    }
    
  /** reveal methods and variables by assigning them to object properties */
    return {
        setName: publicSetName,
        greeting: publicVar,
        getName: publicGetNAme
    };
})();

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
9. Ví dụ:

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();

Ưu điểm vượt trội của

const my RevealingModule = (function() {
    let privateVar = 'Peter';
    const publicVar = 'Hello World';
    
    function privateFunction() {
        console.log('Name: ' + privateVar);
    }
    
    function publicSetName(name) {
        privateVar = name;
    }
    
    function publicGetName() {
        privateFunction();
    }
    
  /** reveal methods and variables by assigning them to object properties */
    return {
        setName: publicSetName,
        greeting: publicVar,
        getName: publicGetNAme
    };
})();

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
2 so với
module.publicMethod(); // prints 'Hello World'
8:

  • Chúng ta có thể thay đổi từ public sang private và ngược lại bằng cách thay đổi chỉ một dòng trong return statement
  • Object trả về không bao gồm các định nghĩa function, tất cả các right-hand side expression đều được định nghĩa bên trong IIFE, làm cho code sạch vầ dễ hiểu hơn.

ES6 Module

Trước sự xuất hiện của ES6, JavaScript không hề có feature tạo module, vậy nên các lập trình viên phải dựa vào các thư viện thứ ba hoặc

module.publicMethod(); // prints 'Hello World'
8 để implement module. Nhưng với ES6, mọi chuyện đã khác.

const myRevealingModule = {
    setName: publicSetName.
    greeting: publicVar,
    getName: publicGetName
};
3 được lưu các file riêng biệt. Chỉ duy nhất một module trong một file. Mọi thứ trong một module mặc định là private. Function, variable, và class được expose ra ngoài bằng cách sử dụng
const myRevealingModule = {
    setName: publicSetName.
    greeting: publicVar,
    getName: publicGetName
};
4 keyword. Và code trong một module luôn ở
const myRevealingModule = {
    setName: publicSetName.
    greeting: publicVar,
    getName: publicGetName
};
5.

Export module

Có 2 cách để export một khai báo function và variable:

  • Sử dụng keyword
    const myRevealingModule = {
        setName: publicSetName.
        greeting: publicVar,
        getName: publicGetName
    };
    
    4 trước khai báo function và variable. Ví dụ:
// utils.js

export const greeting = 'Hello World';

export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
  • Sử dụng
    const myRevealingModule = {
        setName: publicSetName.
        greeting: publicVar,
        getName: publicGetName
    };
    
    4 keyword ở cuối file kết hợp với tên function và variable muốn export. Ví dụ:
// utils.js
function multiply(num1, num2) {
  console.log('Multiply:', num1, num2);
  return num1 * num2;
}
function divide(num1, num2) {
  console.log('Divide:', num1, num2);
  return num1 / num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
export {multiply, divide};

Import Module

Tương tự như export một module, có hai cách để import một module bằng cách sử dụng

const myRevealingModule = {
    setName: publicSetName.
    greeting: publicVar,
    getName: publicGetName
};
8 keyword. Ví dụ:

  • Import nhiều item một lần
// main.js

// import nhiều item
import { sum, multiply } from './utils.js';

console.log(sum(3, 7));
console.log(multiply(3, 7));
  • Import cả một module
// main.js

// import cả module
import * as utils from './utils.js';

console.log(utils.sum(3, 7));
console.log(utils.multiply(3, 7));

Import và Export có thể được alias

Chức năng này được sinh ra để tránh các conflict trong naming. Ví dụ:

  • Alias export
   const myModule = {
       publicMethod: function() {
           privateMethod();
       }
   };
0
  • Alias import
   const myModule = {
       publicMethod: function() {
           privateMethod();
       }
   };
1

Singleton Pattern

const myRevealingModule = {
    setName: publicSetName.
    greeting: publicVar,
    getName: publicGetName
};
9 là một object chỉ khởi tạo duy nhất một lần, nghĩa là nó chỉ tạo một instance mới của một class nếu chưa tồn tại instance nào, còn nếu có thì nó chỉ việc trả lại instance đó. Nhờ vậy mà dù có gọi hàm khởi tạo nhiều lần thì chúng ta cũng chỉ nhận được một object duy nhất, giúp tránh lãng phí bộ nhớ.

JavaScript đã xây dựng sẵn

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
0 như là một tính năng, được gọi là
myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
1. Ví dụ:

   const myModule = {
       publicMethod: function() {
           privateMethod();
       }
   };
2

Bởi vì mỗi object trong JavaScript chiếm một vùng trong bộ nhớ và khi gọi tới object

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
2, chúng ta nhận được một tham chiếu tới nó. Nếu thử gán biến
myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
2 vào một biến khác và thay đổi biến đó. Ví dụ:

   const myModule = {
       publicMethod: function() {
           privateMethod();
       }
   };
3

Điều này làm thay đổi cả 2 object bởi vì JavaScript truyền tham chiếu chứ không phải truyền giá trị. Vậy nên vẫn chỉ có duy nhất một object trong bộ nhớ:

   const myModule = {
       publicMethod: function() {
           privateMethod();
       }
   };
4

Chúng ta cũng có thể implement

const myRevealingModule = {
    setName: publicSetName.
    greeting: publicVar,
    getName: publicGetName
};
9 bằng
myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
5. Ví dụ:

   const myModule = {
       publicMethod: function() {
           privateMethod();
       }
   };
5

Trong đoạn code trên, chúng ta tạo một instance mới bằng cách gọi hàm

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
6. Nếu một instance đã tồn tại, hàm này đơn giản chỉ trả về instance đó, nếu instance chưa tồn tại, nó tạo một instance mới bằng hàm
myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
7

Factory Pattern

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
8 là một pattern sử dụng phương thức đặc biệt để tạo các object mà không cần chỉ định rõ chính xác class hay constructor nào,

Pattern này có ích trong trường hợp chúng ta cần khởi tạo nhiều loại object phụ thuộc vào một số điều kiện nhất định. Ví dụ:

   const myModule = {
       publicMethod: function() {
           privateMethod();
       }
   };
6

Ở đây chúng ta tạo các class

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
9 và
// utils.js

export const greeting = 'Hello World';

export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
0 (với một vài giá trị mặc định) là nguyên mẫu cho các object. Sau đó định nghĩa thêm class
// utils.js

export const greeting = 'Hello World';

export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
1 có nhiệm vụ khởi tạo và trả về một trong hai object ở trên dựa vào thuộc tính
// utils.js

export const greeting = 'Hello World';

export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
2 trong
// utils.js

export const greeting = 'Hello World';

export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
3

   const myModule = {
       publicMethod: function() {
           privateMethod();
       }
   };
7

Tạo một object

// utils.js

export const greeting = 'Hello World';

export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
4 từ class
// utils.js

export const greeting = 'Hello World';

export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
1. Sau đó chúng ta tạo một object của
myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
9 hoặc
// utils.js

export const greeting = 'Hello World';

export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
0 bằng hàm
// utils.js

export const greeting = 'Hello World';

export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
8 và truyền object
// utils.js

export const greeting = 'Hello World';

export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
3 là tham số bao gồm thuộc tính
// utils.js

export const greeting = 'Hello World';

export function sum(num1, num2) {
  console.log('Sum:', num1, num2);
  return num1 + num2;
}
export function subtract(num1, num2) {
  console.log('Subtract:', num1, num2);
  return num1 - num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
2 là
// utils.js
function multiply(num1, num2) {
  console.log('Multiply:', num1, num2);
  return num1 * num2;
}
function divide(num1, num2) {
  console.log('Divide:', num1, num2);
  return num1 / num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
export {multiply, divide};
1 hoặc
// utils.js
function multiply(num1, num2) {
  console.log('Multiply:', num1, num2);
  return num1 * num2;
}
function divide(num1, num2) {
  console.log('Divide:', num1, num2);
  return num1 / num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
export {multiply, divide};
2.

Decorator Pattern

Decorator pattern được sử dụng để mở rộng chức năng của một object mà không làm thay đổi class hiện tại hay hàm tạo. Pattern này có thể được sử dụng để thêm feature mới vào object.

Một ví dụ đơn giản của pattern này:

   const myModule = {
       publicMethod: function() {
           privateMethod();
       }
   };
8

Một ví dụ thực tế khác:

Giả sử giá của xe phụ thuộc vào số tính năng nó có. Nếu không sử dụng decorator pattern, chúng ta sẽ phải tạo nhiều class khác nhau cho tượng trưng cho mỗi loại xe, mỗi class lại định nghĩa một cost method để tính giá trị:

   const myModule = {
       publicMethod: function() {
           privateMethod();
       }
   };
9

Nhưng với decorator pattern, chúng ta chỉ cần tạo một base class

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
9 và tính toán giá dựa vào decorator function. Ví dụ:

module.publicMethod(); // prints 'Hello World'
0

Class

myRevealingModule.setName('Mark');

// prints Name: Mark
myRevealingModule.getName();
9 để khởi tạo các object, sau đó được truyền vào decorator function để override hàm
// utils.js
function multiply(num1, num2) {
  console.log('Multiply:', num1, num2);
  return num1 * num2;
}
function divide(num1, num2) {
  console.log('Divide:', num1, num2);
  return num1 / num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
export {multiply, divide};
5 tính giá mới và thêm các thuộc tính xác định tính năng được thêm vào cho
// utils.js
function multiply(num1, num2) {
  console.log('Multiply:', num1, num2);
  return num1 * num2;
}
function divide(num1, num2) {
  console.log('Divide:', num1, num2);
  return num1 / num2;
}
// This is a private function
function privateLog() {
  console.log('Private Function');
}
export {multiply, divide};
1 instance. Khi sử dụng:

module.publicMethod(); // prints 'Hello World'
1

Kết luận

Chúng ta đã tìm hiểu qua một vài design pattern sử dụng trong JavaScript, còn nhiều pattern khác có thể áp dụng được nhưng tôi không đề cập trong bài viết. Tuy nhiên thì việc lạm dụng quá nhiều design pattern cũng là không nên, hãy cân nhắc trước khi áp dụng để tìm được phương án phù hợp nhất. Hi vọng bài viết sẽ giúp ích cho các bạn.

Tham khảo

https://blog.bitsrc.io/understanding-design-patterns-in-javascript-13345223f2dd