+21

Design Patterns: Strategy Pattern trong TypeScript 😊 (Series: Bón hành TypeScript - PHẦN 1)

Cách sử dụng Strategy Pattern bằng TypeScript để giải quyết các vấn đề thực tế trong các project web.

Chào mừng bạn đến với loạt bài Design Patterns trong TypeScript, loạt bài này mình sẽ giới thiệu một số Design Patterns hữu ích trong phát triển web bằng TypeScript.

Các Design Patterns rất quan trọng đối với các web developer và chúng ta có thể code tốt hơn bằng cách thành thạo chúng. Trong bài viết này, mình sẽ sử dụng TypeScript để giới thiệu Strategy Pattern .

Vấn đề

Đăng ký và Đăng nhập là các tính năng quan trọng trong các ứng dụng web. Khi đăng ký một ứng dụng web, cách phổ biến hơn để đăng ký là sử dụng account/password, email hoặc số điện thoại di động... Khi bạn đã đăng ký thành công, bạn có thể sử dụng hàm tương ứng để Login.

function login(mode) {
  if (mode === "account") {
    loginWithPassword();
  } else if (mode === "email") {
    loginWithEmail();
  } else if (mode === "mobile") {
    loginWithMobile();
  }
}

Khi ứng dụng web cần hỗ trợ các hàm Login khác, ví dụ: ngoài hàm Login email, trang Login còn hỗ trợ các tính năng Login của nền tảng bên thứ ba như Google, Facebook, Apple và Twitter.

image.png

Vậy để hỗ trợ thêm các phương thức hàm Login của bên thứ ba, chúng ta cần sửa đổi function login trước đó:

function login(mode) {
  if (mode === "account") {
    loginWithPassword();
  } else if (mode === "email") {
    loginWithEmail();
  } else if (mode === "mobile") {
    loginWithMobile();
  } else if (mode === "google") {
    loginWithGoogle();
  } else if (mode === "facebook") {
    loginWithFacebook();
  } else if (mode === "apple") {
    loginWithApple();
  } else if (mode === "twitter") {
    loginWithTwitter();
  } 
}

Nhìn có vẻ không ổn chút nào nhở! Nếu sau này chúng ta tiếp tục thêm hoặc sửa đổi các phương thức Login, chúng ta sẽ thấy rằng function login này ngày càng trở nên khó maintenance hơn. Đối với vấn đề này, chúng ta có thể sử dụng Strategy Pattern để đóng gói các hàm Login khác nhau vào các strategy khác nhau.

Strategy Pattern

Để hiểu rõ hơn về đoạn Strategy Pattern, trước tiên chúng ta hãy xem sơ đồ UML tương ứng:

image.png

Trong hình trên, chúng ta xác định một Interface Strategy, sau đó implement hai strategy Login cho Twitteraccount/password dựa trên Interface này.

Interface strategy

interface Strategy {
  authenticate(args: any[]): boolean;
}

Strategy Twitter Class được triển khai từ Interface Strategy

class TwitterStrategy implements Strategy {
  authenticate(args: any[]) {
    const [token] = args;
    if (token !== "tw123") {
      console.error("Twitter account authentication failed!");
      return false;
    }
    console.log("Twitter account authentication succeeded!");
    return true;
  }
}

LocalStrategy class cũng được triển khai từ Interface Strategy

class LocalStrategy implements Strategy {
  authenticate(args: any[]) {
    const [username, password] = args;
    if (username !== "bytefer" || password !== "666") {
      console.log("Incorrect username or password!");
      return false;
    }
    console.log("Account and password authentication succeeded!");
    return true;
  }
}

Sau khi có các được triển khai từ Interface Strategy khác nhau, chúng ta định nghĩa một lớp Authenticator để chuyển đổi giữa các strategy Login khác nhau và thực hiện các thao tác authentication tương ứng.

Authenticator class

class Authenticator {
  strategies: Record<string, Strategy> = {};
  use(name: string, strategy: Strategy) {
    this.strategies[name] = strategy;
  }
  authenticate(name: string, ...args: any) {
    if (!this.strategies[name]) {
      console.error("Authentication policy has not been set!");
      return false;
    }
    return this.strategies[name].authenticate.apply(null, args);
    // authenticate.apply là cú pháp apply args của typescript
  }
}

