Thiết kế hướng đối tượng trong Ruby on Rails
Nếu hình dung phần mềm của bạn là một căn nhà thì Design patterns chính là bản thiết kế của căn nhà đó. Hay có thể nói Design patterns là một giải pháp tối ưu trong thiết kế cấu trúc phần mềm có nguồn gốc từ lập trình hướng đối tượng (OPP). Design patterns không phụ thuộc vào bất kỳ ngôn ngữ lập ...
Nếu hình dung phần mềm của bạn là một căn nhà thì Design patterns chính là bản thiết kế của căn nhà đó. Hay có thể nói Design patterns là một giải pháp tối ưu trong thiết kế cấu trúc phần mềm có nguồn gốc từ lập trình hướng đối tượng (OPP). Design patterns không phụ thuộc vào bất kỳ ngôn ngữ lập trình hay công nghệ mà bạn sử dụng. Trong các dự án về Web thì design patterns đơn giản nhất mà các bạn nhìn thấy là mô hình MVC. Có 3 nhóm Design patterns chính là: Creational
- Abstract Factory
- Builder
- Factory Method
- Prototype
- Singleton
Structural
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
Behavioral
- Chain of responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
Sau đây mình xin giới thiệu về một số design patterns tiêu biểu nhất. Bài viết mình có tham khảo từ các nguồn https://bogdanvlviv.github.io/posts/ruby/patterns/design-patterns-in-ruby.html https://dalibornasevic.com/posts/9-ruby-singleton-pattern
Đây là kỹ thuật nhằm tạo ra một AbstractFactory(lớp trừu tượng) cho các ConcreteFactory(lớp con hay còn gọi là lớp kế thừa), mà giữa các ConcreteFactory sẽ có một điểm chung hoặc sự phụ thuộc nào đó. AbstractFactory sẽ định nghĩa ra các phương thức và các phương thức này sẽ được cài đặt tại các ConcreteFactory. Bản chất của kỹ thuật này chính là tính kế thừa trong lập trình hướng đối tượng.
Ví dụ:
class Animal attr_accessor :name def initialize name @name = name end def eat end def speak end def sleep end end
class Tiger < Animal def eat puts "Tiger #{name} is eating anything it wants." end def speak puts "Tiger #{name} Roars!" end def sleep puts "Tiger #{name} sleeps anywhere it wants." end end
class Frog < Animal def eat puts "Frog #{name} is eating anything it wants." end def speak puts "Frog #{name} Roars!" end def sleep puts "Frog #{name} sleeps anywhere it wants." end end
class Plant attr_accessor :name def initialize name @name = name end def grow end end
class Tree < Plant def grow puts "The tree #{name} grows tall." end end
class Algae < Plant def grow puts "The tree #{name} grows tall." end end
class Habitat def initialize(organism_factory, number_animals: 0, number_plants: 0) @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 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
class JungleOrganismFactory def new_animal name Tiger.new name end def new_plant name Tree.new name end end
class PondOrganismFactory def new_animal name Frog.new name end def new_plant name Algae.new name end end
jungle = Habitat.new JungleOrganismFactory.new, number_animals: 1, number_plants: 4 jungle.simulate_one_day pond = Habitat.new PondOrganismFactory.new, number_animals: 2, number_plants: 4 pond.simulate_one_day
Trong ví dụ trên thì mình có 2 AbstractFactory là Animal và Plant.
- Animal định nghĩa nghĩa ra 3 phương thức: eat, speak, sleep
- Plant định nghĩa ra một phương thức là grow
Và có 2 ConcreteFactory là Tiger và Tree. Tiger sẽ cài đặt phương thức eat, speak và sleep. Tree sẽ cài đặt phương thức grow.
Là các phương thức được khai báo tại AbstractFactory nhưng không được cài đặt. Ở ví dụ trên chính là các phương thức eat, speak, sleep của lớp Animal và grow của lớp Plant
Đây là kỹ thuật nhằm đơn giản hóa một đối tượng phức tạp bằng cách chia ra thành các đối tượng nhỏ hơn. Và sau đấy chúng ta lại ghép các đối tượng nhỏ này thông qua Builder để tạo thành một Product mà chúng ta mong muốn ban đầu.
Giả sử chúng ta muốn lắp ghép một chiếc máy tính Computer, Computer được cấu tạo từ Motherboard, Drive và Display(màn hình). Motherboard gồm có cpu và memory_size. Cpu lại gồm 2 loại là BasicCPU và TurboCPU. Lớp ComputerBuilder chính là Builder cung cấp những hàm lắp ghép các thành phần trong một Computer để trả ra một product thông qua hàm result. Director là một class quản lý việc lắp ghép và đưa ra sản phẩm.
Ví dụ:
class CPU end class BasicCPU < CPU end class TurboCPU < CPU end
class Motherboard attr_accessor :cpu attr_accessor :memory_size def initialize(cpu=BasicCPU.new, memory_size=1024) @cpu = cpu @memory_size = memory_size end end
class Drive attr_reader :type # either :cd, :dvd or :hard_disk attr_reader :size # in Mb attr_reader :writable # true if this drive is writable def initialize(type, size, writable) @type = type @size = size @writable = writable end end
class Computer attr_accessor :display attr_accessor :motherboard attr_reader :drives def initialize(display=:crt, motherboard=Motherboard.new, drives=[]) @motherboard = motherboard @drives = drives @display = display end end
class ComputerBuilder attr_reader :computer def initialize @computer = Computer.new end def basic_cpu computer.motherboard.cpu = BasicCPU.new end def turbo_cpu computer.motherboard.cpu = TurboCPU.new end def display=(display) computer.display = display end def memory_size=(size_in_mb) computer.motherboard.memory_size = size_in_mb end def add_cd(writer=false) computer.drives << Drive.new(:cd, 760, writer) end def add_dvd(writer=false) computer.drives << Drive.new(:dvd, 4000, writer) end def add_hard_disk size_in_mb computer.drives << Drive.new(:hard_disk, size_in_mb, true) end def result @computer end end
class Director def initialize builder @builder = builder end def create_company builder.turbo_cpu builder.add_hard_disk 1_000_000 builder.memory_size = 16000 builder.add_cd true builder.add_dvd computer = builder.result end end
Là một design pattern giới hạn sự khởi tạo một biến instance và biến instance phải là biến toàn cục. Nó rất hữu hiệu khi bạn muốn truy cập vào các phần khác nhau của ứng dụng, thường là các chức năng đăng nhập, giao tiếp với hệ thống bên ngoài hay truy cập cơ sở dữ liệu.
Có hai cách để cài đặt singleton pattern trong Ruby:
Single Instance của một class
class Logger def initialize @log = File.open("log.txt", "a") end @@instance = Logger.new def self.instance return @@instance end def log(msg) @log.puts(msg) end private_class_method :new end Logger.instance.log "message 1"
Trong ví dụ này, bên trong lớp Logger chúng ta tạo ra một biến instance của lớp Logger và chúng ta có thể truy cập vào biến instance này với phương thức Logger.instance. Bất kỳ khi nào chúng ta cần viết một thứ gì đó vào file log sử dụng phương thức log. Trong phương thức khởi tạo initialize chúng ta chỉ cần mở file log để thêm bất kỳ thứ gì. Ở phần cuối lớp Logger, chúng ta tạo một phương thức private new để chúng ta không thể tạo các instance mới của lớp Logger. Đó là Singleton pattern: chỉ có 1 instance và là biến toàn cục.
Ruby Singleton module
Ruby Standard Library có một module singleton để cài đặt mô hình Singleton. Cũng như ví dụ trước nếu sử dụng module singleton. Ở đây chúng ta require và include module Singleton bên trong lớp Logger, định nghĩa phương thức khởi tạo initialize, mở file log để ghi vào file log. Module singleton tạo ra sự khởi tạo lười biếng (tạo ra instance từ lớp Logger vào thời điểm chúng ta gọi phương thức Logger.instance) mà không phải trong thời gian load (giống như ví dụ trước). Ngoài ra, trong module singleton cũng tạo ra phương thức private new, do đó chúng ta không phải gọi private_class_method
require "singleton" class Logger include Singleton def initialize @log = File.open("log.txt", "a") end def log(msg) @log.puts(msg) end end Logger.instance.log "message 2"
Một đối tượng đại diện cho một đối tượng khác. Tạo ra một đại diện hoặc thay vào chỗ của đối tượng khác để kiếm soát việc truy cập vào đối tượng đó. Trong ruby có gem pundit hỗ trợ việc này Ví dụ:
class Account attr_reader :balance def initialize(starting_balance=0) @balance = starting_balance end def deposit amount @balance += amount end def withdraw(amount) @balance -= amount end end
require "etc" class AccountProtectionProxy def initialize(real_account, owner_name) @subject = real_account @owner_name = owner_name end def deposit amount check_access return @subject.deposit(amount) end def withdraw amount check_access return @subject.withdraw(amount) end def balance check_access return @subject.balance end def check_access if Etc.getlogin != @owner_name raise "Illegal access: #{Etc.getlogin} cannot access account." end end end
account = Account.new(100) account.deposit(50) account.withdraw(10) proxy = AccountProtectionProxy.new(account, 'russolsen') proxy.deposit(50) proxy.withdraw(10)
Trong ví dụ trên thì đối tượng cần thay thế (đối tượng thật) là Account và đối tượng đại diện (proxy) là AccountProtectionProxy. Ở lớp AccountProtectionProxy ta thấy có 2 phương thức withdraw và deposit tương tự như lớp Account. Nhưng với mỗi phương thức của lớp AccountProtectionProxy có thêm việc kiểm tra quyền truy cập thông qua phương thức check_access