+9

Design Pattern cùng Flutter. Tập 7: State - "Siêu nhân biến hình"

Giới thiệu

Giả sử như hôm nay bạn có tiền và bắt đầu đi rút tiền ở một cây ATM, bạn sẽ thấy các trạng thái đơn giản như: chờ đút thẻ vào atm, nhập mật khẩu, thao tác rút tiền, nhận tiền và trả thẻ. Đối với mỗi trạng thái đều có thể chuyển sang trạng thái tiếp theo dựa vào trạng thái hiện tại. Đó gọi là biết mình biết ta, trăm trận toàn tạch 🤡.

Xời, với những trạng thái đơn giản như thế thì chỉ cần if else là xong nhỉ:

  • If cây ATM còn tiền thì cho rút tiền else thông báo là bạn nghèo rồi
  • If đút thẻ đúng thì cho vào else thì đá nó ra
  • If nhập mật khẩu đúng thì chuyển sang thao tác rút tiền ngược lại else bắt nhập lại mật khẩu

Vẫn còn chịu được, ơ thế giờ lỡ thêm 1 trạng thái else nữa thì sao nhỉ:

  • If nhập mật khẩu đúng thì chuyển sang rút tiền else if nhập 5 lần sai sẽ bắt nhập lại mật khẩu else đá thẻ ra và thông báo đi trả thẻ cho người khác đi =))

Lúc này thấy là rối rối rồi đúng không? Việc tổ chức theo dạng condition thì cả mình và dev khác vào đọc sẽ tẩu hoả nhập ma mất =)))) Chưa kể đến các business logic rắc rối và phức tạp như thế nào. Vậy ra, lúc này phải cần một mẫu thiết kế thôi, điển hình cho mẫu thiết kế này được gọi tên: State - "Siêu nhân biến hình".

Vậy...

State là ai, địa chỉ nhà ở đâu?

State là một loại mẫu thiết kế thuộc behavioural được ra đời để đóng gói lại các logic của các State class và thay đổi hành vi của nó tuỳ vào ngữ cảnh đang xảy ra. Tất nhiên là nhờ vậy mà việc tạo mới một State class dễ dàng và rõ ràng hơn.

Thế...

Mục tiêu là gì, tại sao nó lại tồn tại

Mẫu thiết kế State ra đời với mục tiêu giống y đúc mẫu thiết kế Strategy (thật ra là base từ Strategy), nhưng khác biệt to lớn nhất ở đây là Strategy sẽ lựa chọn thuật toán ổn định và ít sửa đổi, còn State linh hoạt hơn với sự thay đổi trạng thái, nên tuỳ vào mục đích sử dụng để có thể chọn thằng nào. Chung quy Strategy sẽ "toàn năng" nhưng thiếu "biến hình" như State 👻.

State Class Diagram

Cách tiếp cận tổng quát của mẫu thiết kế State được biểu diễn bằng sơ đồ lớp bên dưới:

Các thành phần chính:

  • State: Interface hoặc abstract class đại diện cho một lớp thành phần, định nghĩa ra phương thức dùng chung cho các ConcreteStates. Tuyệt đối không nên định nghĩa ra phương thức dùng chung mà tồn tại state không sử dụng nó.
  • ConcreteStates: Là lớp kế thừa State sẽ biểu diễn cụ thể state. Mỗi class sẽ triển khai gắn liền với Context, tham chiếu đến Context và có thể lấy thông tin hoặc chuyển đổi trạng thái của state được khai báo trong Context (thông qua hàm setState).
  • Context: Duy trì một instance của ConcreteStates, nên được khai báo như một Singleton (nếu chưa biết về Singleton có thể tham khảo bài viết này) . Tham chiếu đến lớp interface State và sử dụng phụ thuộc vào các ConcreteStates. Lưu ý là Context sẽ không biết chi tiết về các ConcreteStates, sẽ cung cấp một phương thức setState để thay đổi trạng thái hiện tại từ các lớp ConcreteStates.

Ứng dụng

Mẫu thiết kế state được sử dụng nếu thấy đối tượng đó có nhiều trạng thái và có nhiều sự thay đổi tuỳ thuộc vào trạng thái hiện tại của nó. Bằng cách đóng gói mỗi trạng thái của nó thành những class riêng biệt thì việc thêm trạng thái mới không còn khó khăn mà lại không ảnh hưởng đến các trạng thái khác, quá dữ. Ý tưởng của mẫu thiết kế này đã tuân thủ một số quy tắc trong SOLID: chữ S (Mỗi class chỉ chịu một trách nhiệm và được đóng gói trong lớp của nó) và chữ O (Dễ dàng mở rộng và hạn chế sửa đổi). Hầu như logic nào có nhiều trạng thái khác nhau và có thể tăng thêm số lượng trạng thái thì đều có thể triển khai bằng mẫu thiết kế State. Ví dụ thực tế: Khi quản lý dự án sẽ cần theo dõi trạng thái của những tasks: New, InProgress, Done, Pending, Closed,..., thực tế của thực tế hơn nữa mà ngày nào cũng gặp, bạn sẽ thấy cách hiển thị màu sắc của đèn giao thông cũng sẽ phải thông qua màu sắc hiện tại hoặc là ngay hiện tại khi mình đang viết bài viết này, nó cũng đang ở trạng thái lưu nháp, viết xong sẽ Xuất bản bài viết,..

