12/08/2018, 18:23

Design pattern trong Ruby (phần 1)

Giới thiệu Abstract Factory là 1 design pattern dùng cho việc tạo ra một tập hợp các object liên quan hoặc phụ thuộc lẫn nhau mà không chỉ rõ ra đó là các object thuộc class cụ thể nào tại thời điểm thiết kế. Cấu trúc của design pattern này gồm có 3 thành phần: AbstractFactory: base của các ...

Giới thiệu

Abstract Factory là 1 design pattern dùng cho việc tạo ra một tập hợp các object liên quan hoặc phụ thuộc lẫn nhau mà không chỉ rõ ra đó là các object thuộc class cụ thể nào tại thời điểm thiết kế. Cấu trúc của design pattern này gồm có 3 thành phần:

  • AbstractFactory: base của các ConcreteFactory và chứa những xử lí chung của chúng
  • ConcreteFactory: dùng để tạo ra các object thực tế
  • Product: object được tạo ra bởi ConcreteFactory

Để hiểu hơn về Abstract Factory hãy cùng theo dõi bài toán dưới đây.

Bài toán

Giả sử chúng ta cần xây dựng chương trình mô phỏng 1 cái ao, trong đó có 2 loài động vật là vịt và ếch, 2 loài thực vật là tảo và hoa súng.

  • Class thể hiện các loài động vật

    • Vịt: class Duck, có method eat
    • Ếch: class Frog, có method eat
  • Class thể hiện các loài thực vật

    • Tảo: class Algae, có method grow
    • Hoa súng: class WaterLily, có method grow
  • Class tạo ra hệ sinh thái trong ao

    • Định nghĩa động vật và thực vật trong ao bằng constructor
    • Có method trả về các object động vật và thực vật
  • Điều kiện: hệ sinh thái trong ao (sự kết hợp của động vật và thực vật) chỉ có thể là 1 trong 2 kiểu dưới đây

    • Duck và WaterLily
    • Frog và Algae

Sample code

Class Duck và Frog

class Duck
  def initialize(name)
    @name = name
  end

  def eat
    puts "Duck #{@name} is eating"
  end
end

class Frog
  def initialize(name)
    @name = name
  end

  def eat
    puts "Frog #{@name} is eating"
  end
end

Class Algae và WaterLily

class Algae
  def initialize(name)
    @name = name
  end

  def grow
    puts "Algae #{@name} is growing"
  end
end

class WaterLily
  def initialize(name)
    @name = name
  end

  def grow
    puts "WaterLily #{@name} is growing"
  end
end

Để tạo được hệ sinh thái trong ao với điều kiện ở trên, chúng ta sẽ dùng đến 2 class:

  • Tạo Frog và Algae => class FrogAndAlgaeFactory
  • Tạo Duck và WaterLily => class DuckAndWaterLilyFactory

Ngoài ra chúng ta sẽ tạo ra 1 class OrganismFactory có nhiệm vụ tạo ra hệ sinh thái trong ao và là base của 2 cái trên.

# Abstract Factory tạo hệ sinh thái trong ao
class OrganismFactory
  def initialize(number_animals, number_plants)
    @animals = []
    # định nghĩa động vật trong ao 
    number_animals.times do |i|
      animal = new_animal("Animal #{i}")
      @animals << animal
    end

    @plants = []
    # định nghĩa thực vật trong ao
    number_plants.times do |i|
      plant = new_plant("Plant #{i}")
      @plants << plant
    end
  end

  # trả về mảng chứa thực vật
  def get_plants
    @plants
  end

  # trả về mảng chứa động vật
  def get_animals
    @animals
  end
end

# Concrete Factory tạo Frog và Algae
class FrogAndAlgaeFactory < OrganismFactory
  private

  def new_animal(name)
    Frog.new(name)
  end

  def new_plant(name)
    Algae.new(name)
  end
end

# Concrete Factory tạo Duck và WaterLily
class DuckAndWaterLilyFactory < OrganismFactory
  private

  def new_animal(name)
    Duck.new(name)
  end

  def new_plant(name)
    WaterLily.new(name)
  end
end

Chạy code

factory = FrogAndAlgaeFactory.new(4,1)
animals = factory.get_animals
animals.each { |animal| animal.eat }
#=> Frog Animal 0 is eating
#=> Frog Animal 1 is eating
#=> Frog Animal 2 is eating
#=> Frog Animal 3 is eating
plants = factory.get_plants
plants.each { |plant| plant.grow }
#=> Algae Plant 0 is growing

factory = DuckAndWaterLilyFactory.new(3,2)
animals = factory.get_animals
animals.each { |animal| animal.eat }
#=> Duck Animal 0 is eating
#=> Duck Animal 1 is eating
#=> Duck Animal 2 is eating
plants = factory.get_plants
plants.each { |plant| plant.grow }
#=> WaterLily Plant 0 is growing
#=> WaterLily Plant 1 is growing

