Design Pattern - Adapter
Adapter Adapter là gì? Chúng ta có thể hiểu nôm na. Nó giúp các thành phần, hay những thiết bị khác nhau có thể kết nối với nhau. Ví dụ như một chiếc máy vi tính đời cũ dùng cổng PS2 vậy, nhưng chúng ta lại muốn dùng chuột với cổng USB. 2 thiết bị rõ ràng không thể kết nối với nhau vì 2 cổng ...
Adapter
Adapter là gì? Chúng ta có thể hiểu nôm na. Nó giúp các thành phần, hay những thiết bị khác nhau có thể kết nối với nhau.
Ví dụ như một chiếc máy vi tính đời cũ dùng cổng PS2 vậy, nhưng chúng ta lại muốn dùng chuột với cổng USB. 2 thiết bị rõ ràng không thể kết nối với nhau vì 2 cổng của chúng khác nhau. Để kết nối với nhau chúng cần một thiết bị USB-to-PS2 converter. Thiết bị này đóng vai trò chính là một adapter.
Tương tự như vậy, trong thế giới phần mềm, adapter cũng rất quan trọng, thậm chí nó còn được sử dụng nhiều hơn trong phần cứng. Vì phần mềm được tạo lên từ các ý tưởng, chúng được code trên những interface khác nhau, và tạo lên vô số các đối tượng không tương thích - tức là chúng không thể giao tiếp với nhau.
Adapter trong phần mềm
Hãy tưởng tượng chúng ta có một class để encrypt 1 file như sau:
class Encrypter def initialize(key) @key = key end def encrypt(reader, writer) key_index = 0 while not reader.eof? clear_char = reader.getc encrypted_char = clear_char ^ @key[key_index] writer.putc(encrypted_char) key_index = (key_index + 1) % @key.size end end end
Phương thức encrypt sẽ lấy 2 file, một file để đọc và một file để ghi bằng cách encrypt từng byte của file thứ nhất bằng key. Để sử dụng class này ta dùng đơn giản như sau:
reader = File.open('message.txt') writer = File.open('message.encrypted','w') encrypter = Encrypter.new('my secret key') encrypter.encrypt(reader, writer)
Nhưng nếu dữ liệu mà bạn muốn bảo vệ nằm trong 1 string chứ không phải trong 1 file. Trong trường hợp này, bạn cần một đối tượng để mở file - tức là interface sẽ giống như đối tượng IO của Ruby đưa ra bên ngoài, nhưng thực tế là bên trong nó sẽ đọc các ký tự character từ string.
class StringIOAdapter def initialize(string) @string = string @position = 0 end def getc if @position >= @string.length raise EOFError end ch = @string[@position] @position += 1 return ch end def eof? return @position >= @string.length end end
Class StringIOAdapter sẽ có 2 biến instance: 1 để lưu string và 2 là để lưu vị trí position. Mỗi lần getc được gọi StringIOAdapter sẽ trả về kí tự character ở vị trí position hiện tại và position sẽ tăng lên tới giá trị tiếp theo. getc cũng sẽ raise ra exception EOFError nếu đọc tới position cuối cùng. Với class StringIOAdapter, chúng ta hoàn toàn có thể encrypt 1 string với class Encrypter
encrypter = Encrypter.new('XYZZY') reader= StringIOAdapter.new('We attack at dawn') writer=File.open('out.txt', 'w') encrypter.encrypt(reader, writer)
StringIOAdapter chính là ví dụ đơn giản về Adapter
Adapter là một đối tượng giúp lấp đầy những chỗ thiếu sót, không phù hợp giữa interface mà bạn có với interface mà bạn cần.
Adapter sử dụng thế nào trong Ruby
Ở trên, chúng ta đã hiểu và biết cách hoạt động, cũng như cách tạo 1 Adapter nhưng trong Ruby, việc tạo và sử dụng Adapter còn linh hoạt và thú vị hơn nhiều. Vì sao ư, đơn giản vì Ruby cho phép chúng ta thay đổi hầu hết các class bất kỳ lúc nào.
Trước tiên, chúng ta cũng tạo 1 ví dụ khác về Adapter sau:
#Chúng ta có 1 class để render 1 object (TextObject) ra ngoài màn hình #với các thông tin như text, size, và color class Renderer def render(text_object) text = text_object.text size = text_object.size_inches color = text_object.color # render the text ... end end class TextObject attr_reader :text, :size_inches, :color def initialize(text, size_inches, color) @text = text @size_inches = size_inches @color = color end end
Đoạn code trên dễ dàng in ra màn hình thông tin của object TextObject, thế nhưng nếu muốn in ra màn hình thông tin của object BritishTextObject thì sao:
class BritishTextObject attr_reader :string, :size_mm, :colour # ... end
Rõ ràng Renderer không thể làm việc với object BritishTextObject, vì các field của chúng không phù hợp với nhau. Để làm việc này, rất đơn giản chúng ta xây dựng 1 class Adapter như sau:
class BritishTextObjectAdapter < TextObject def initialize(bto) @bto = bto end def text return @bto.string end def size_inches return @bto.size_mm / 25.4 end def color return @bto.colour end end
Đó chỉ là một cách, với Ruby chúng ta có thể sử dụng theo một cách khác.
Thay vì sử dụng 1 class Adapter, chúng ta sẽ viết thêm các method còn thiếu vào chính class BritishTextObjec:
# Make sure the original class is loaded require 'british_text_object' # Now add some methods to the original class class BritishTextObject def color return colour end def text return string end def size_inches return size_mm / 25.4 end end
Đoạn code trên sẽ dùng method require để load class BritishTextObject gốc, cách làm này không tạo thêm 1 class mới, mà nó chỉ mở class đã tồn tại và thêm vào các method mới. Với cách này, bạn không chỉ thêm method, mà còn có thể thay đổi các method cũ hoặc xóa chúng hoàn toàn. (Cách này thậm chí còn có thể áp dụng với các class của Ruby)
Còn một cách nữa có thể dùng trong Ruby, đó là thay vì thay đổi class, chúng ta sẽ thay đổi chính intance, cách này sẽ tạo ra ít ảnh hưởng hơn với cách thay đổi class ở trên (có lẽ sẽ an toàn hơn với những class phức tạp)
bto = BritishTextObject.new('hello', 50.8, :blue) class << bto def color colour end def text string end def size_inches return size_mm/25.4 end end
Kết luận
Ở trên chúng ta đã được biết tới cách sử dụng Adapter theo một cách khác với Ruby, có thể gọi là Modify chẳng hạn. 2 cách này đều giúp chúng ta làm 1 việc nhưng nó vẫn có những điểm khác nhau, và làm sao để biết lúc nào nên áp dụng cách nào.
Có thể nhận thấy, cách Modify làm cho code đơn giản, và nó cũng khá dễ hiểu, tuy nhiên bạn chỉ nên áp dụng cách này nếu:
- Cách thay đổi của bạn rất đơn giản và rõ ràng.
- Bạn hiểu rõ về class mà bạn thay đổi để tránh dẫn tới những rủi ro về sau.
Còn việc áp dụng Adapter thì nên áp dụng khi:
- Interface không phù hợp phức tạp và lớn.
- Bạn không hiểu class hoạt động thế nào. Do đó cách tốt nhất là không nên thay đổi class đó mà nên xây dựng 1 Adapter riêng.
Tham khảo
Github (updating):https://github.com/ducnhat1989/design-patterns-in-ruby
Sách: “DESIGN PATTERNS IN RUBY” của tác giả Russ Olsen
:
- CÁC NGUYÊN TẮC TRONG DESIGN PATTERN