Search…

Design Pattern: Adapter Pattern

08/10/202013 min read
Bài viết đề cập đến một Design Pattern được sử dụng rất phổ biến trong lập trình hướng đối tượng, đó là Adapter Pattern.

Quá trình phát triển của tư duy lập trình cũng tương tự như quá trình phát triển của loài người, đi từ thấp lên cao. Ban đầu là lập trình tuần tự, sau đó là lập trình cấu trúc, rồi lập trình thủ tục, và giờ đây là lập trình hướng đối tượng.

Trải qua 1 thời kì tư duy lập trình hướng đối tượng trở nên phổ biến, những người lập trình tiên phong đã tìm ra các vấn đề chung trong hướng đối tượng và nghĩ ra các giải pháp để giải quyết các vấn đề đó 1 cách tối ưu trong phần lớn các trường hợp, và những người đó được biết đến dưới cái tên Gang of Four (GoF), những người đã lần đầu tạo nên khái niệm design pattern qua cuốn sách “Design Pattern: A Element of reusable object oriented software”.

Bài viết đề cập đến 1 design pattern được sử dụng rất phổ biến trong lập trình hướng đối tượng, đó là Adapter Pattern.

Tổng quan

Lập trình hướng đối tượng là 1 phương pháp tư duy lập trình mới (so với lập trình hướng thủ tục và lập trình hướng cấu trúc) có thể giải quyết tốt hơn các bài toán trong thực tế nhờ khả năng trừu tượng hóa các đối tượng ngoài thực tế vào chương trình thông qua khái niệm “Lớp” và “Đối tượng”.

Khi lập trình hướng đối tượng ra đời thì khái niệm “cấu trúc chương trình” được đẩy lên 1 tầm cao mới. Chương trình không còn là 1 sự thực thi tuần tự từng câu lệnh, hay sự gọi tuần tự các hàm, mà giờ đây, nó là “các hành động của đối tượng” và “sự tương tác giữa các đối tượng”. 