Giới thiệu

Builder là 1 design pattern thường được sử dụng để tạo ra các object trong những trường hợp sau

  • Cần rất nhiều dòng code để tạo ra 1 object
  • Việc tạo ra 1 object là khó khăn
  • Trong quá trình tạo object cần kiểm tra 1 số điều kiện

Cấu trúc của design pattern này gồm có 3 thành phần:

  • Director: sử dụng các interface được cung cấp bởi Builder để tạo ra object
  • Builder: xác định interface của các method
  • ConcreteBuilder: implement các interface được xác định bởi Builder

Sample code 1

Ở ví dụ này hãy cùng nhau viết 1 chương trình pha nước đường. Đầu tiên chúng ta sẽ viết class SugarWater với vai trò là ConcreteBuilder. Trong class này có các biến lưu trữ số lượng đường và nước.

# SugarWater: ConcreteBuilder
class SugarWater
  attr_accessor :water, :sugar
  def initialize(water, sugar)
    @water = water
    @sugar = sugar
  end
end

Tiếp theo là class SugarWaterBuilder với vai trò là Builder. Class này cung cấp các interface dùng trong quá trình pha nước đường thông qua 3 method

  • add_sugar: thêm đường
  • add_water: thêm nước
  • result: trả về trạng thái của cốc nước đường
# SugarWaterBuilder: các interface dùng trong quá trình pha nước đường (Builder)
class SugarWaterBuilder
  def initialize
    @sugar_water = SugarWater.new(0,0)
  end

  # thêm đường
  def add_sugar(sugar_amount)
    @sugar_water.sugar += sugar_amount
  end

  # thêm nước 
  def add_water(water_amount)
    @sugar_water.water += water_amount
  end

  # trả về trạng thái của cốc nước đường 
  def result
    @sugar_water
  end
end

Cuối cùng là class Director với method cook định nghĩa các bước trong quá trình pha nước đường.

class Director
  def initialize(builder)
    @builder = builder
  end
  
  # định nghĩa các bước pha nước đường
  def cook
    @builder.add_water(150)
    @builder.add_sugar(90)
    @builder.add_water(300)
    @builder.add_sugar(35)
  end
end

Thử dùng chương trình trên

builder = SugarWaterBuilder.new
director = Director.new(builder)
director.cook

Sau khi chạy thì trạng thái của cốc nước đường như sau

p builder.result
#=> <SugarWater:0x007fc773085bc8 @water=450, @sugar=125>

Sample code 2

Ở ví dụ trước Director đã có thể pha nước đường, ở ví dụ này chúng ta sẽ thêm chức năng pha nước muối. Trước tiên chúng ta thêm class SaltWater.

# SaltWater: ConcreteBuilder
class SaltWater
  attr_accessor :water, :salt
  def initialize(water, salt)
    @water = water
    @salt = salt
  end

  # thêm nguyên liệu (muối)
  def add_material(salt_amount)
    @salt += salt_amount
  end
end

Tiếp theo là thay đổi class SugarWater, thêm method add_material cho giống với class SaltWater.

# SugarWater: ConcreteBuilder
class SugarWater
  attr_accessor :water, :sugar
  def initialize(water, sugar)
    @water = water
    @sugar = sugar
  end

  # thêm nguyên liệu (đường)
  def add_material(sugar_amount)
    @sugar += sugar_amount
  end
end

Bây giờ chúng ta sẽ sửa lại Builder, có 2 thay đổi như sau:

  • Đổi tên class thành WaterWithMaterialBuilder
  • Đổi tên class thêm nguyên liệu thành add_material
# SugarWaterBuilder: các interface dùng trong quá trình pha nước (Builder)
class WaterWithMaterialBuilder
  def initialize(class_name)
    @water_with_material = class_name.new(0,0)
  end

  # thêm nguyên liệu 
  def add_material(material_amount)
    @water_with_material.add_material(material_amount)
  end

  # thêm nước
  def add_water(water_amount)
    @water_with_material.water += water_amount
  end

  # trả về tình trạng của cốc nước 
  def result
    @water_with_material
  end
end

Cuối cùng là class Director, chúng ta sẽ sửa lại method add_sugar thành add_material.

class Director
  def initialize(builder)
    @builder = builder
  end
  def cook
    @builder.add_water(150)
    @builder.add_material(90)
    @builder.add_water(300)
    @builder.add_material(35)
  end
end

Thử sử dụng code mới cho việc pha nước:

  • Nước đường

    builder = WaterWithMaterialBuilder.new(SugarWater)
    director = Director.new(builder)
    director.cook
    
    p builder.result
    #=> #<SugarWater:0x007fc773085bc8 @water=450, @sugar=125>
    
  • Nước muối

    builder = WaterWithMaterialBuilder.new(SaltWater)
    director = Director.new(builder)
    director.cook
    
    p builder.result
    #=> #<SaltWater:0x007f92cc103ba8 @water=450, @salt=125>
    

