[Design Patterns #4] Adapter Pattern

Bài toán cần giải quyết

Giả sử chúng ta đang phát triển một ứng dụng trên nền tảng web, ứng dụng bao gồm 2 phần chính là UI (User Interface) và Back-end (chịu trách nhiệm xử lý về logic, đọc / ghi database, đưa dữ liệu lên cho UI hiển thị). Phần Back-end chúng ta đang sử dụng được phát triển bởi một công ty khác, tạm gọi là công ty A đi.

Vào một ngày xấu trời, CTO – Giám đốc công nghệ của công ty thông báo rằng phần Back-end mà công ty A phát triển có nhiều lỗ hổng về security, do đó công ty quyết định chúng ta phải chuyển sang dùng Back-end của công ty B. Và có một vấn đề xảy ra, đó là các Interfaces của Back-end A và Back-end B khác nhau (khác nhau về function name, về danh sách parameters, kiểu dữ liệu của paramaters) do đó nếu muốn dùng Back-end B thì chúng ta đang đứng trước nguy cơ phải sửa rất nhiều code.

Ae nào cần xem lại khái niệm interface trong OOP thì xem lại bài này nhé: https://www.cppdeveloper.com/tutorial/tinh-truu-tuong-trong-lap-trinh-huong-doi-tuong/

Nhìn hình bên dưới là có thể tưởng tượng ra được tình huống hiện tại, UI đang lắp vừa khít vào Back-end A yên ổn, tự nhiên đòi chuyển sang Back-end B. Giờ mà không sửa code UI hiện thì lắp vào Back-end B thế méo nào được.

Giải quyết vấn đề bằng Adapter Pattern

Adapter Pattern cho phép bạn sửa interface giữa các object hoặc các class mà không phải sửa đổi trực tiếp các object hoặc class.

Trong tình huống này, Adapter Pattern sẽ là giải pháp cho chúng ta. Chúng ta sẽ dùng Adapter Pattern để chuyển đổi interface của Back-end B sang interface của Back-end A, điều đó sẽ giúp cho UI có thể làm việc được với Back-end B mà không cần phải sửa code (hoặc sửa rất ít). Ta cần tạo một adapter ở giữa để giúp cho UI có thể làm việc được với Back-end B. Adapter này cũng tương tự như adpater chuyển từ ổ cắm tròn sang ổ cắm dẹt, giúp cho phích cắm đầu tròn có thể nối được vào ổ cắm dẹt (Hình dưới)

Cụ thể hơn, giả sử UI đang có một class tên là User để xử lý thông tin về người dùng. Do UI đang làm việc với Back-end A nên class User lúc đó đang tuân theo interface được đưa ra bởi Back-end A, cụ thể một đối tượng User cần phải có 2 hàm là setNamegetName để đọc và ghi tên người dùng. Nhưng khi chuyển sang Back-end B thì do Back-end B xử lý tên người dùng theo một cách khác nên interface mà Back-end B đưa ra cho một đối tượng User phải tuân theo cũng khác. Back-end B chia tên người dùng thành “first name” và “last name” và expect đối tượng User phải cung cấp 4 hàm để đọc ghi: setFirstName, setLastName, getFirstName, getLastName.

Bây giờ việc của chúng ta là làm thế nào để Back-end B vẫn có thể sử dụng đối tượng của class User (mà UI đang có) làm input để xử lý. Để làm được việc đó, chúng ta sẽ tạo ra một adapter như sau →

*** Class diagram

*** Interface mà Back-end A quy định cho đối tượng user

*** Class User hiện tại đang implement BackendAUserInterface như sau

*** Interface mà Back-end B quy định cho đối tượng user

*** Giả sử có class UserB implement BackendBUserInterface như sau

Nếu chúng ta sử dụng class UserB thay cho User để làm việc với Back-end B thì quá đơn giản, câu chuyện chả còn gì để nói và chúng ta sẽ phải sửa code của UI ở tất cả chỗ nào đang dùng User, chuyển sang dùng UserB. Những chỗ nào đang call hàm của User thì phải chuyển sang call hàm của UserB. Nhưng chúng ta đang không muốn nông dân như vậy, chúng ta muốn làm cái gì đó thông minh hơn, không cần sửa code UI mà vẫn dùng được các đối tượng của class User cùng với Back-end B. Mình đưa ra code sample của UserB để giải thích cho các bạn dễ hiểu hơn về cái mà Back-end B đang mong muốn, chúng ta sẽ không dùng class này ở đây. Vì vậy, bước tiếp theo là phải tạo cái adapter để Back-end B có thể sử dụng được các đối tượng của class User.

*** Tạo adapter

Adapter này sử dụng mối quan hệ composition lưu đối tượng cần được adapt, đó chính là đối tượng của class User, đối tượng này sẽ được truyền vào cho adapter thông qua hàm khởi tạo.

Sự khác biệt giữa các đối tượng UserUserB là các đối tượng User lưu trữ tên người dùng thành một chuỗi duy nhất, trong khi các đối tượng UserB lưu first name và last name riêng. Để chuyển đổi giữa các đối tượng UserUserB, thì tên của người dùng chứa trong đối tượng User cần phải được tách ra thành 2 phần là first name và last name. Sau khi tách được first name và last name thì có thể coi đối tượng adapter bây giờ tương đương với đối tượng của class UserB (vì cùng implement BackendBUserInterface) →

*** Test adapter

Chạy đoạn chương trình test này sẽ cho ra kết quả như sau →

*** Quay lại bài toán ban đầu thì như vậy sau khi tạo đối tượng adapter, chúng ta có thể truyền nó vào cho Back-end B. Back-end B chỉ cần biết là nó đang làm việc với một đối tượng của class đã implement interface mà nó đưa ra là BackendBUserInterface mà không hề biết đến sự tồn tại của User hay BackendAUserInterface. Bằng cách đó chúng ta sẽ không phải sửa quá nhiều code để chuyển từ Back-end A sang làm việc với Back-end B.

Và trong tương lai nếu có chuyển sang dùng Back-end C, Back-end D, … thì cũng chỉ cần tạo class adapter mới và thay thế adapter mà thôi.

— Phạm Minh Tuấn (Shun) —