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