Giới thiệu

Trong design pattern này việc tạo ra các instance sẽ do các sub-class đảm nhiệm. Nói cách khác, chúng ta chỉ tạo ra interface để tạo ra object còn tạo object của class nào là do các sub-class quyết định.

Cấu trúc của pattern Factory Method gồm có 3 thành phần

  • Creator: base của các ConcreteFactory và chứa những xử lí chung của chúng
  • ConcreteCreator: dùng để tạo ra các object thực tế
  • Product: object được tạo ra bởi ConcreteFactory

Sample code

Ở ví dụ này chúng ta sẽ mô phỏng 1 nhà máy sản xuất nhạc cụ, đầu tiên là kèn saxophone.

  • Class biểu thị kèn saxophone: Saxophone, có method play
  • Class biểu thị nhà máy: `InstrumentFactory
    • Nhận số lượng nhạc cụ là argument của constructor method
    • Có method ship_out để xuất xưởng các nhạc cụ
# Saxophone (Product)
class Saxophone
  def initialize(name)
    @name = name
  end

  def play
    puts "#{@name} is playing"
  end
end

# Nhà máy (Creator)
class InstrumentFactory
  def initialize(number_saxophones)
    @saxophones = []
    number_saxophones.times do |i|
      saxophone = Saxophone.new("Saxophone #{i}")
      @saxophones << saxophone
    end
  end

  # xuất nhạc cụ
  def ship_out
    @tmp = @saxophones.dup
    @saxophones = []
    @tmp
  end
end

Thử chạy chương trình ở trên

factory = InstrumentFactory.new(3)
saxophones = factory.ship_out
saxophones.each { |saxophone| saxophone.play }
#=> Saxophone 0 is playing
#=> Saxophone 1 is playing
#=> Saxophone 2 is playing

Từ bây giờ chúng ta sẽ thêm 1 loại nhạc cụ nữa là Trumpet. Class Trumpet có interface giống với Saxophone.

# Trumpet (Product)
class Trumpet
  def initialize(name)
    @name = name
  end

  def play
    puts "Trumpet #{@name} is playing"
  end
end

Hãy cùng xem lại class InstrumentFactory ở phía trên

# Nhà máy (Creator)
class InstrumentFactory
  def initialize(number_saxophones)
    @saxophones = []
    number_saxophones.times do |i|
      saxophone = Saxophone.new("Saxophone #{i}")
      @saxophones << saxophone
    end
  end

  # Xuất nhạc cụ
  def ship_out
    @tmp = @saxophones.dup
    @saxophones = []
    @tmp
  end
end

Sau khi thêm Trumpet thì InstrumentFactory lại có vấn đề ở constructor method initialize

saxophone = Saxophone.new("サックス #{i}")

Để giải quyết vấn đề này chúng ta sẽ tách phần tạo Saxophone trong InstrumentFactory ra sub-class SaxophoneFacotory. Ngoài ra chúng ta cũng tạo thêm class TrumpetFactory.

# Nhà máy (Creator)
class InstrumentFactory
  def initialize(number_instruments)
    @instruments = []
    number_instruments.times do |i|
      instrument = new_instrument("Instrument #{i}")
      @instruments << instrument
    end
  end

  # xuất nhạc cụ
  def ship_out
    @tmp = @instruments.dup
    @instruments = []
    @tmp
  end
end

# SaxophoneFactory: tạo saxophone (ConcreteCreator)
class SaxophoneFactory < InstrumentFactory
  def new_instrument(name)
    Saxophone.new(name)
  end
end

# TrumpetFactory: tạo trumpet (ConcreteCreator)
class TrumpetFactory < InstrumentFactory
  def new_instrument(name)
    Trumpet.new(name)
  end
end

InstrumentFactory đang trừu tượng hoá method tạo ra nhạc cụ new_instrument.

Thử chạy chương trình ở trên

factory = SaxophoneFactory.new(3)
saxophones = factory.ship_out
saxophones.each { |saxophone| saxophone.play }
#=> Saxophone Instrument 0 is playing
#=> Saxophone Instrument 1 is playing
#=> Saxophone Instrument 2 is playing

factory = TrumpetFactory.new(2)
trumpets = factory.ship_out
trumpets.each { |trumpet| trumpet.play }
#=> Trumpet Instrument 0 is playing
#=> Trumpet Instrument 1 is playing
  • https://morizyun.github.io/ruby/design-pattern-index.html
  • https://bogdanvlviv.com/posts/ruby/patterns/design-patterns-in-ruby.html
0