I'm Anh, from 🇻🇳 and be a 👨💻, like to build a system by DDD.
Currently have interested in System Design 👷 🏗️ and Database.
Learn DesignPattern
I'm Anh, from 🇻🇳 and be a 👨💻, like to build a system by DDD.
Currently have interested in System Design 👷 🏗️ and Database.
Singleton Pattern cho phép bạn có thể chắc chắn rằng class chỉ có duy nhất một instance song song với đó, nó sẽ cung cấp glbal access point đến instance đó.
Đây là cách nó hoạt động: hãy tưởng tượng bạn vừa tạo một object nhưng sau đó, khi bạn quyết định tạo thêm một object khác nữa, thay vì nhận về một object mới tinh bạn chỉ nhận về được object mà bạn đã có trước đó.
Chú ý rằng điều này là không thể với các constructor
thông thường vì các constructor
thông thường sẽ luôn trả về một object mới tinh cho chúng ta
Cũng tương tự như các biến toàn cục, Singleton pattern cho phép chúng ta có thể truy cập đến các objects ở bất cứ đâu trong chương trình. Tuy nhiên nó cũng bảo vệ các objects đó khỏi việc bị ghi đè bởi các đoạn code khác.
Có một vấn đề khác mà chúng ta cần quan tâm ở đây đó là việc chúng ta không hề muốn các đoạn code giải quyết vấn đề thứ nhất (Đảm bảo class chỉ có duy nhất một instance) nằm rải rác ở nhiều nơi trong chương trình của chúng ta, tốt hơn hết là giữ nó ở trong một class duy nhất (nhất là khi code của bạn phụ thuộc nhiều vào nó)
Có 2 bước
constructor
trở thành private để tránh việc sử dụng new
operator ở nhiều chỗ khác nhaustatic creation method
hoạt động với vai trò như constructor nhưng thực chất là nó sẽ gọi tới private constructor
đã có trước đó, lưu instance mới tạo vào một static field
. Những lần gọi hàm creation sau đó đều trả về cached object.Chính phủ của một đất nước là một ví dụ điển hình về Singleton pattern
. Mỗi quốc gia chỉ có một chính phủ duy nhất. Tên gọi "Chính phủ nước ..." giống như một con trỏ, trỏ tới một nhóm người nhất định đang làm việc cho chính phủ của một quốc gia.
Singleton class
với static method là getInstance
sẽ tạo mới instance nếu nó chưa tồn tại còn nếu nó đã tồn tại thì sẽ luôn trả về instance hiện có chứ không hề tạo mới instance.
Singleton Pattern
khi class chỉ nên có duy nhất một instance được sử dụng bởi mọi clients (VD: DB object)Singleton Pattern
khi bạn muốn kiểm soát nghiêm ngặt các biến global.Đáng kể nhất đó là khi viết unit test cho Singleton vì ta cần tạo mock object
. Do constructor
của Singleton là private cũng như ta không thể ghi đè được static method (với nhiều ngôn ngữ hiện có) thì việc tạo mock object
cần phải áp dụng một cách làm mới. Hoặc nếu không bạn có thể không viết test hoặc không dùng Singleton Pattern.
Adapter Pattern cho phép các objects với interface khác nhau có thể hoạt động với nhau.
Hãy tưởng tượng bạn đang phát triển một app theo dõi giá cổ phiếu. App này download dữ liệu cổ phiếu từ nhiều nguồn với XML format, hiển thị nó dưới dạng đồ thị. Thế nhưng khi bạn muốn tích hợp thêm một thư viện analytics khác vào thì thư viện này lại sử dụng JSON format.
Bạn hoàn toàn có thể sửa thư viện để nó có thể làm việc với XML format, tuy nhiên điều này sẽ làm ảnh hưởng đến code hiện có của thư viện cũng như có những trường hợp bạn không thể truy cập vào source code của thư viện nên cách tiếp cận này là không thể.
Bạn có thể tạo ra một adapter
. Đây là một object đặc biệt, nó convert interface của một object sang dạng mà object khác có thể hiểu được.
Một Adapter có thể wrap lấy một object để che dấu đi sự chuyển đổi kiểu dữ liệu phức tạp ở phía sau. Ví dụ bạn có thể wrap một object làm việc với đơn vị (m, km) để nó có thể xử lí với các đơn vị như (feet hoặc miles). Các object được wrap lại này không nhất thiết phải biết về adapter.
Adapter không những đáp ứng được việc chuyển đổi format dữ liệu mà nó còn giúp các objects với interface khác nhau có thể làm việc được với nhau. Cơ chế như sau:
Có những trường hợp ta hoàn toàn có thể tạo ra two-way adapter có thể convert lời gọi theo 2 chiều.
Dưới đây là mô hình cho app tracking stock ở phía trên.
Object adapter
Adapter sẽ implement interface của một object và wrap các objects còn lại.
Client
là class bao hàm business logicClient Interface
mô tả protocol mà các classes khác phải tuân thủ khi làm việc với Client code.Service
thường là các thư viện bên ngoài, hoặc các code đã có từ trước. Client không thể dùng nó trực tiếp do interface bị xung đột.Adapter
là class có khả năng làm việc với cả client và service. Nó triển khai client interface vào wrap service object. Adapter nhận lời gọi từ phía client, chuyển chúng thành các lời gọi theo format mà service object có thể hiểu được.Class adapter
Cách làm này cho phép Adapter class kế thừa trực tiếp từ Client Class và Service Class (điều này chỉ được phép với C++)
Class Adapter
không cần phải wrap bất kì object nào vì nó kế thừa từ client và service. Quá trình adaptation sẽ diễn ra trong method được ghi đè.Nó cho phép ta có thể tạo ra các middle-layer
class làm việc như một translator giữa code hiện tại và các code sẵn có cũng như các thư việc từ bên ngoài hoặc các class với interface kì cục khác.
Observer pattern is used when there is one-to-many relationship between objects such as if one object is modified, its dependent objects are to be notified automatically
Ref: https://www.tutorialspoint.com/design_pattern/observer_pattern.htm
Cho phép chúng ta có thể tạo ra các objects phức tạp theo từng bước một. Pattern cũng cho phép chúng ta có thể tạo các kiểu objects khác nhau sử dụng cùng một source code.
Hãy tưởng tượng việc chúng ta phải viết các constructor code dài ngoằng, với nhiều dòng code và tham số. Đây là một điều vô cùng tồi tệ khi bạn phải maintain mớ bòng bong đó.
Ví dụ thực tế là khi xây nhà. Bạn cần
Nhưng nếu bạn muốn ngôi nhà của mình to hơn, sáng hơn với sân sau hoặc hệ thống sưởi cho mùa đông ?
Giải pháp đơn giản nhất đó là kế thừa từ House
class, sau đó tạo các subclasses để giải quyết hết các nhu cầu trên. Tuy nhiên điều mà bạn nhận lại được đó là một đống các subclasses đi kèm. Cứ khi nào có parameter mới là một lần bạn phải tạo thêm một subclass mới nữa.
Một giải pháp khác nữa đó là tạo một constructor khổng lồ, đáp ứng mọi parameters cho House
base class. Cách làm này giúp giảm đi đáng kể số lượng các subclasses, nhưng nó lại tạo ra thêm các vấn đề khác đi kèm.
Đa phần trong mọi trường hợp, chúng ta sẽ không sử dụng mọi parameters. Điều này vô tình dẫn đến việc các lời gọi hàm trông sẽ rất xấu.
Tách construction code ra khỏi class và đưa nó đến các objects khác gọi là các builders
.
Pattern này sẽ tổ chức quá trình tạo object thành các bước (buildWalls
, buildDoor
, ...). Để tạo ra object bạn chỉ cần gọi đến các hàm mà mình cần (tương ứng với đặc tả object mà bạn mong muốn).
Có những lúc các bước xây dựng object sẽ được implement theo những cách khác nhau (với cabin thì buildWalls
sẽ sử dụng gỗ, còn với nhà thông thường buildWalls
sẽ sử dụng gạch).
Trong những trường hợp như vậy bạn có thể tạo các classes khác nhau để triển khai tập hợp các builder methods
theo những cách khác nhau. Khi đó quá trình tạo một object sẽ là tập hợp lời gọi các builder methods
.
Director Class
định nghĩa các bước để thực thi building steps, trong khi builder sẽ cung cấp implementation cho các bước này.
Phía client không nhất thiết phải sử dụng Director Class
mà hoàn toàn có thể gọi các builder methods theo trình tự mà mình mong muốn.
Tuy nhiên Director Class
có thể là nơi đặt các construction routines để bạn có thể tái sử dụng chúng. Ngoài ra thì Director Class
cũng giúp chúng ta có thể che dấu đi các bước tạo ra object, client chỉ đơn thuần gọi đến Director Class
và lấy về object mong muốn.
Builder
vào cho Director
Các high level class không nên phụ thuộc vào low level class. Cả hai chỉ nên phụ thuộc vào các yếu tố trừu tượng (abstraction). Abstractions không nên phụ thuộc vào detail. Details nên phụ thuộc vào Abstractions.
Khi thiết kế phần mềm, ta thường chia các classes thành 2 nhóm như dưới đây:
Thường thì chúng ta sẽ bắt đầu với các low-level classes rồi mới đi lên high-level classes. Các tiếp cận này thường diễn ra khi chúng ta bắt đầu triển khai prototype cho project và lúc đó chúng ta cũng chưa chắc chắn rằng các high-level classes sẽ có những logic gì ở bên trong do low level class chưa được triển khai.
Với cách tiếp cận như thế này thì business logic sẽ bị phụ thuộc vào các primitive low-level classes.
Dependency Inversion principle đưa ra các tiếp cận ngược lại so với cách tiếp cận ở phía trên
openReport(file)
thay vì một chuỗi các methods openFile(x)
, readBytes(n)
, closeFile(x)
. Các interfaces này được coi như thuộc về high-level.Dependency inversion thường đi kèm với open/closed principle. Khi đó ta có thể extend low-level classes để sử dụng cho các business logic khác thay vì sửa trực tiếp các classes hiện có.
Ta thấy rằng BudgetReport
class sẽ phụ thuộc vào low-level classes (loại DB nào).
Để giải quyết vần đề này ta sẽ tạo một high-level interface để high-level class sử dụng nó. Khi đó các low-level classes có thể có nhiều cách triển khai khác nhau tuỳ theo business logic (liên quan đến chuyện lưu trữ hay lấy về dữ liệu)
Lúc này ta thấy low-level classes sẽ phụ thuộc vào high-level classes
.
Client không nên bị ép sao cho phụ thuộc vào các methods mà client không sử dụng
Hãy làm cho interfaces của bạn nhỏ nhất, hẹp nhất có thể để client không nhất thiết phải implement những methods mà nó không sử dụng.
Hãy chia nhỏ "fat interface" thành các interfaces nhỏ nhất và cụ thể nhất có thể
Client chỉ cần implement những methods mà client thực sự cần.
VD: Hãy thử tưởng tượng ban đang làm một thư viện giúp tích hợp apps với nhiều loại cloud computing providers. Ban đầu bạn chỉ triển khai với AWS, tuy nhiên khi số lượng cloud providers tăng lên sẽ xảy ra TH một số interface của loại cloud này không có các methods mà loại cloud kia triển khai.
Cách giải quyết ở đây vẫn là chia nhỏ interface ra thành nhiều interfaces khác nhau, mỗi "interface con" này sẽ chỉ chứa những method thực sự liên quan đến chức năng của nó.
Tuy nhiên khi interface đã đạt được một mức độ cụ thể nhất định, ta cũng không nhất thiết phải chia nhỏ nó ra thêm nữa vì càng nhiều interfaces thì code sẽ càng trở nên phức tạp hơn. Hãy giữ sự cân bằng.
Cho phép ta có thể tạo một tập các objects liên quan đến nhau mà không cần phải thông qua các classes cụ thể nào.
Giả sử bạn đang dev một shop simulator về đồ nội thất, code của bạn gồm các classes:
Chair
+ Sofa
+ Coffee Table
Modern
, Victorian
, ArtDeco
Bạn muốn bán cho khách hàng đúng loại đồ nội thất mà họ muốn, đồng thời đáp ứng kịp thời cho sự thay đổi catalog của nhà cung cấp mà không cần phải thay đổi core code.
Abstract Factory sẽ xử lí vấn đề bằng cách tạo ra các interfaces cho các đồ dùng (VD: Chair
, Sofa
, Coffee Table
). Sau đó các kiểu dáng sẽ là các class implements các interfaces trên.
Tiếp theo đó là tạo Abstract Factory
- bản chất là một interface với các methods tạo ra các đồ nội thất Chair
, Sofa
, ... tương ứng là createChair
, createSofa
. Các methods này sẽ trả về các abstract
product type mà ta đã định nghĩa thông qua các interface Chair
, Sofa
, ...
Với các kiểu dáng, ta sẽ tạo ra các Factory
class implement Abstract Factory
ở trên, với các methods sẽ trả về (ModernChair
, ModernSofa
tương ứng với createChair()
và createSofa()
).
Do phía client không quan tâm đến class cụ thể của factory, nên ta có thể truyền xuống cho client code (kiểu dáng cũng như factory type).
Phía client sẽ xử lí các loại Chair
như VictorianChair
hoặc ModernChair
như nhau vì đều là Chair
(abstract interface). Với cách tiếp cận này thì client chỉ có thể biết về method sitOn()
của Chair
mà thôi. Hơn nữa Sofa
và CoffeeTable
được trả về sẽ luôn đồng loại với Chair
.
Còn một vấn đề nữa đó là khi client chỉ sử dụng interface của factory thôi thì sao ? Khi đó factory object sẽ được khởi tạo từ ban đầu, bản thân factory type sẽ được lấy từ các biến môi trường hoặc các biến config.
abstract products
nhóm theo các kiểu dáng. Mỗi abstract product phải được triển khai toàn bộ các kiểu dáng Modern
, ArtDeco
, ...Abstract Product
.Abstract Factory
, với các methods tạo ra các products thuộc cùng một kiểu dáng.Concrete Factory
tạo ra các Concrete Product
xong chữ kí các hàm creation của nó sẽ phải trả về Abstract Product
Một class chỉ nên có duy nhất một lí do để thay đổi.
Một class chỉ nên đảm nhận một chức năng duy nhất. Tránh việc tạo ra một class với nhiều chức năng trong nó. Khi đó việc thay đổi class để đáp ứng với requirement sẽ làm ảnh hưởng đến những phần khác của class mà ta không chủ tâm muốn thay đổi chúng.
Bạn không nhất thiết phải áp dụng một design hoàn hảo cho 200 dòng codes. Chỉ cần chia hàm là ổn.
VD:
Chúng ta hoàn toàn có đủ lí do để chia class Employee
dưới đây thành các subclasses.
Bản chất của việc sử dụng class Employee
là để quản lí nhân viên, nhưng report của nhân viên sẽ thay đổi theo thời gian. Nên ta sẽ tách class Employee thành 2 class: Employee
và TimeSheetReport
.
Lấy ví dụ với hàm sau:
const randomInRange = (min: number, max: number): number => {
return Math.random() * (max - min) + min;
}
Hàm này rõ ràng phụ thuộc vào hàm thư viện Math.random()
, nếu trường hợp hàm này không hoạt động thì hàm randomInRange
cũng sẽ không hoạt động theo. Do đó Math.random()
là một dependency.
Sửa lại hàm một chút
const randomInRange = (min: number, max: number, random: () => number = Math.random) => {
return random() * (max - min) + min;
}
Ta thấy rằng hàm randomInRange
không còn phụ thuộc vào Math.random
nữa, ngoài ra ta cũng có thể tuỳ ý sử dụng bất kì một hàm random
có sẵn tuỳ ý muốn. Đây chính là cách triển khai nguyên thuỷ nhất của dependency inversion
Ta sẽ truyền vào module mọi dependencies mà nó cần
Có 2 lí do chính:
getRandomInRange
ở phía trên ta không thể test được nếu không mock do nó là hàm random
. Code test chông sẽ như sau:const randomMock = () => return 0.1;
const res = getRandomInRange(1, 2, randomMock);
assert.equal(res, 1.1);
const otherRandom = (): number => {
// implementation
}
const getRandomInRange = (min: number, max: number, otherRandom): number => {
// ...
}
Khi định nghĩa các behaviour trong thực tế, ta sẽ đi theo flow sau:
class Counter {
public state: number = 0;
public increase(): void {
this.state += 1;
console.log(`State increased. Current state is ${this.state}.`);
}
public decrease(): void {
this.state -= 1;
console.log(`State decreased. Current state is ${this.state}.`);
}
}
Ở đoạn code trên ta thấy class phụ thuộc vào dependency là console
. Nếu ta:
thì vấn đề phụ thuộc vào dependency sẽ được giải quyết
interface Logger {
log: (message: string) => void
}
class Counter {
constructor(
private logger: Logger,
) {}
public state: number = 0;
public increase(): void {
this.state += 1;
this.logger.log(`State increased. Current state is ${this.state}.`);
}
public decrease(): void {
this.state -= 1;
this.logger.log(`State decreased. Current state is ${this.state}.`);
}
}
const instance = new Counter(console);
Ngoài console
ta cũng có thể truyền các dependencies khác miễn sao nó implement Logger interface
là được.
const alertLogger: Logger = {
log: (message: string): void => {
alert(message);
},
};
const counter = new Counter(alertLogger);
Việc inject các dependencies vào các modules đang được thực hiện bằng tay. Tuy nhiên ta có thể triển khai tự động
bằng DI Containers.
Về mặt định nghĩa DI Containers có chức năng cung cấp (provide) các dependencies cho các modules.
SINGLETON
scope: Sau khi instance được tạo ra, nó sẽ được lưu vào cache.
Do mọi thứ sẽ được container đảm nhận nên ta không cần thiết phải truyền dependencies bằng tay.
Phía dưới là DI của NestJS
Reference from this
Giả sử bạn có một class Notifier
với method send
, method này chỉ gửi message đi mà thôi, thế nhưng bạn còn muốn nhiều hơn thế, thay vì chỉ gửi message bạn còn muốn gửi
Một cách giải quyết đơn giản đó là extends
từ class Notifier
thành các classes:
Thế nhưng cách làm này có một nhược điểm có thể thấy rõ đó là việc nó sẽ làm tăng số lượng classes lên một cách "đáng kể".
Một cách giải quyết ở đây đó là Aggregation hoặc Composition, bản thân 1 object sẽ kết tập từ nhiều objects thuộc về các classes khác, từ đó "chuyển tiếp - delegate" công việc cho các classes kia.
Có thể mô tả pattern này trong 1 từ đó là "Wrapper", khái niệm wrapper ở đây sẽ được hiểu như việc một object có thể "link" tới các target objects
khác. Wrapper thường delegate các requests mà nó nhận được (trong thực tế thì wrapper có thể chỉnh sửa kết quả hoặc param sau hoặc trước khi delegate cho các target objects).
Bản thân các wrapper cũng sẽ implements các interface giống như các objects được "bao" bên trong wrapper, từ phương diện của client thì các objects này là như nhau.
Các decorator sẽ bao lấy nhau thành 1 stack, stack cuối cùng sẽ được sử dụng bởi client. Do các decorator đều implement 1 interface nên client code sẽ không quan tâm tới việc đó là "pure" object hay là một decorator.
Là creational pattern
cho phép ta có thể copy object mà không cần phải phụ thuộc vào class của nó.
Bạn muốn copy một object, điều đầu tiên cần làm đó là tạo một instance mới của class, sau đò duyệt qua toàn bộ các giá trị của các fields thuộc về object hiện có và gán giá trị đó cho object mới tạo. Cách làm này không sai nhưng không thể áp dụng khi object có những private fields
.
Ngoài ra còn có một vấn đề khác đó là bạn cần phải biết class tương ứng với object vừa tạo, điều này khiến code của bạn sẽ phụ thuộc vào một class. Hơn nữa có thể bạn chỉ biết mỗi interface của object chứ không biết được class cụ thể của nó.
Prototype Pattern
sẽ uỷ thác việc clone object cho chính object được clone. Cụ thể là bạn có thể định nghĩa một interface chung cho các object mà bạn muốn clone. Đơn giản chỉ là định nghĩa interface
với method clone
.
Việc triển khai clone
method là hoàn toàn giống nhau ở các class. Cụ thể là tạo một object mới, sao lưu toàn bộ giá trị của các fields vào object mới. Bản thân mọi ngôn ngữ lập trình cũng cho phép các object có thể truy cập đến các private field của object khác nên ta hoàn toàn có thể sao lưu các giá trị private
một cách dễ dàng.
Một object hỗ trợ cloning sẽ được gọi là prototype
.
Khi object của bạn có nhiều fields cũng như các config khác thì việc clone chúng có thể được triển khai ở subclass.
(1) Định nghĩa Prototype interface
với clone
method
(2) ConcretePrototype class sẽ implement Prototype
interface, ngoài việc copy object gốc, clone
method có thể có các xử lí khác nếu object cần clone là nested object.
Class nên được mở rộng thay vì được sửa đổi trực tiếp
Mục đích chính của nguyên tắc này đó là giữ cho code được toàn vẹn khi thêm tính năng mới.
Một class được gọi là mở
nếu bạn có thể dễ dàng kế thừa, tạo sub-class, thêm các methods hoặc fields mới.
Khi class không thể mở rộng được nữa (có thể được sử dụng bởi các classes khác, interface của nó đã hoàn thiện và sẽ không thay đổi trong tương lai) thì class sẽ được coi là đóng
.
Tuy nhiên một class có thể được coi là mở
(cho việc mở rộng) và đóng
(với việc chỉnh sửa) cùng 1 lúc.
Khi class đã được đưa vào framework và được sử dụng rộng rãi thì việc thay đổi source code của class là một việc làm không cần thiết, thay vào đó bạn có thể kế thừa, tạo subclasses từ class đó.
Trên thực tế, nếu một class bị bug, bạn nên sửa trực tiếp nó thay vì tạo ra subclass như một bản vá cho class hiện thời vì subclass không có nhiệm vụ là giải quyết issues của parent-class.
VD:
Bạn có một ứng dụng EC, bạn muốn thay đổi công thức tính phí shipping
. Bạn hoàn toàn có thể thay đổi trực tiếp class Order
như ở dưới đây.
Thế nhưng việc thay đổi class code như vậy có thể làm ảnh hưởng đến các phần khác của ứng dụng, thay vào đó bạn có thể ứng dụng Strategy Pattern
, đưa getShippingCost()
method sang một class khác có chung interface.
Lúc này khi bạn muốn thêm một shipping method khác, bạn chỉ cần tạo một class mới implement Shipping Interface
mà không cần phải đụng chạm gì đến Order class
. Client code sử dụng Order class
chỉ cần link Order Class
với Shipping Object
mới là xong.
Trong Client Side Rendering (CSR), chỉ có phần khung HTML được render bởi phía server ngoài ra:
Đây như một phương thức giúp triển khai các SPA (Single Page Application), xoá nhoà đi sự khác biệt giữa website và installed application.
Cùng xét ví dụ hiển thị thời gian đơn giản như sau:
<div id="root"></div>
function tick() {
const element = (
<div>
<h1>Hello World</h1>
<h2>It is {new Date().toLocaleTimeString()}</h2>
</div>
);
ReactDOM.render(element, document.getElementById('root'));
}
setInterval(tick, 1000);
Code HTML chỉ gồm một thẻ div
duy nhất. Phần code hiển thị, xử lí thời gian đều nằm ở phía JS (TS), mọi thứ được update ngay lập tức mà không cần phải thông qua server hay API nào cả.
Đi kèm theo sự phức tạp của các trang web như:
Việc render một trang với file bundle có kích thước lớn sẽ làm tăng FCP và TTI của page.
Như hình minh hoạ phía trên, khi việc render file bundle càng mất thời gian thì khoảng cách giữa FP
với FCP
và TTI
càng tăng, đồng nghĩa với việc khoảng thời gian mà người dùng nhìn thấy một trang trắng cũng sẽ tăng theo.
Mọi ứng dụng được dev bởi React đều thực thi ở phía client, nó sẽ tương tác với API để lấy về data hoặc lưu data. Việc bạn nhấn vào 1 link hoặc thay đổi route không làm phát sinh bất kì một req nào về phía server, mọi xử lí sẽ nằm ở phía client.
CSR cho phép SPA có thể navigate mà không cần reload page - Đây là một trải nghiệm tuyệt vời đối với người dùng. Qua đó khiến cho web trở nên responsive hơn.
Bên cạnh những ưu điểm trên, CSR cũng có một số nhược điểm sau:
SEO
: các web crawler có thể dễ dàng thông dịch được các web được render bởi server. Với CSR mọi thứ sẽ phức tạp hơn khi phải render một trang lớn với các network req đi kèm, sẽ khó cho crawler có thể index page do việc render page sẽ lâu hơn.Performance
: CSR cải thiện trải nghiệm của người dùng khi không phải load đi load lại trang quá nhiều nhưng ở thời điểm load page đầu tiên, người dùng có thể phải chờ một khoảng thời gian đáng kể nếu kích thước file bundle lớn.Khả năng maintain code
: sẽ có nhiều component bị lặp lại ở cả phía client và server (VD như validate, format logic...)Data fetching
: với CSR, việc fetching data sẽ thiên về event-driven
(hướng sự kiện) - sẽ phát sinh khi load page hoặc click button.Vấn đề về hiệu năng với CSR thường sẽ nằm ở file bundle. Để cải thiện điều này chỉ có một cách duy nhất đó là tối ưu hoá code.
<head>
trong code HTML<link rel="preload" as="script" href="critical.js">
phần code phía trên sẽ load file critical.js
trước khi cơ chế render page được thực hiện. Tuy nhiên nó lại không hề block lại việc render page.
Chat component
sẽ không nhất thiết phải load ở lần load đầu tiênWebpack
.Là Pattern cho phép ta chia một class lớn hoặc tập các classes có mối liên hệ gần nhau thành 2 classes, tập riêng biệt, độc lập với nhau.
Ta cùng nhau lấy một ví dụ với class tổng quát là Shape
với 2 sub-classes là Circle
và Square
. Giả sử trong trường hợp ta muốn kết hợp với màu, ví dụ như: Red
& Blue
thì ta cần cả thảy 4 sub-classes: BlueCircle
, BlueSquare
, RedCircle
, RedSquare
.
Nếu sau này ta thêm một shape mới như Triangle
thì ta cần thêm 2 sub-classes đó là BlueTriangle
, RedTriangle
. Sau đó nếu thêm màu mới thì ta lại phải thêm 3 sub-classes nữa.
Tổng quát lên, nếu số lượng shape và màu tăng thì số lượng sub-classes sẽ tăng với tốc độ của hàm mũ.
Vấn đề này xảy ra là do ta đang cố gắng mở rộng class Shape
thành 2 nhánh là color
và form
. Đây là vấn đề thường gặp với class inheritance
.
Bridge pattern giải quyết vấn đề này bằng cách thay vì kế thừa sẽ chuyển qua object composition. Tách một nhánh cũ thành một tập class và sub-classes riêng biệt. Class gốc ban đầu sẽ tham chiếu đến class thuộc về nhánh mới này thay vì giữ toàn bộ thuộc tính và behavior của nó trong một class duy nhất.
Lúc này, việc thêm màu mới cho Shape
không quá khó khăn khi chỉ cần thêm vào list các colors của Shape
mà thôi. Reference tới Color
thuộc Shape
sẽ đóng vai trò như một chiếc cầu nối giữa 2 classes.
Abstraction
là tầng ở mức cao, nó không thực hiện một hành động thực tế nào cả, thay vào đó nó sẽ chuyển tiếp công việc này cho tầng implementation
(còn gọi là tầng platform
).
Khi nói về ứng dụng trong thực tế thì những gì được gọi là abstraction
thường sẽ được cụ thể hoá thông qua UI. Và việc triển khai sẽ nằm ở API hoặc OS.
Nói một cách tổng quát, ta có thể chia một ứng dụng trong thực tế ra thành 2 nhánh độc lập:
Tuy nhiên trong kịch bản tồi tệ nhất, app có thể trông như một bát mì Ý khổng lồ khi cả tá điều kiện kết nối giữa UI và API tồn tại trong code của bạn.
Việc sửa đổi một app với kiến trúc là monolithic sẽ rất khó vì bạn phải hiểu rõ toàn bộ hệ thống, còn với app được chia modules rõ ràng thì việc sửa đổi sẽ dễ hơn rất nhiều.
Bạn có thể đưa các đoạn code liên quan đến một interface-platform cụ thể vào các classes riêng biệt. Nhưng sẽ có rất nhiều
classes mới sẽ được sinh ra. Việc kế thừa class cũng sẽ tăng nhanh chóng mặt vì việc thêm UI mới hoặc API mới sẽ đòi hỏi các classes mới.
Chúng ta có thể giải quyết vấn đề trên bằng Bridge pattern. Ta sẽ chia các classes thành 2 nhánh kế thừa:
Abstraction object điểu khiển tầng UI, nó sẽ chuyển tiếp công việc xử lí cho các implementation objects. Các implementation objects khác nhau hoàn toàn có thể tráo đổi vai trò cho nhau nếu chúng cùng tuân theo interface chung cũng như cho phép UI hoạt động với Window, Linux.
Kết quả là ta có thể thay đổi UI classes mà không làm ảnh hưởng gì đến API-related classes.
Abstraction
: cung cấp high-level control. Nó sẽ chuyển tiếp việc xử lí cấp thấp (low-level) cho implementation object
Implementation
: định nghĩa interface cho các class triển khai. Abstraction object chỉ có thể giao tiếp với implementation object thông qua các methods được định nghĩa ở interface này.Concrete Implementations
: platform-specific code.Client
chỉ quan tâm tới việc tương tác với abstraction.Bridge Pattern giúp ta chia nhỏ Monolithic class thành các nhánh class kế thừa độc lập với nhau. Khi class càng lớn, càng dài thì sẽ càng khó để nắm bắt luồng làm việc, cũng như fix bug cho nó. Ngoài ra việc thêm tính năng mới cũng gặp nhiều khó khăn. Việc chia class thành các nhánh class kế thừa cho phép ta có thể thay đổi được class trong nhánh kế thừa này mà không làm ảnh hưởng đến các classes ở nhánh kế thừa khác.
Sử dụng pattern khi bạn muốn mở rộng class theo các nhánh độc lập nhau. Từ đó original class có thể chuyển tiếp các công việc cho các class con trong cây kế thừa thay vì làm trực tiếp mọi việc.
Pattern này cho phép ta có thể kết hợp các objects lại thành một cấu trúc cây, sau đó làm việc với cấu trúc cây này như thể nó là các objects riêng lẻ.
Sử dụng Composite pattern chỉ phát huy tác dụng khi core model của bạn có cấu trúc cây.
Lấy ví dụ trong thực tế, bạn có 2 loại objects: Products
và Boxes
. Box
có thể chứa một vài Products
hoặc thậm chí các Boxes
nhỏ hơn ...
Vấn đề ta gặp phải là khi tính tổng giá tiền. Trong thực tế ta có thể mở toàn bộ các Boxes
ra rồi tính tổng tiền nhưng trong chương trình điều này không đơn giản chỉ là chạy mấy vòng lặp for
là xong.
Do các classes này sẽ lồng nhau (ít hoặc nhiều tầng) nên việc tính trực tiếp như vậy là điều không thể.
Composite pattern cho rằng nên sử dụng Products
và Boxes
thông qua một interface chung - interface này sẽ định nghĩa method dùng cho việc tính giá tiền.
Method này sẽ hoạt động như thế nào ? Với product nó sẽ trả về giá tiền của product luôn, còn với box thì nó sẽ duyệt qua toàn bộ các items bên trong box, nếu gặp box con bên trong nó sẽ duyệt các items bên trong box con rồi tính giá tiền, cứ thế cho đến khi toàn bộ giá tiền của các components con trong box được tính thì thôi.
Method này sẽ chạy một cách đệ quy để tính giá tiền
Cách làm này có ưu điểm ở chỗ bạn không cần quan tâm đến class của object mà bạn đang tính tiền. Bạn sẽ xử lí chúng như nhau thông qua một interface chung. Khi bạn gọi method, object sẽ truyền request xuống theo độ sâu của cây.
Component
interface định nghĩa các operations chung mà các elements đơn giản và phức tạp đều cóLeaf
là element thấp nhất, không có sub-element. Leaf sẽ thực hiện công việc trên thực tế do chúng không thể chuyển tiếp công việc xuống cho element nào thấp hơn cả.Container
có các sub element, một container không hề biết gì về class của sub element mà chỉ làm việc với sub element thông qua interface chung mà thôi. Khi nhận được request, nó sẽ chuyển tiếp xuống cho sub element hoặc các containers con. Container chỉ xử lí các kết quả trung gian và trả về kết quả cuối cùng cho client mà thôi.Là một creational design pattern. Ở superclass sẽ tạo ra các objects, nhưng ở các subclasses sẽ cho phép ta có thể thay đổi kiểu của object
Vấn đề
Giả sử bạn đang xây dựng một ứng dụng vận chuyển hoặc logistics chuyên dùng cho xe tải Truck
. Ứng dụng của bạn trở nên nổi tiếng, bạn nhận được đơn hàng từ các hãng vận chuyển đường biển. Lúc này làm cách nào để đáp ứng nhu cầu của khách hàng đây, thêm class Ship
song song với class Truck
hiện có ? Nó có thể giải quyết vấn đề hiện tại nhưng nếu sau này phát sinh thêm một hình thái vận chuyển nữa thì sao ? Nếu vẫn theo cách tiếp cận cũ, code của bạn sẽ trở nên rất to và phức tạp.
Giải pháp
Thay vì sử dụng new
trực tiếp cho constructor tương ứng với phương tiện vận chuyển, ta có thể gọi đến factory method
, bên trong method này sẽ gọi đến new
operator để tạo ra kết quả như là một sản phẩm
.
Có thể nhiều người sẽ thấy rằng chỉ đơn thuần là chuyển việc gọi constructor từ một nơi sang một nơi khác. Nhưng trong thực tế ta có thể override factory method và thay đổi lại class của products
được tạo ra bởi method.
Các factory method của các subclasses chỉ có thể trả về các products với kiểu khác nhau chỉ khi chúng có chung base class
hoặc interface
. Bản thân factory method của base class cũng phải có kiểu trả về được định nghĩa như là interface
chung (được đề cập ở câu trước).
Truck
và Ship
class đều triển khai Transport
interface, method deliver
của hai class này sẽ có các cách triển khai khác nhau. Tuy vậy phía client code khi sử dụng Truck
hoặc Ship
đều không phân biệt được loại nào với loại nào mà client code sẽ coi chúng đều là Transport
. Việc các method deliver
được triển khai như thế nào hoàn toàn không quan trọng đối với client.
Cấu trúc
Truck
hoặc Ship
ở ví dụ trênProduct
(chức năng chính của nó KHÔNG HẲN lúc nào cũng là trả về một object mà nó còn có thể chứa các core business logic).Ta cũng có thể định nghĩa base class như là một abstract class
để bắt buộc các subclasses sẽ triển khai lại factory method.
Factory method không nhất thiết phải tạo mới một instance mà có thể lấy từ cache, object pool hoặc các nguồn khác.
Ref: https://refactoring.guru/design-patterns/factory-method
Khi tiến hành extend class hãy cố gắng để bạn có thể sử dụng objects của sub-class để thay thế cho objects của parent class mà không làm ảnh hưởng tới client code.
Điều đó có nghĩa rằng sub-class nên tương thích với các behaviors hiện có của super class. Khi overriding một method, thay vì thay đổi toàn bộ logic của method đó, hãy code theo hướng mở rộng behavior cho nó.
Substitution Principle là một tập hợp các quy tắc để kiểm tra xem objects của sub-class có hoạt động được với client code hiện thời như super-class hay không.
Dưới đây là check list:
1. Param types của method trong sub-class nên match hoặc mang tính trừu tượng cao hơn so với param types của method trong superclass
VD: Class hiện thời có method feed(Cat c)
.
Good: nếu ở sub-class ta override method trên theo hướng sau → feed(Animal a)
: method này vẫn hoạt động như method của superclass do Cat
là tập con của Animal
, hơn thế nữa nó cũng có thể áp dụng cho nhiều use-case khác nhau như Dog
, Cow
, ...
Bad: nếu ở sub-class ta override method trên theo hướng → feed(BengalCat c)
: BengalCat
là một tập con của Cat
nên method này chỉ hoạt động được với một vài TH nhất định và không còn thích hợp với client code như cũ nữa.
2. Return type của method trong sub-class nên match hoặc là subtype của return type thuộc về method trong superclass
Ta có thể thấy yêu cầu về return type
là ngược lại so với param type
. Lấy ví dụ như sau, method của super-class buyCat(): Cat
thì method của sub-class nên là buyCat(): BengalCat
thay vì buyCat(): Animal
.
3. Method trong sub-class không nên throw ra những Exceptions mà base method không yêu cầu phải throw ra
Nói cách khác là type của exception trong sub-class nên match
hoặc là subtype
của exception mà base method có thể throw ra. Trên thực tế thì try-catch
block của client code thường sẽ chỉ bắt các exceptions mà base-method có thể throw ra. Việc sub-class method throw exception khác type sẽ làm cho try-catch
block của client code không bắt được exception đó.
4. Sub-class không nên tăng mức độ chặt chẽ của pre-conditions. Lấy ví dụ, nếu base method param yêu cầu int
nhưng sub-class lại yêu cầu positive int
thì việc làm này sẽ làm tăng tính chặt chẽ của pre-condition (strengthen pre-condition). Điều này sẽ khiến cho client code cảm thấy "bối rối"
5. Sub-class không nên làm yếu post-condition. Lấy ví dụ sau khi mọi thao tác kết thúc xong, ta sẽ tiến hành đóng connection tới DB tại base method nhưng sub-class method vẫn giữ nguyên connection để tái sử dụng. Client code không hề biết về việc này nên sau khi thao tác xong, client tắt toàn bộ chương trình, dẫn tới tình trạng còn connection tới DB vẫn chưa được đóng lại.
6. Tính bất biến của super-class phải được bảo đảm. Tính bất biến có thể hiểu đơn giản như sau: một con mèo, dù là giống mèo gì đi nữa cũng vẫn sẽ có 4 chân, và kêu meow meow. Tuy nhiên tính bất biến
trong thực tế lại rất dễ bị xâm phạm. Do đó khi tiến hành extends class ta chỉ nên thêm methods
hoặc thêm fields
mới chứ không nên chỉnh sửa lại quá nhiều những phần sẵn có của super-class.
7. Sub-class không nên thay đổi các giá trị private của super-class. Do với một vài ngôn ngữ như Python
hay JS
, chúng vẫn cho phép bạn có thể truy cập vào private field của super-class.
VD:
Cùng lấy một ví du về sự vi phạm quy tắc Substitution.
Cụ thể như sau, ta thấy sub-class
throw ra một Exception
mà super-class
hoàn toàn không có, điều này sẽ làm cho client-code phụ thuộc vào class nó sử dụng. Phân tích ta sẽ thấy như sau, nếu document không phải là ReadOnlyDocument
thì sẽ throw ra Exception
vậy nên ở client-code (ở đây là Project
) sẽ phải check xem document có phải là ReadOnlyDocument
, từ đó client-code sẽ phụ thuộc vào class ReadOnlyDocument
.
Sửa lại cấu trúc kế thừa, ta sẽ có như sau:
sub-class WritableDocument
sẽ kế thừa từ super-class Document
với việc viết thêm method save()
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.