authenticate.apply gọi hàm authenticate với giá trị this đã cho và các đối số được cung cấp dưới dạng một mảng (hoặc một đối tượng giống như mảng)

Sau đó, chúng ta có thể sử dụng các hàm Login khác nhau để đạt được authentication user theo các cách sau:

const auth = new Authenticator();
auth.use("twitter", new TwitterStrategy());
auth.use("local", new LocalStrategy());
function login(mode: string, ...args: any) {
  return auth.authenticate(mode, args);
}
login("twitter", "123");
login("local", "bytefer", "666");

Khi bạn chạy thành công đoạn code trên, output tương ứng được hiển thị trong hình sau:

image.png

Strategy Pattern ngoài việc sử dụng cho trường hợp authentication Login cũng có thể được sử dụng trong nhiều kịch bản khác nhau (Ví dụ như: form validation). Nó cũng có thể được sử dụng để tối ưu hóa các vấn đề với quá nhiều nhánh if/else.

Nếu bạn sử dụng Node.js để phát triển các authentication service, thì thông thương các bạn sẽ sử dụng thư viện passport.js này đúng ko. Nếu các bạn đã từng sử dụng thư viện/Mô-đun Passport.js này nhưng ko biết cách nó hoạt động ra sao, thì Strategy Pattern sẽ giúp bạn hiểu về nó hơn. Dùng một thứ mình hiểu vẫn tốt hơn là dùng mà ko hiểu đúng ko. Ahihi

Mô-đun passport.js này rất mạnh mẽ và hiện hỗ trợ tới 538 strategy:

image.png

Một số tình huống mà bạn có thể suy nghĩ đến việc sử dụng Strategy Pattern:

  • Khi một hệ thống cần tự động chọn một trong số các thuật toán. Và mỗi thuật toán có thể được gói gọn trong một strategy.
  • Nhiều class chỉ khác nhau về hành vi và có thể sử dụng Strategy Pattern để tự động chọn hành vi cụ thể sẽ được thực thi trong thời gian chạy.

English Version

Using the Strategy Pattern with TypeScript to Solve Real-World Issues in Web Projects

Welcome to the series of articles on Design Patterns in TypeScript. In this series, I will introduce some useful design patterns for web development using TypeScript.

Design Patterns are essential for web developers, and we can improve our code by mastering them. In this article, I will use TypeScript to introduce the Strategy Pattern.

Problem

Registration and login are important features in web applications. The most common way to register in a web application is by using an account/password, email, or mobile number. Once you have successfully registered, you can use the corresponding function to log in.

function login(mode) {
  if (mode === "account") {
    loginWithPassword();
  } else if (mode === "email") {
    loginWithEmail();
  } else if (mode === "mobile") {
    loginWithMobile();
  }
}

When a web application needs to support additional login methods, such as login with Google, Facebook, Apple, or Twitter, we need to modify the existing login function:

function login(mode) {
  if (mode === "account") {
    loginWithPassword();
  } else if (mode === "email") {
    loginWithEmail();
  } else if (mode === "mobile") {
    loginWithMobile();
  } else if (mode === "google") {
    loginWithGoogle();
  } else if (mode === "facebook") {
    loginWithFacebook();
  } else if (mode === "apple") {
    loginWithApple();
  } else if (mode === "twitter") {
    loginWithTwitter();
  } 
}

This doesn't look good! If we continue to add or modify login methods in the future, we will find that this login function becomes harder to maintain. To address this issue, we can use the Strategy Pattern to encapsulate different login functions into separate strategies.

Strategy Pattern

To understand the Strategy Pattern better, let's first take a look at the corresponding UML diagram:

image.png

In the above diagram, we define an Interface Strategy and then implement two login strategies for Twitter and account/password based on this interface.

Strategy Interface

interface Strategy {
  authenticate(args: any[]): boolean;
}

