12/08/2018, 13:20

Design Pattern - Abstract Factory

Tiếp theo bài viết Design Pattern - Factory, chúng ta sẽ tiếp tục tìm hiểu sâu hơn về các cách sử dụng khác của Factory với Ruby. Parameterized Factory Một vấn đề lập trình viên thường gặp phải đó là việc phải mở rộng chuơng trình của mình để tương thích với nhiều yêu cầu hoặc nhiều loại data, ...

Tiếp theo bài viết Design Pattern - Factory, chúng ta sẽ tiếp tục tìm hiểu sâu hơn về các cách sử dụng khác của Factory với Ruby.

Parameterized Factory

Một vấn đề lập trình viên thường gặp phải đó là việc phải mở rộng chuơng trình của mình để tương thích với nhiều yêu cầu hoặc nhiều loại data, object hơn. Chẳng hạn với chương trình Class Pond ở bài viết trước, chúng ta muốn có thêm các lớp thực vật cây cỏ trong chuơng trình như:

class Algae
  def initialize(name)
    @name = name
  end
  def grow
    puts("The Algae #{@name} soaks up the sun and grows")
  end
end
class WaterLily
  def initialize(name)
    @name = name
  end
  def grow
    puts("The water lily #{@name} floats, soaks up the sun, and grows")
  end
end

thì ta buộc phải thêm tham số vào Class Pond.

class Pond
  def initialize(number_animals, number_plants)
    @animals = []
    number_animals.times do |i|
      animal = new_animal("Animal#{i}")
      @animals << animal
    end
    @plants = []
    number_plants.times do |i|
      plant = new_plant("Plant#{i}")
      @plants << plant
    end
  end
  def simulate_one_day
    @plants.each {|plant| plant.grow }
    @animals.each {|animal| animal.speak}
    @animals.each {|animal| animal.eat}
    @animals.each {|animal| animal.sleep}
  end
end

Chúng ta cũng cần sửa lại các subclass nữa

class DuckWaterLilyPond < Pond
  def new_animal(name)
    Duck.new(name)
  end
  def new_plant(name)
    WaterLily.new(name)
  end
end
class FrogAlgaePond < Pond
  def new_animal(name)
    Frog.new(name)
  end
  def new_plant(name)
    Algae.new(name)
  end
end

Có vẻ chúng ta đang gặp một vấn đề ở đây, đó là nếu như yêu cầu không phải là bổ sung thêm 1 loại mà là 10 hay 20 loại thì sao ?

Có một giải pháp tốt hơn là đó là sử dụng 1 Factory duy nhất nhưng nó sẽ nhận tham số truyền vào để sử dụng method tuơng ứng như sau:

class Pond
  def initialize(number_animals, number_plants)
    @animals = []
    number_animals.times do |i|
      animal = new_organism(:animal, "Animal#{i}")
      @animals << animal
    end
    @plants = []
    number_plants.times do |i|
      plant = new_organism(:plant, "Plant#{i}")
      @plants << plant
    end
  end
# ...
end
class DuckWaterLilyPond < Pond
  def new_organism(type, name)
    if type == :animal
      Duck.new(name)
    elsif type == :plant
      WaterLily.new(name)
    else
      raise "Unknown organism type: #{type}"
    end
  end
end

Cách này giúp chúng ta giảm được số lượng code trong source, thay vì viết thêm method mới, ta chỉ việc sửa đổi 1 method duy nhất trong subclass tuơng ứng.

Classes Are Just Objects, Too

Như ví dụ trên, mặc dù đã loại bỏ được việc phải thêm method nhưng chúng ta vẫn phải thêm các subclass tương ứng cho từng loại, như class DuckWaterLilyPond hoặc FrogAlgaePond. Việc này khá bất tiện và khó quản lý nếu ta cần phải mô phỏng nhiều loại thực, động vật trong Pond.

Có một cách khá đơn giản, đó là cứ coi các class Frog, Duck, WaterLily và Algae chỉ là những objects. Chúng ta có thể loại bỏ các subclass và thay vào đó là lưu các class thực, động vật vào các biến instance của class Pond.

