Gia Bao TSC

Adapter Design Pattern

Trong bài viết này chúng ta sẽ tìm hiểu một pattern thuộc nhóm Structural. Pattern này đóng vai trò trung gian giữa hai lớp, chuyển đổi giao diện của một hay nhiều lớp có sẵn thành một giao diện khác, thích hợp cho lớp đang viết. Chúng ta đang nói về Adapter Pattern, hay còn gọi là Wrapper Pattern.

 Design Patterns  posted at February 28, 2019

Adapter trong thực tế

Adapter là một khái niệm rất thông dụng trong đời sống hàng ngày. Ta 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 hai đồ 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ó một 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. Một 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 bạn có một thẻ micro-SD và một 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 đó ta sẽ sử dụng một 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

Một trong những mục đích của hướng đối tượng là để giúp chúng ta 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ẽ đó mà ngoài thực tế có adapter thì trong hướng đối tượng cũng có thể có adapter với mục đích tương tự như adapter ngoài thực tế. Lấy lại ví dụ về một thẻ micro-SD và máy ảnh, nếu ta coi vẻ bề ngoài và kích thước của thẻ nhớ là một phần “giao diện” của thẻ nhớ, thì adapter thẻ nhớ đóng vai trò là một 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ử ta có một hệ thống phần mềm (hãy tưởng tượng nó là cái máy ảnh), và có một số đối tượng (hãy tưởng tượng nó là cái thẻ micro-SD) được một 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.

Các khái niệm

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

  • Client: Đây là lớp sẽ sử dụng đối tượng của bạn (đối tượng mà bạn muốn chuyển đổi giao diện).
  • Adaptee: Đây là những lớp bạn 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

Trong hướng đối tượng có hai khái niệm quan trọng song hành cùng nhau, đó là:

  • Composition: cấu thành. Nghĩa là một lớp B nào đó sẽ trở thành một thành phần của lớp A (một 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à một 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ó một 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.

Và ứng với hai khái niệm này sẽ cho ta hai cách để chúng ta cài đặt lớp adapter: Object AdapterClass Adapter.

Object Adapter Pattern

Đây là một phương pháp cài đặt Adapter Pattern dựa trên ý tưởng về composition. Một lớp mới (Adapter) sẽ tham chiếu đến một (hoặc nhiều) đối tượng của lớp có sẵn với giao diện không tương thích (Adaptee), đồng thời cài đặt giao diện mà người dùng mong muốn (Target). Trong lớp mới này, khi cài đặt các phương thức của giao diện người dùng mong muốn, sẽ gọi phương thức cần thiết thông qua đối tượng thuộc lớp có giao diện không tương thích. Tiếp hợp đối tượng tránh được vấn đề đa thừa kế, không có trong các ngôn ngữ hiện đại (Java, C#).

Object Adapter Pattern


public interface Target {

	// Client is working
	public String estimate(int i);
}

public class Adaptee {

	// Old interface, need to adapter for working
	public double precise(double a, double b) {
		System.out.println("Old lib::precise");
		return a/b;
	}
}

public class Adapter implements Target {

	// Old model
	Adaptee adaptee;
	
	// Pass adaptee object to Adapter
	public Adapter(Adaptee component) {
		this.adaptee = component;
	}
	
	@Override
	public String estimate(int i) {

		return String.valueOf(Math.round(adaptee.precise(i, 3)));
	}
}

public class Clients {

	public static void main(String[] ars) {
		
		System.out.println("--- Adapter Design Pattern ---");

		Target target = new Adapter(new Adaptee());
		System.out.println(target.estimate(8));
	}
}

Class Adapter Pattern

Trong mô hình này, một lớp mới (Adapter) sẽ kế thừa lớp có sẵn với giao diện không tương thích (Adaptee), đồng thời cài đặt giao diện mà người dùng mong muốn (Target). Trong lớp mới, khi cài đặt các phương thức của giao diện người dùng mong muốn, phương thức này sẽ gọi các phương thức cần thiết mà nó thừa kế được từ lớp có giao diện không tương thích.

Class Adapter Pattern


public interface Target {
	
	// Client is working
	String estimate(int i);
}

public class Adaptee {

	// Old interface, need to adapter for working
	public double precise(double a, double b) {
		
		System.out.println("Old lib::precise");
		return a/b;
	}
}

public class Adapter extends Adaptee implements Target {

	@Override
	public String estimate(int i) {
		return String.valueOf(Math.round(precise(i, 3)));
	}
}

public class Clients {

	public static void main(String[] args) {

		System.out.println("--- Adapter Design Pattern ---");
		
		Target target = new Adapter();
		System.out.println(target.estimate(5));
	}
}

Liên quan

– Bridge: có cấu trúc tương tự, nhưng mục tiêu khác (tách một giao diện khỏi phần cài đặt).

– Decorator: bổ sung thêm chức năng nhưng không làm thay đổi giao diện, trong mẫu thiết kế Decorator, một Adapter sẽ phối hợp hai đối tượng khác nhau.

– Proxy: Định nghĩa một giao diện đại diện cho các đối tượng khác mà không làm thay đổi giao diện của các đối tượng được đại diện, điều này thực hiện được nhờ các Adapter.

Java API

Mẫu thiết kế Adapter được dùng phổ biến trong Java AWT (WindowAdapter, ComponentAdapter, ContainerAdapter, FocusAdapter, KeyAdapter, MouseAdapter và MouseMotionAdapter), …
Ví dụ: giao diện WindowListener có 7 phương thức. Khi listener của ta cài đặt giao diện này, cần phải cài đặt tất cả 7 phương thức xử lý sự kiệ, dù một số phương thức chỉ cài đặt rỗng do không dùng đến loại sự kiện đó. Lớp WindowAdapter cài đặt giao diện WindowListener, cài đặt sẵn và rỗng cả 7 phương thức. Như vậy nếu lớp listener của ta thừa kế từ lớp WindowAdapter, chỉ cần override những phương thức quan tâm.

Sử dụng

  • Sử dụng một lớp đã tồn tại trước đó nhưng giao diện sử dụng không phù hợp như mong muốn, ta lại không có mã nguồn để sửa đổi giao diện đó.
  • Sử dụng một lớp, nhưng lớp này được tạo ra với mục đích sử dụng chung, nên không phù hợp cho việc tạo một giao diện đặc thù.
  • Có sự chuyển đổi giao diện từ nhiều nguồn khác nhau.
  • Tiếp hợp nhiều đối tượng cùng một lúc, nhưng giao diện mong muốn không phải là interface mà là một lớp trừu tượng.

Source Code

Gia Bao TSC