Thực hành

Lấy đại một ví dụ ở trên thử luôn cho nóng nhé, để mình random xem như thế nào:

Tada, vậy là mình sẽ phải theo sự sắp đặt ở trên thôi (ai gato sẽ bảo mình sắp xếp kịch bản 😗) . Thế ta cùng đi vào sử dụng mẫu thiết kế State để quản lý đèn giao thông nhé.

Để phân tích bước đầu tiên dựa vào class diagram trên, ta cần một thằng ITrafficLightState đóng vai trò là một interface chứa phương thức nextTrafficLight(context) , để thêm màu mè dễ phân biệt ta sẽ thêm getColor() để get màu của một cái đèn và getDuration() để lấy ra thời gian tồn tại của cái đèn đó và next sang cái đèn tiếp theo.

abstract class ITrafficLightState {
  void nextTrafficLight(TrafficLightContext context);
  Color getColor();
  Duration getDuration();
}

Để dễ dàng tưởng tượng hơn, ta khai báo lớp context trước, ở đây là TrafficLightContext, sẽ sử dụng interface ITrafficLightState để thay đổi trạng thái bằng hàm setState(state) và có thể chuyển được sang trạng thái tiếp theo. À cũng đừng quên lấy ra currentColor và cả currentDuration nữa nhé. Tổng quan ta sẽ được như sau:

class TrafficLightContext {
  ITrafficLightState _state;

  TrafficLightContext(this._state);

  Color get currentColor => _state.getColor();
  Duration get currentDuration => _state.getDuration();

  void setState(ITrafficLightState state) {
    _state = state;
  }

  void nextTrafficLight() {
    _state.nextTrafficLight(this);
  }
}

Tiếp đến, ta xây dựng các concreteStates: đèn xanh, đèn vàng và đèn đỏ. Ở mỗi state, nó sẽ biết mình là ai và sẽ chuyển sang state nào mong muốn:

GreenState

class GreenState extends ITrafficLightState {
  @override
  void nextTrafficLight(TrafficLightContext context) {
    context.setState(YellowState());
  }

  @override
  Color getColor() {
    return Colors.green;
  }

  @override
  Duration getDuration() {
    return const Duration(seconds: 15);
  }
}

RedState

class RedState implements ITrafficLightState {
  @override
  void nextTrafficLight(TrafficLightContext context) {
    context.setState(GreenState());
  }

  @override
  Color getColor() {
    return Colors.red;
  }

  @override
  Duration getDuration() {
    return const Duration(seconds: 20);
  }
}

YellowState

class YellowState extends ITrafficLightState {
  @override
  void nextTrafficLight(TrafficLightContext context) {
    context.setState(RedState());
  }

  @override
  Color getColor() {
    return Colors.yellow;
  }

  @override
  Duration getDuration() {
    return const Duration(seconds: 3);
  }
}

Cuối cùng, là phần hiển thị: Để đơn giản hoá việc hiển thị, ta cần tạo 1 hình tròn chứa màu của đèn giao thông và hiển thị ra thời gian tồn tại của đèn hiện tại. Như mã nguồn ở trên, đèn xanh là hết 15 giây sẽ chuyển sang đèn vàng 3 giây, hết 3 giây đó sẽ chuyển sang đèn đỏ là 20 giây.

@RoutePage()
class StatePage extends StatefulWidget {
  const StatePage({super.key});

  @override
  State<StatePage> createState() => _StatePageState();
}

class _StatePageState extends State<StatePage> {
  late TrafficLightContext _trafficLight;
  late Timer _timer;
  late int _currentTime = 0;

  @override
  void initState() {
    super.initState();
    _trafficLight = TrafficLightContext(GreenState());
    _startTimer();
  }

  @override
  void dispose() {
    super.dispose();
    _timer.cancel();
  }

  void _startTimer() {
    _currentTime = _trafficLight.currentDuration.inSeconds;

    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        _currentTime--;

        if (_currentTime == 0) {
          _trafficLight.nextTrafficLight();
          timer.cancel();
          _startTimer();
        }
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: const PrimaryAppBar(
        title: 'State',
      ),
      body: SafeArea(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(
              width: 200,
              height: 200,
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: _trafficLight.currentColor,
              ),
            ),
            const SizedBox(height: 20),
            Center(
              child: Text(
                _currentTime.toString(),
                style: const TextStyle(
                  fontSize: 30,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Thành quả

Như vậy, chúng ta đã tạo ra một cột đèn giao thông hàng "pha ke" rep 1:1 rồi. Hi vọng với ví dụ này bạn có thể hiểu được ý tưởng sơ qua của mẫu thiết kế State.

Về source code mình đã up lên github, mọi người có thể vào tham khảo và cho mình xin một up-vote ạ ^^

Tổng kết

Thật tuyệt vời nếu bạn đã đi được đến đây với mình. Cảm ơn các bạn và cùng mình đón chờ một mẫu thiết kế mới toanh. Design Pattern cùng Flutter. Tập 8: Facade - "Thống nhất đất nước"


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í