12/08/2018, 11:55

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.pngadapter 2.png

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
0