TwitterStrategy class implementing the Strategy Interface

class TwitterStrategy implements Strategy {
  authenticate(args: any[]) {
    const [token] = args;
    if (token !== "tw123") {
      console.error("Twitter account authentication failed!");
      return false;
    }
    console.log("Twitter account authentication succeeded!");
    return true;
  }
}

LocalStrategy class also implementing the Strategy Interface

class LocalStrategy implements Strategy {
  authenticate(args: any[]) {
    const [username, password] = args;
    if (username !== "bytefer" || password !== "666") {
      console.log("Incorrect username or password!");
      return false;
    }
    console.log("Account and password authentication succeeded!");
    return true;
  }
}

After having different implementations of the Strategy Interface, we define an Authenticator class to switch between different login strategies and perform the corresponding authentication operations.

Authenticator class

class Authenticator {
  strategies: Record<string, Strategy> = {};
  use(name: string, strategy: Strategy) {
    this.strategies[name] = strategy;
  }
  authenticate(name: string, ...args: any) {
    if (!this.strategies[name]) {
      console.error("Authentication policy has not been set!");
      return false;
    }
    return this.strategies[name].authenticate.apply(null, args);
  }
}

authenticate.apply calls the authenticate function with the given this value and arguments provided as an array (or an array-like object)

Then, we can use different login functions to achieve user authentication as follows:

const auth = new Authenticator();
auth.use("twitter", new TwitterStrategy());
auth.use("local", new LocalStrategy());

function login(mode: string, ...args: any) {
  return auth.authenticate(mode, args);
}

login("twitter", "123");
login("local", "bytefer", "666");

When you successfully run the above code, the corresponding output will be displayed as shown in the following image:

image.png

The Strategy Pattern can be used not only for authentication purposes but also in various other scenarios (e.g., form validation). It can also be used to optimize issues with excessive if/else branching.

If you use Node.js for developing authentication services, you might have used the Passport.js library. If you have used this Passport.js module but don't understand how it works, the Strategy Pattern will help you understand it better. It's always better to use something you understand, right?

The passport.js module is powerful and currently supports up to 538 strategies:

image.png

Here are some situations where you might consider using the Strategy Pattern:

  • When a system needs to automatically choose one of several algorithms, and each algorithm can be encapsulated in a strategy.
  • When multiple classes differ only in behavior and can use the Strategy Pattern to automatically select the specific behavior to be executed at runtime.

日本語版

TypeScriptを使用したストラテジーパターンによるウェブプロジェクトの現実的な問題の解決方法

TypeScriptによるデザインパターン シリーズへようこそ。このシリーズでは、TypeScriptを使用したウェブ開発におけるいくつかの便利なデザインパターンを紹介します。

デザインパターンはウェブ開発者にとって必須であり、これらを習得することでコードを改善することができます。この記事では、TypeScriptを使用してストラテジーパターンを紹介します。

問題

登録とログインはウェブアプリケーションにおける重要な機能です。ウェブアプリケーションで一般的に使用される登録方法は、アカウント/パスワード、メール、または携帯電話番号を使用する方法です。登録が成功したら、対応する機能を使用してログインすることができます。

function login(mode) {
  if (mode === "account") {
    loginWithPassword();
  } else if (mode === "email") {
    loginWithEmail();
  } else if (mode === "mobile") {
    loginWithMobile();
  }
}

ウェブアプリケーションがGoogle、Facebook、Apple、Twitterなどの追加のログイン方法をサポートする必要がある場合、既存のlogin関数を変更する必要があります。

function login(mode) {
  if (mode === "account") {
    loginWithPassword();
  } else if (mode === "email") {
    loginWithEmail();
  } else if (mode === "mobile") {
    loginWithMobile();
  } else if (mode === "google") {
    loginWithGoogle();
  } else if (mode === "facebook") {
    loginWithFacebook();
  } else if (mode === "apple") {
    loginWithApple();
  } else if (mode === "twitter") {
    loginWithTwitter();
  } 
}