class Pond
  def initialize(number_animals, animal_class, number_plants, plant_class)
    @animal_class = animal_class
    @plant_class = plant_class
    @animals = []
    number_animals.times do |i|
      animal = new_organism(:animal, "Animal#{i}")
      @animals << animal
    end
    @plants = []
    number_plants.times do |i|
      plant = new_organism(:plant, "Plant#{i}")
      @plants << plant
    end
  end
  def simulate_one_day
    @plants.each {|plant| plant.grow}
    @animals.each {|animal| animal.speak}
    @animals.each {|animal| animal.eat}
    @animals.each {|animal| animal.sleep}
  end
  def new_organism(type, name)
    if type == :animal
      @animal_class.new(name)
    elsif type == :plant
      @plant_class.new(name)
    else
      raise "Unknown organism type: #{type}"
    end
  end
end

Với cách này, số lượng subclass đã được loại bỏ hoàn toàn, nhưng class Pond vẫn không quá phức tạp.

Mở rộng chương trình

Với mô hình Pond, chương trình của chúng ta vẫn cần mở rộng để có thể mô phỏng nhiều loại môi trường sống khác, có thể đó là mô hình của một khu rừng jungle chẳng hạn. Môi trường jungle cũng cần có các động thực vật riêng, chúng ta cần xây dựng class tigers và trees.

class Tree
  def initialize(name)
    @name = name
  end
  def grow
    puts("The tree #{@name} grows tall")
  end
end

class Tiger
  def initialize(name)
    @name = name
  end
  def eat
    puts("Tiger #{@name} eats anything it wants.")
  end
  def speak
    puts("Tiger #{@name} Roars!")
  end
  def sleep
    puts("Tiger #{@name} sleeps anywhere it wants.")
  end
end

Tiếp theo, ta cần class cho môi trường jungle. Có thể thấy jungle và pond rất tương đồng, vì thế chúng ta có thể sử dụng lại class Pond cho jungle này. Việc này khá đơn giản, ta chỉ cần đổi tên class Pond sang 1 tên chung, Habitat chẳng hạn, sau đó dùng lại code như sau:

jungle = Habitat.new(1, Tiger, 4, Tree)
jungle.simulate_one_day
pond = Habitat.new( 2, Duck, 4, WaterLily)
pond.simulate_one_day

Tuy nhiên, class mới Habitat này lại không có sự liên kết hay ràng buộc giữa động, thực vật với nhau. Ví dụ, Tiger và Lily thì không bao giờ sống trong cùng một môi trường được.

unstable = Habitat.new( 2, Tiger, 4, WaterLily)

Giải pháp cho vấn đề này là thay vì xác định cụ thể các sinh vật sống trong môi trường Habitat, ta sẽ đi qua một đối tượng duy nhất, đối tượng này sẽ tạo ra một tập các sinh vật tương thích với môi trường sống.

Đối tượng duy nhất này được gọi là Abstract Factory.

Với yêu cầu trên, chúng ta sẽ xây dựng 2 abstract factory.

class PondOrganismFactory
  def new_animal(name)
    Frog.new(name)
  end
  def new_plant(name)
    Algae.new(name)
  end
end

class JungleOrganismFactory
  def new_animal(name)
    Tiger.new(name)
  end
  def new_plant(name)
    Tree.new(name)
  end
end

Và class Habitat sẽ được thay đổi lại như sau:

class Habitat
  def initialize(number_animals, number_plants, organism_factory)
    @organism_factory = organism_factory
    @animals = []
    number_animals.times do |i|
      animal = @organism_factory.new_animal("Animal#{i}")
      @animals << animal
    end
    @plants = []
    number_plants.times do |i|
      plant = @organism_factory.new_plant("Plant#{i}")
      @plants << plant
    end
  end
  # Rest of the class...
end

jungle = Habitat.new(1, 4, JungleOrganismFactory.new)
jungle.simulate_one_day
pond = Habitat.new( 2, 4, PondOrganismFactory.new)
pond.simulate_one_day

Mô hình UML cho Abstract Factory pattern

abstract_factory.png

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