Design Pattern - Adapter
Bài đăng này đã không được cập nhật trong 3 năm
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
Bài viết liên quan:
All rights reserved