+6

Design Pattern cùng Flutter. Tập 3: Adapter - "Bộ chuyển đổi diệu kỳ"

Giới thiệu

Một ngày đẹp trời, cái điện thoại Android đời tống của bạn không còn cử động được nữa, đành phải đi "mượn luôn" con IPhone 15 Promax 1TB full chức năng của thằng bạn để dùng. Hí hửng đập định cắm con tai nghe đời tống của mình vào để nghe nhạc, nhưng không, lỗ cắm không phù hợp, bỏ thì không có tiền mua cái mới mà vương thì không sử dụng được, thế là buồn nguyên ngày mới chợt nhớ ra thằng bạn nó có cổng chuyển đổi từ jack cắm 3.5mm sang jack cắm lightning của Iphone, nên cũng đã đi "mượn luôn" của nó về mà dùng luôn. Thế là đã có thể sử dụng cái tai nghe cũ của mình mà cũng không cần phải mua mới. Quá là tiện!!!

Cổng chuyển đổi jack cắm 3.5mm sang jack cắm lightning

Cổng chuyển đổi jack cắm 3.5mm sang jack cắm lightning
Vậy ra, cổng chuyển đổi đã đóng vai trò là một **Adapter** đứng ở giữa để kết nối 2 thằng không liên quan gì đến cuộc đời nhau mà lại có thể gắn kết lại với nhau và hoạt động một cách rất *mượt* và *trơn tru*.

Hiểu nôm na là thế, vậy Adapter nó là ai, địa chỉ nhà ở đâu?

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

Adapter là một loại mẫu thiết kế thuộc structural được ra đời nhằm kết hợp những interface của class không tương thích với nhau thành interface của class mà mình mong muốn.

Ví dụ khác:

Xe chuyên chở ô tô trên đường ray (https://refactoring.guru/)

Xe chuyên chở ô tô trên đường ray

Với hình ảnh trên, ta thấy để một chiếc xe oto mà có thể vi vu trên đường ray, thì cần một "Xe chuyên chở ô tô trên đường ray" mới có thể di chuyển được.

Ý tưởng chính của mẫu thiết kế này là làm cho những thứ không tương thích với nhau có thể cộng tác và đưa ra một kết quả mà client yêu cầu .

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

Mẫu thiết kế Adapter ra đời với hai mục tiêu chính:

  1. Chuyển đổi interface của một lớp sang interface của lớp khác mà client yêu cầu.
  2. Gắn kết các interface với nhau và làm việc cùng nhau mà không phá vỡ quy tắc của nó.

Adapter Class Diagram

Cách tiếp cận tổng quát của Adapter được biểu diễn bằng sơ đồ lớp bên dưới. Bao gồm 2 loại: Object AdapterClass Adapter:

Object Adapter

Object Adapter

Class Adapter

Class Adapter

Tuy có hai cách triển khai Adapter đó là: Object AdapterClass Adapter (ta sẽ đi cụ thể hơn vào phần tiếp) nhưng về thành phần tham gia thì khá giống nhau:

Ta cùng điểm qua vài điểm chính của 2 sơ đồ này nhé:

  • Target hoặc ITarget: Định nghĩa ra interface mang chức năng chính mà Application sử dụng.
  • Adaptee: Định nghĩa interface đã tồn tại thực tế cần chuyển đổi (ví dụ: thư viện bên thứ 3).
  • Adapter: Đóng vai trò chuyển đổi interface từ Adaptee thành Target interface. Ở đây, chứa những yêu cầu của Application để đưa ra mà Adaptee hiểu và cũng chính là thành phần chính trong mẫu thiết kế Adapter.

Object adapter vs Class adapter

Với Adapter, ta có hai cách triển khai hợp lệ là: Object AdapterClass Adapter. Đối với Object Adapter thì thành phần chính là Adapter sẽ implement interface Target và sẽ reference với Adaptee class để sử dụng hàm liên quan. Còn Class Adapter thì thành phần chính là Adapter sẽ kế thừa Target class và Adaptee class, bằng cách này thì specificOperation của Adaptee sẽ được gọi trong operation của Target. Vậy câu hỏi đặt ra là nên sử dụng loại Adapter nào?

  • Một trong những điểm mạnh của Class Adapter là có thể override lại method của Adaptee, nhưng vì nó chỉ có thể kế thừa duy nhất Adaptee class nên không thể nào linh hoạt bằng Object Adapter với composition, nghĩa là, có thể sử dụng các adaptees của Adaptee class miễn là phù hợp với mục đích mà Adapter yêu cầu. Đây cũng chính là sự linh hoạt với composition để tiếp cận chữ L của nguyên lý SOLID (Trong một chương trình, các object của class con có thể thay thế class cha mà không làm thay đổi tính đúng đắn của chương trình) (1).
  • Hiện nay, một số ngôn ngữ không hỗ trợ đa kế thừa. Đối với ngôn ngữ Dart, nó cũng vậy nên việc sử dụng Class Adapter là không khả thi (2).

=> Từ (1) và (2) suy ra, ta nên sử dụng Object Adapter

Ứng dụng

Việc ứng dụng Adapter cũng có nhiều trường hợp:

  • Trường hợp hệ thống có các class cũ cần nâng cấp, có chứa những method quan trọng. Ta nên ứng dụng Apdater để tích hợp những thành phần hiện có sang class mới mà không cần phải sửa lại class cũ.
  • Nếu sử dụng interface của bên thư viện thứ ba mà không phù hợp với mục đích hiện tại. Hoặc nguồn dữ liệu từ APIs mà bạn không muốn liên quan đến business logic của chương trình. Mẫu thiết kế cũng tiện ích nếu sử dụng nhiều adaptees từ nhiều nguồn APIs khác nhau nhưng chung một mục đích, hoặc có thể từ nhiều adapters khác nhau với nhiều nguồn APIs khác nhau. Đoạn này thấy nhức nhức cái đầu rồi đúng không? Ta vào ví dụ thực hành nó để dễ tưởng tượng hơn nhé.

Thực hành

Ta sẽ đi vào ví dụ muốn lấy danh sách những người liên lạc để hiển thị ra ứng dụng. Tuy nhiên, chúng ta lại có 2 nguồn thu thập dữ liệu Contact với format khác nhau là: JSON và XML, kèm theo mong muốn hiển thị một cách tổng quát nhất thì không thể đưa dữ liệu cụ thể của từng nguồn dữ liệu vào được. Việc của chúng ta là chỉ hiển thị dữ liệu Contact, còn ta không quan tâm đến dữ liệu đó được lấy từ nguồn nào, cách lấy ra làm sao, trả về dữ liệu gì. Do đó, thứ ta cần là một mẫu thiết kế có thể trả về danh sách của những người liên lạc lấy từ 2 nguồn dữ liệu khác nhau mà không cần quan tâm nó sẽ lấy như thế nào. Và ứng cử viên số 1 của lần này sẽ là mẫu thiết kế Adapter.

Đầu tiên, ta nhìn vào sơ đồ tổng quan:

Nhìn vào sơ đồ, ta thấy có 2 cách lấy dữ liệu tương ứng với AdapteeXMLContactAPIAdapteeJSONContactAPIAdaptee sẽ trả về thông tin liên lạc khác nhau. IContactAdapter là một abstract class chứa hàm thực thi là getContacts, JSONContactAdapter class và XMLContactAdapter class sẽ implement hàm bắt buộc từ IContactAdapter. JSONContactAdapter sẽ chứa hàm getContacts và sử dụng JSONContactAPIAdaptee để parse dữ liệu từ nguồn JSON, tương tự, XMLContactAdapter cũng chứa hàm getContacts và sử dụng JSONContactAPIAdaptee để parse dữ liệu từ nguồn XML. Cuối cùng, dù từ nhiều nguồn lấy dữ liệu Contact khác nhau, nhưng thứ ta cần cuối cùng là class Contact được sử dụng trong ContactSection là được.

Contact

Lớp chứa thông tin liên lạc, được sử dụng trong giao diện để hiển thị.

class Contact {
  String name;
  String email;
  String phone;
  String portfolioLink;
  bool isFavorite;


  Contact({
    this.name = '',
    this.email = '',
    this.phone = '',
    this.portfolioLink = '',
    this.isFavorite = false,
  });
}

JSONContactData và XMLContactData

Lớp chứa thông tin liên lạc khi lấy dữ liệu từ nguồn JSON và XML. JSONContactData từ nguồn JSONContactAPIAdaptee sẽ có trường email mà không có trường phone, còn XMLContactData từ nguồn JSONContactAPIAdaptee sẽ có trường phone mà không có trường email.

class JSONContactData {
  String name;
  String email;
  String portfolioLink;
  bool isFavorite;

  JSONContactData({
    this.name = '',
    this.email = '',
    this.portfolioLink = '',
    this.isFavorite = false,
  });
}

class XMLContactData {
  String name;
  String phone;
  bool isFavorite;

  XMLContactData({
    this.name = '',
    this.phone = '',
    this.isFavorite = false,
  });
}

IContactAdapter

Là abstract class , chứa hàm thực thi ràng buộc, cụ thể là chứa getContacts để lấy về danh sách liên hệ, sẽ trả về List< Contact >.

abstract class IContactAdapter {
  Future<List<Contact>> getContacts();
}

JSONContactAdapter và XMLContactAdapter

Là các Adapter class dùng để gọi các Adaptee tương ứng và trả về dữ liệu sau khi đã fetch. Lưu ý là ở đây cũng có sử dụng một mẫu thiết kế Adapter nhỏ để mapper các dữ liệu từ JSONContactDataXMLContactData thành dữ liệu của Contact tương ứng

class JSONContactAdapter implements IContactAdapter {
  const JSONContactAdapter({
    this.adaptee = const JSONContactAPIAdaptee(),
  });

  final JSONContactAPIAdaptee adaptee;

  @override
  Future<List<Contact>> getContacts() async {
    final data = await adaptee.fetchJSONContacts();

    return data
        .map(
          (e) => Contact(
            name: e.name,
            email: e.email,
            portfolioLink: e.portfolioLink,
            isFavorite: e.isFavorite,
          ),
        )
        .toList();
  }
}
class XMLContactAdapter implements IContactAdapter {
  const XMLContactAdapter({
    this.xmlContactAPIAdaptee = const XMLContactAPIAdaptee(),
  });

  final XMLContactAPIAdaptee xmlContactAPIAdaptee;

  @override
  Future<List<Contact>> getContacts() async {
    final data = await xmlContactAPIAdaptee.fetchXMLContacts();

    return data
        .map(
          (e) => Contact(
            name: e.name,
            phone: e.phone,
            isFavorite: e.isFavorite,
          ),
        )
        .toList();
  }
}

ContactSection

Là một widget để hiển thị UI danh sách liên lạc theo từng loại khác nhau, ở đây ta sẽ sử dụng một type IContactAdapter để có thể tải dữ liệu từ nhiều nguồn khác nhau, nó cũng sẽ chỉ quan tâm đến type khác nhau chứ không cần quan tâm đến cách lấy dữ liệu từ nguồn đó như thế nào.

class ContactSection extends StatefulWidget {
  const ContactSection({
    required this.contactAdapter,
    this.title = '',
    super.key,
  });

  final IContactAdapter contactAdapter;
  final String title;

  @override
  State<ContactSection> createState() => _ContactSectionState();
}

class _ContactSectionState extends State<ContactSection> {
  bool isShowContacts = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(widget.title, style: const TextStyle(fontWeight: FontWeight.bold)),
        ElevatedButton(
          onPressed: () {
            setState(() {
              isShowContacts = !isShowContacts;
            });
          },
          child: Text(isShowContacts ? 'Hide contacts' : 'Show contacts'),
        ),
        if (isShowContacts)
          FutureBuilder(
            future: widget.contactAdapter.getContacts(),
            builder: (context, snapshot) {
              final data = snapshot.data;

              if (data == null) {
                return const Center(child: CircularProgressIndicator());
              }

              return ListView.separated(
                shrinkWrap: true,
                itemCount: data.length,
                separatorBuilder: (context, index) => const SizedBox(height: 10),
                itemBuilder: (context, index) {
                  final name = data[index].name;
                  final email = data[index].email;
                  final phone = data[index].phone;
                  final portfolio = data[index].portfolioLink;

                  const nameStyle = TextStyle(
                    color: Colors.black,
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                  );
                  const moreInfoStyle = TextStyle(
                    color: Colors.grey,
                    fontSize: 12,
                  );

                  return Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        if (name.isNotEmpty) Text('Họ và tên: $name', style: nameStyle),
                        if (email.isNotEmpty) Text('Email: $email', style: moreInfoStyle),
                        if (phone.isNotEmpty) Text('Số điện thoại: $phone', style: moreInfoStyle),
                        if (portfolio.isNotEmpty)
                          Text('Liên kết: $portfolio', style: moreInfoStyle),
                      ],
                    ),
                  );
                },
              );
            },
          ),
      ],
    );
  }
}

AdapterPage

Cuối cùng là AdapterPage, ta sẽ gọi các ContactSection tương ứng với từng loại như sau:

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

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      appBar: PrimaryAppBar(
        title: 'Adapter',
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              ContactSection(
                contactAdapter: JSONContactAdapter(),
                title: 'Contacts from JSON API',
              ),
              SizedBox(height: 20),
              ContactSection(
                contactAdapter: XMLContactAdapter(),
                title: 'Contacts from XML API',
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Thành quả

Và cuối cùng, tadaaa, đây là thành quả mà ta nhận được:

Tuy là phần cấu tạo có phần rườm rà và theo quy chuẩn, nhưng nó lại rất tiện để có thể mở rộng đúng không nào ^^.

Các bạn có thể xem lại bằng github này nhé.

Tổng kết

Đi đến đây, chắc bạn cũng đã hiểu được sự hữu ích mà mẫu thiết kế Adapter mang lại. Ở series tiếp theo, ta sẽ đi vào một mẫu thiết kế mới Tập 4: Template Method - "Quy trình tạo nên chìa khoá"


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í