+9

Design Pattern cùng Flutter. Tập 6: Strategy - "Chiến lược toàn năng"

Giới thiệu

Khi bạn muốn có nhiều thuật toán riêng của bạn và muốn thay đổi theo runtime.

Khi bạn muốn code của bạn không phải if else hay switch case tùm lum thứ.

Khi bạn muốn tách biệt thuật toán và tuân theo nguyên lý của SOLID (ở đây là S, OL).

Khi bạn muốn sử dụng lại các thuật toán khác.

Đúng rồi, đây cũng là dấu hiệu mà bạn nên sử dụng mẫu thiết kế Strategy.

Vậy...

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

Strategy là một loại mẫu thiết kế thuộc behavioural được ra đời khi bạn thấy dấu hiệu như trên. Nó là một trong những loại mẫu thiết kế phổ biến nhất trong thiết kế phần mềm, liên quan đến sự linh hoạt, khả năng bảo trì và tính mở rộng của hệ thống.

Thế...

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

Mẫu thiết kế Strategy ra đời với mục tiêu sau:

  1. Xác định một tập hợp các đối tượng đại diện cho các thuật toán cần thực hiện, thường là những đối tượng xử lý phức tạp.
  2. Cho phép thay đổi linh hoạt các thuật toán mà không cần thay đổi lớp sử dụng.
  3. Tuân thủ nguyên tắc của SOLID:
  • S: Việc tách biệt thuật toán làm mỗi class chỉ chịu một trách nhiệm duy nhất.
  • O: Dễ dàng thêm thuật toán mới mà không phải sửa mã nguồn hiện có.
  • L: Các thuật toán có thể dễ dàng hoán đổi lẫn nhau mà không làm thay đổi tính đúng đắn của chương trình.

Strategty Class Diagram

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

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

  • Strategy: Interface hoặc abstract class đại diện cho một lớp thành phần, định nghĩa ra hàm dùng chung cho các thuật toán.
  • ConcreteStrategies: Là lớp kế thừa Strategy biểu diễn các thuật toán cụ thể, được sử dụng bởi lớp Context.
  • Context: Tham chiếu đến lớp Strategy và sử dụng phụ thuộc vào các ConcreteStrategies.

Ứng dụng

Mẫu thiết kế Strategy rất hữu ích với trường hợp muốn đóng gói tập hợp các thuật toán và có thể dễ dàng hoán đổi lẫn nhau. Ngoài ra, bạn không muốn dùng các condition như if-else và switch-case để bớt tăng độ khó cho chính mình và cả người maintain sau đó. Một nguyên tắc chung, nếu thấy nhiều hành vi khác nhau gộp chung trong một class (sử dụng condition) thì đây là dấu hiệu dễ nhận biết nhất nên sử dụng Strategy để đóng gói các logic xử lý vào các lớp thuật toán riêng biệt.

Thực hành

Tưởng tượng rằng ta sẽ có nhiều màn hình khác nhau, nhưng khi chuyển hướng sang mỗi màn hình thì chỉ một hiệu ứng chuyển trang mặc định. Nếu lỡ một ngày, khách hàng muốn screen 1 chuyển trang với hiệu ứng khác, screen 2 hiệu ứng khác để tăng sự màu mè hơn, ta cũng phải đáp ứng được, nếu không thì sẽ bị gõ cho to đầu mất 😄 . Ví dụ ta có hai hiệu ứng chuyển màn hình là: FadeTransitionSlideTransition ta sẽ phải implement sao cho dễ dàng mở rộng nhất. Để làm được điều đó, ta thử xài Strategy xem có ổn không nhé.

Theo class diagram tên, đầu tiên ta phải tạo một interface strategy là PageTransitionStrategy và có sẵn phương thức routeTransitionsBuilder để những class stategies khác chỉ việc kế thừa xây dựng logic riêng của nó.

abstract class PageTransitionStrategy {
  RouteTransitionsBuilder routeTransitionsBuilder();
}

Lưu ý: Định nghĩa hàm RouteTransitionsBuilder sẽ được khai báo như sau

typedef RouteTransitionsBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child);

Tiếp đến, ta xây dựng các concreteStrategies, cụ thể là: FadeTransitionStrategySlideTransitionStrategy

class FadeTransitionStrategy implements PageTransitionStrategy {
  @override
  RouteTransitionsBuilder routeTransitionsBuilder() {
    return (context, animation, secondaryAnimation, child) {
      return FadeTransition(
        opacity: animation,
        child: child,
      );
    };
  }
}
class SlideTransitionStrategy implements PageTransitionStrategy {
  @override
  RouteTransitionsBuilder routeTransitionsBuilder() {
    return (context, animation, secondaryAnimation, child) {
      return SlideTransition(
        position: Tween<Offset>(
          begin: const Offset(1.0, 0.0),
          end: Offset.zero,
        ).animate(animation),
        child: child,
      );
    };
  }
}

Không thể thiếu là một context để sử dụng strategy và một hàm trả về RouteType

class RouteTypeContext {
  final PageTransitionStrategy transitionStrategy;

  RouteTypeContext({
    required this.transitionStrategy,
  });

  RouteType routeType() {
    return RouteType.custom(
      transitionsBuilder: ((context, animation, secondaryAnimation, child) =>
          transitionStrategy.routeTransitionsBuilder()(
            context,
            animation,
            secondaryAnimation,
            child,
          )),
    );
  }
}

Tiếp đến, ta khai báo các transition ứng với mỗi context

final fadeTransition = RouteTypeContext(transitionStrategy: FadeTransitionStrategy());
final slideTransition = RouteTypeContext(transitionStrategy: SlideTransitionStrategy());

Và sử dụng nó để tạo hiệu ứng chuyển trang

AutoRoute(
        page: StrategyRoute.page,
        children: [
          AutoRoute(
            type: fadeTransition.routeType(),
            page: FadeTransitionStrategyRoute.page,
            initial: true,
          ),
          AutoRoute(
            type: slideTransition.routeType(),
            page: SlideTransitionStrategyRoute.page,
          ),
        ],
),

Cuối cùng, là phần hiển thị:

class StrategyPage extends StatelessWidget {
  const StrategyPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: const PrimaryAppBar(
        title: 'Strategy',
      ),
      body: SafeArea(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            ElevatedButton(
              onPressed: () {
                context.router.push(const FadeTransitionStrategyRoute());
              },
              child: const Text('Fade Transition'),
            ),
            const SizedBox(height: 30),
            ElevatedButton(
              onPressed: () {
                context.router.push(const SlideTransitionStrategyRoute());
              },
              child: const Text('Slide Transition'),
            ),
            const SizedBox(
              height: 300,
              child: AutoRouter(),
            ),
          ],
        ),
      ),
    );
  }
}

Thành quả

Như vậy, chúng ta đã tạo ra hai hiệu ứng chuyển trang để sử dụng, việc tạo thêm một hiệu ứng chuyển trang khác thì cũng không ảnh hưởng gì đến code cũ cả, tuy hơi rườm rà nhưng sẽ tiện cho sau này rất nhiều.

Lưu ý: Ví dụ trên có sử dụng thư viện auto_route để dễ dàng navigate, các bạn có thể vào đọc thêm ở link này nha

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

Vậy là đã xong một mẫu thiết kế mới nữa, mình tin chắc rằng nếu bạn đã theo dõi tới đây thì sẽ thấy mẫu thiết kế không còn khô khan như trước đây nữa, càng ngày sẽ thấy thú vị hơn. Tập này đến đây là kết thúc, mời các bạn đón chờ Design Pattern cùng Flutter. Tập 7: State - "Siêu nhân biến hình"


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í