これはうまくいきません! 将来的にログイン方法を追加または変更し続けると、このlogin関数を維持することが困難になることがわかります。この問題に対処するために、ストラテジーパターンを使用して異なるログイン機能を別々のストラテジーにカプセル化することができます。

ストラテジーパターン

ストラテジーパターンをよりよく理解するために、対応するUML図をまず見てみましょう。

image.png

上記の図では、Interface Strategyを定義し、このインターフェースを基にTwitterおよびアカウント/パスワードに対する2つのログインストラテジーを実装しています。

ストラテジーインターフェース

interface Strategy {
  authenticate(args: any[]): boolean;
}

ストラテジーインターフェースを実装したTwitterStrategyクラス

class TwitterStrategy implements Strategy {
  authenticate(args: any[]) {
    const [token] = args;
    if (token !== "tw123") {
      console.error("Twitterアカウントの認証に失敗しました!");
      return false;
    }
    console.log("Twitterアカウントの認証に成功しました!");
    return true;
  }
}

ストラテジーインターフェースを実装したLocalStrategyクラス

class LocalStrategy implements Strategy {
  authenticate(args: any[]) {
    const [username, password] = args;
    if (username !== "bytefer" || password !== "666") {
      console.log("ユーザー名またはパスワードが間違っています!");
      return false;
    }
    console.log("アカウントとパスワードの認証に成功しました!");
    return true;
  }
}

異なるストラテジーインターフェースの実装ができたら、Authenticatorクラスを定義し、異なるログインストラテジー間を切り替えて対応する認証操作を実行します。

Authenticatorクラス

class Authenticator {
  strategies: Record<string, Strategy> = {};
  use(name: string, strategy: Strategy) {
    this.strategies[name] = strategy;
  }
  authenticate(name: string, ...args: any) {
    if (!this.strategies[name]) {
      console.error("認証ポリシーが設定されていません!");
      return false;
    }
    return this.strategies[name].authenticate.apply(null, args);
  }
}

authenticate.applyは、与えられたthis値と配列(または配列風オブジェクト)として提供された引数を使用してauthenticate関数を呼び出します

その後、異なるログイン関数を使用してユーザー認証を実現できます。

const auth = new Authenticator();
auth.use("twitter", new TwitterStrategy());
auth.use("local", new LocalStrategy());

function login(mode: string, ...args: any) {
  return auth.authenticate(mode, args);
}

login("twitter", "123");
login("local", "bytefer", "666");

上記のコードを正常に実行すると、次の画像に示すような対応する出力が表示されます。

image.png

ストラテジーパターンは認証目的だけでなく、他のさまざまなシナリオ(例:フォームのバリデーション)にも使用できます。また、冗長なif/elseの問題を最適化するためにも使用できます。

認証サービスの開発にNode.jsを使用している場合、おそらくPassport.jsライブラリを使用したことがあるかもしれません。このPassport.jsモジュールを使用したことがあるが、その仕組みが理解できていない場合、ストラテジーパターンが理解を助けてくれるでしょう。理解できるものを使用する方が常に良いですよね。

passport.jsモジュールは非常に強力で、現在538のストラテジーをサポートしています。

image.png

以下は、ストラテジーパターンを使用することを検討すべきいくつかの状況です。

  • システムが複数のアルゴリズムのうちの1つを自動的に選択する必要があり、各アルゴリズムをストラテジーにカプセル化できる場合。
  • 複数のクラスが振る舞いのみが異なり、実行時に特定の振る舞いを自動的に選択することができる場合、ストラテジーパターンを使用できます。

Mình hy vọng bạn thích bài viết này và học thêm được điều gì đó mới.

Donate mình một ly cafe hoặc 1 cây bút bi để mình có thêm động lực cho ra nhiều bài viết hay và chất lượng hơn trong tương lai nhé. À mà nếu bạn có bất kỳ câu hỏi nào thì đừng ngại comment hoặc liên hệ mình qua: Zalo - 0374226770 hoặc Facebook. Mình xin cảm ơn.

Momo: NGUYỄN ANH TUẤN - 0374226770

TPBank: NGUYỄN ANH TUẤN - 0374226770 (hoặc 01681423001)

image.png


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í