Trong lập trình hướng đối tượng, 1 khái niệm cực kì quan trọng đó là “giao diện của lớp” (không phải kiểu interface của Java hay C#). “Giao diện của lớp” biểu thị những “tính chất” bên ngoài của lớp, gồm các hành động của lớp (method) và các thuộc tính của lớp (attribute/property). Cách duy nhất tác động vào 1 đối tượng là phải thông qua “giao diện của nó” (ví dụ: khi muốn đối tượng Cat thực hiện hành động Run thì ta phải thông qua giao diện của lớp Cat, giao diện của nó phải có 1 phương thức là Run(), khi đó thì ta sẽ gọi Cat.Run()).

Có thể coi người sử dụng đối tượng có 2 loại:

  • Người định nghĩa (Người định nghĩa ra đối tượng đó).
  • Người sử dụng (Người sử dụng đối tượng, gọi các phương thức của đối tượng đó).

Nếu đứng ở góc nhìn của “người sử dụng đối tượng”, chỉ có 2 điều ta cần phải quan tâm, 1 là giao diện của đối tượng là gì, và 2 là đối tượng đó thực hiện những hành động gì.

  • Quan tâm tới “hành động của đối tượng là gì” vì đó là lý do dùng đến đối tượng đó.
  • uan tâm tới "giao diện của đối tượng" vì phải biết giao diện của nó mới có thể tương tác được với nó (phải biến đối tượng có những phương thức nào mới có thể gọi, tương tự như khi sử dụng 1 lớp thì cần phải biết lớp đó có những thuộc tính, phương thức nào).

Nếu coi 1 đối tượng trong lập trình là 1 đối tượng ngoài đời thực thì giao diện của đối tượng sẽ là vẻ bề ngoài của đối tượng (attribute/property) và các hành vi cử chỉ của đối tượng đó (method).

Tầm quan trọng của giao diện

Giao diện của lớp dùng để tác động vào đối tượng. Vì lẽ đó “giao diện của lớp” là 1 khái niệm rất quan trọng, nó ảnh hưởng rất lớn đến cấu trúc của chương trình cũng như khả năng bảo trì và nâng cấp của chương trình.

Khi ta nói đến cấu trúc của 1 cái gì đó, thì nghĩa là đang nói đến sự sắp đặt, trật tự cũng như “giao diện” (bề ngoài) của các thành phần trong nó. Để những thành phần có thể được ghép nối lại với nhau thì nó phải có “giao diện” tương đồng nhau (có thể ghép nối được). Do vậy, khi muốn chỉnh sửa hay nâng cấp 1 thành phần trong cấu trúc của 1 cái gì đó, ta phải bảo toàn “giao diện” của nó (để nó vẫn có thể ghép vào được các thành phần cũ).

Trong lập trình phần mềm và hệ thống cũng như vậy, cấu trúc của 1 chương trình là sự sắp đặt, trật tự cũng như giao diện của các đối tượng trong chương trình. Khi muốn chỉnh sửa hay nâng cấp 1 thành phần nào đó của chương trình (đối tượng), để không phải chỉnh sửa thêm các thành phần khác nữa, thì đối tượng được chỉnh sửa/nâng cấp phải bảo toàn được giao diện ban đầu của nó. Đó là lý do giao diện là 1 khái niệm rất quan trọng đối với cấu trúc của 1 chương trình. Nếu chương trình có cấu trúc tốt, thì nếu bị 1 lỗi nào đó, dù lớn, chỉ cần chỉnh sửa 1 thành phần/đối tượng nào đó, còn không phải rà soát lại toàn bộ source code của chương trình để chỉnh sửa lỗi dù lỗi đó có nhỏ đến đâu.

Đặt vấn đề

Có bao giờ gặp phải trường hợp phải port 1 game hay ứng dụng từ nền tảng này sang nền tảng khác?

Đó là 1 điều không phải hiếm đối với ngày nay, khi mà sự đa dạng các nền tảng phát triển cũng như nền tảng chạy ứng dụng được tăng nhanh hơn bao giờ hết. Và đây cũng là việc thường phải đối mặt khi làm trong công ty, tham gia các cuộc thi hay ngay khi đang ngồi trên ghế nhà trường loay hoay với các đồ án. Vậy việc port 1 ứng dụng dễ hay khó? Nó dễ nếu biết cách làm, còn là 1 công việc cực kì nhức óc nếu không theo bất kì phương pháp nào.

Ban đầu phương pháp nghĩ đến là viết lại ứng dụng đó trên nền tảng khác, vì 2 nền tảng khác nhau thì không thể dùng lại code viết trên nền tảng này xài cho nền tảng kia được? Nhưng có 1 điều khá là hiển nhiên nếu để ý, phương pháp đầu tiên đối mặt với 1 vấn đề hoàn toàn mới sẽ là 1 phương pháp tổng quát, và cũng là phương pháp tồi tệ, nhức óc và tốn nhiều công sức nhất.

Vậy đâu là phương pháp tốt nhất trong trường hợp này? Hiển nhiên là phải làm sao có thể dùng lại những đoạn mã nguồn đã viết. Mã nguồn của 1 chương trình hay game có thể chia làm 2 thành phần: thành phần 1 là framework, thành phần 2 là mã nguồn. Vì thành phần thứ 2 phụ thuộc hoàn toàn vào framework bên dưới, nên khi thay framework thì mã nguồn của thành phần thứ 2 hoàn toàn không còn hoạt động được nữa. Vậy cách giải quyết cho vấn đề này rất đơn giản: tạo cho framework 1 giao diện giống hệt framework cũ!

"Giả sử muốn port game từ XNA sang Java. Bên XNA dùng đối tượng SpriteBatch để vẽ, thì qua bên Java cũng tạo 1 lớp SpriteBatch có giao diện hoàn toàn giống với lớp này bên XNA, như vậy tất cả đoạn code vẽ hình bằng đối tượng SpriteBatch trong mã nguồn sẽ chạy được!"

Và đó cũng chính là 1 mẫu thiết kế trong design patterns: Adapter Pattern – chuyển đổi giao diện ban đầu của 1 lớp thành 1 giao diện phù hợp.

Adapter Pattern

Design Pattern là gì?

Design pattern là các mẫu hay mô hình trong thiết kế phần mềm/hệ thống giúp giải quyết các vấn đề thường gặp trong lập trình. Design pattern cung cấp cách thiết kế chương trình để giải quyết tối ưu các bài toán hay gặp phải trong lập trình như: khởi tạo đối tượng, cấu trúc chương trình, và hành động của các đối tượng…

Adapter trong thực tế

Adapter là 1 khái niệm rất thông dụng trong đời sống hàng ngày. Thường hay bắt gặp các loại adapter như: power adapter (chuyển đổi điện áp), laptop adapter (bộ sạc của laptop) hay memory card adapter… Các adapter này có nhiệm vụ chính là làm cầu nối trung gian để giúp 2 đồ vật gì đó có thể hoạt động với nhau.

Ví dụ như laptop không sử dụng nguồn điện xoay chiều 224V, nên để laptop có thể sử dụng được nguồn điện 224V cần có 1 adapter làm cầu nối trung gian để chuyển nguồn điện xoay chiều 224V thành nguồn điện 1 chiều 12V. Ví dụ khác là thẻ nhớ, trên thị trường có rất nhiều loại thẻ nhớ nhưng loại thịnh hành nhất ngày nay vẫn là loại micro-SD vì tính nhỏ gọn và phổ biến của nó, vậy nếu có 1 thẻ micro-SD và 1 máy ảnh sử dụng thẻ SD, làm sao để có thể cắm thẻ micro-SD này vào máy ảnh? Khi đó sử dụng 1 adapter để chuyển “bề ngoài” của thẻ micro-SD thành SD để có thể cắm vào máy ảnh.

Adapter trong hướng đối tượng

Trong những mục đích của hướng đối tượng có thể “phản ánh” (hay ánh xạ) tốt hơn các đối tượng ngoài thực tế vào trong lập trình. Vì lẽ đó ngoài thực tế có adapter thì trong hướng đối tượng cũng có adapter với mục đích tương tự. Lấy lại ví dụ về 1 thẻ micro-SD và máy ảnh, nếu coi vẻ bề ngoài và kích thước của thẻ nhớ là 1 phần “giao diện” của thẻ nhớ, thì adapter thẻ nhớ đóng vai trò là 1 cầu nối trung gian để chuyển đổi “giao diện” của thẻ nhớ sao cho nó có thể phù hợp với máy ảnh.

Giả sử có 1 hệ thống phần mềm (hãy tưởng tượng nó là cái máy ảnh), và có 1 số đối tượng (hãy tưởng tượng nó là cái thẻ micro-SD) được 1 số developer viết ra nhưng có giao diện không phù hợp với hệ thống này, cách tốt nhất để có thể “ráp” các đối tượng này vào được hệ thống là tạo các adapter để chuyển đổi giao diện của các đối tượng này.

Ý tưởng của Adapter

Giao diện của 1 lớp rất quan trọng, nó biết cách thức tương tác với đối tượng. Trong 1 số trường hợp, giả sử có sẳn 1 đối tượng, đối tượng đó cần, nhưng lại có giao diện không phù hợp với hệ thống nên ta dùng được. Vậy phải chăng định nghĩa lại đối tượng đó với giao diện khác?

Ý tưởng của Adapter Pattern rất đơn giản. Khi có 1 số các đối tượng, nhưng các đối tượng đó lại không sử dụng được với phần mềm đang xây dựng vì nó có giao diện không phù hợp, vậy giải pháp phù hợp nhất sẽ là tạo nên giao diện mới cho các đối tượng đó bằng 1 lớp giao diện trung gian mà người ta gọi là “Adapter”. Lớp này chỉ có trách nhiệm chuyển đổi giao diện, các phương thức của nó sẽ gọi lại các phương thức của đối tượng mà nó “adapt”.

Các khái niệm

Để hiểu về sơ đồ mô tả Adapter Pattern thì trước hết phải hiểu về 3 khái niệm:

  • Client: Đây là lớp sẽ sử dụng đối tượng (đối tượng muốn chuyển đổi giao diện).
  • Adaptee: Đây là những lớp muốn lớp Client sử dụng, nhưng hiện thời giao diện của nó không phù hợp.
  • Adapter: Đây là lớp trung gian, thực hiện việc chuyển đổi giao diện cho Adaptee và kết nối Adaptee với Client.

Phân loại Adapter Pattern

Trong hướng đối tượng có 2 khái niệm quan trọng đó là:

  • Composition: cấu thành. Nghĩa là 1 lớp B nào đó sẽ trở thành 1 thành phần của lớp A (1 field trong lớp A). Tuy lớp A không kế thừa lại giao diện của lớp B nhưng nó có được mọi khả năng mà lớp B có.
  • Inheritance: kế thừa. Nghĩa là lớp Derived sẽ kế thừa từ lớp Base và thừa hưởng tất cả những gì lớp Base có. Nhờ kế thừa mà nó giúp tăng khả năng sử dụng lại code, tăng khả năng bảo trì và nâng cấp chương trình. Và do vậy kế thừa là khái niệm trọng tâm trong hướng đối tượng. Nhưng nó có 1 nhược điểm, đôi khi nếu chúng ta quá lạm dụng nó, nó sẽ làm cho chương trình của chúng ta phức tạp lên nhiều, điển hình là trong lập trình game. Do vậy đôi lúc trong lập trình game người ta thường có khuynh hướng thích sử dụng composition hơn.

Ứng với 2 khái niệm này sẽ có 2 cách cài đặt lớp adapter: Object Adapter và Class Adapter.

Object Adapter Pattern

Object Adapter Pattern

Object Adapter Pattern dựa trên ý tưởng về composition. Lớp Adapter chứa 1 instance của lớp Adaptee bên trong nó. Và giao diện sẽ tương ứng với giao diện cần. Nhưng các phương thức trong giao diện đó không được định nghĩa, mà nó chỉ gọi lại các phương thức tương ứng của lớp Adaptee. Adapter lúc này chỉ như 1 cầu nối trung gian giữa Client và Adaptee, mục đích để “chuyển đổi tên phương  thức” mà Client gọi trên đối tượng Adapter thành phương thức tương ứng ở lớp Adaptee.

Đối với Object Adapter Pattern, sử dụng 1 đối tượng Adapter để “mặc” (wrap) cho đối tượng Adaptee 1 “bộ áo mới” (giao diện mới). Nếu tưởng tượng Adaptee là 1 người, thì Adapter chỉnh là bộ đồ, khi Adaptee “mặc” lên mình Adapter thì Adaptee (lúc này là người) sẽ có 1 vẻ bề ngoài (giao diện) khác.

Class Adapter Pattern

Class Adapter Pattern

Class Adapter Pattern dựa trên ý tưởng về inheritance. Lớp Adapter kế thừa lớp Adaptee và thực thi cả giao diện mới (giao diện mà Client cần). Các phương thức trong giao diện mới sẽ được cài đặt bằng cách gọi lại các phương thức tương ứng của Adaptee mà nó kế thừa được.

Class Adapter tương tự với trường hợp sử dụng thiết bị output trên PC. Hãy tưởng tượng có 1 PC và 1 màn hình, người dùng là Client, màn hình hiển thị của PC là Adaptee (coi nó là 1 “giao diện” hiển thị), và máy in là TargetInterface. Giả sử giờ không cần hiển thị 1 văn bản trên màn hình nữa, mà muốn nó “hiển thị” ra giấy; do đó, cần đến giao diện của máy in (TargetInterface). Vì vậy, sẽ cắm máy in vào máy tính, và giờ đây nó vừa có thể hiển thị lên màn hình, vừa có thể “hiển thị” ra giấy, lúc này hình ảnh của PC được cắm thêm máy in vào tương tự như 1 Class Adapter. Bộ PC này (PC + màn hình + máy in) sẽ có cả giao diện hiển thị của màn hình và giao diện hiển thị của máy tin. Tương tự, trong Class Adapter Pattern, Adapter sẽ kế thừa cả giao diện của Adaptee và TargetInterface.

IO Stream

IO Stream Co., Ltd

30 Trinh Dinh Thao, Hoa Thanh ward, Tan Phu district, Ho Chi Minh city, Vietnam
+84 28 22 00 11 12
developer@iostream.co

383/1 Quang Trung, ward 10, Go Vap district, Ho Chi Minh city
Business license number: 0311563559 issued by the Department of Planning and Investment of Ho Chi Minh City on February 23, 2012

©IO Stream, 2013 - 2024