Rails AntiPatterns, Fat Models và các giải pháp [Part 3]
Bài đăng này đã không được cập nhật trong 8 năm
Tiếp nối 2 phần trước:
- Phần 1: Một số khái niệm và AntiPattern: Voyeuristic Models
- Phần 2: Những giải pháp còn lại của AntiPattern: Voyeuristic Models
Hôm nay, chúng ta sẽ chuyển sang một AntiPattern In Rails khác. Đó là Fat Models AntiPattern
1.2 AntiPattern: Fat Models
Đây là một trong những nội dung trọng điểm của AntiPattern mà chúng ta sẽ đề cập tới trong series bài viết. Sự phức tạp là sát thủ số một trong những project hiện nay, nó đi vào một ứng dụng bằng nhiều cách, kể cả thông qua những tính năng phức tạp, những nhà phát triển quá thông minh, và không quen với Ruby On Rails. Chương này, phần nhiều sẽ đề cập tới sự đơn giản từ một góc nhìn duy nhất: đơn giản hóa bên trong model bằng cách di chuyển sự phức tạp đó tới một unit mới trong ứng dụng - modules và classes. Chúng ta sẽ cùng đi qua một ví dụ minh họa, là một Order model trong một ứng dụng bán hàng online. Order model có những phương thức để tìm kiếm theo trạng thái hoặc nâng cao. Nó cũng có những hàm phục vụ cho việc trích xuất dữ liệu ra XML, JSON, PDF.
# app/models/order.rb
class Order < ActiveRecord::Base
def self.find_purchased
# ...
end
def self.find_waiting_for_review
# ...
end
def self.find_waiting_for_sign_off
# ...
end
def self.find_waiting_for_sign_off
# ...
end
def self.advanced_search(fields, options = {})
# ...
end
def self.simple_search(terms)
# ...
end
def to_xml
# ...
end
def to_json
# ...
end
def to_csv
# ...
end
def to_pdf
# ...
end
end
Khi nhìn nhận rằng đây chỉ là một ví dụ nhỏ của những dòng code trong Order class, thì chúng ta dễ dàng nhận thấy models có thể nhanh chóng vượt ra khỏi tầm kiểm soát. Tất nhiên chúng ta có quyền ước rằng, điều đó làm cho chúng ta thấy model sẽ vượt quá 1000 dòng code trong một file. Mà sự béo ra, to ra của model có xu hướng tới từ những lập trình viên Ruby On Rails mới vào nghề. Vấn đề khó khăn trong việc bảo trì và khả năng đọc để nắm logic của việc béo ra của model như vậy sẽ ngày càng rõ ràng. Trong những giải pháp tiếp theo, chúng ta sẽ dễ dàng thấy được cách mà Ruby On Rails làm cho bạn khi nhóm các phương thức liên quan thành các module. Sau đó, chúng ta sẽ tìm hiểu về những vấn đề cơ bản đằng sau những overweight (thừa cân ) class và thấy được tại sao một số phương thức nên được đưa sang lớp khác của riêng nó.
1.2.1 Delegate Responsibility to New Classes
Trong những giải pháp trước, chúng ta đã thảo luận việc sử dụng Ruby Module cho phân chia những phương thức có liên quan. Đây là một technique rất hữu ích cách ly code hoàn toàn vì lý do dễ đọc. Nó cũng là technique mà nhiều lập trình viên Ruby On Rails thường sử dụng. Cùng xem Order class.
# app/models/order.rb
class Order < ActiveRecord::Base
...
def to_xml
# ...
end
def to_json
# ...
end
def to_csv
# ...
end
def to_pdf
# ...
end
Thông thường, code mà chúng ta muốn di chuyển vào một module là code không phù hợp với class gốc. Những hàm ở trên thực sự không phải là một phần của Order object. Một Order object nên thực hiện những hành vi như: tính toán giá, quản lý các line items, ....
Keepin’ It Classy
Khi code không thuộc về class chứa nó, thì đó là lúc chúng ta nên cấu trúc lại code tới một class nó nên thuộc về. Đây là một áp dụng của Single Responsibility Principle (SRP). Trong khi tinh thần của nguyên tắc này luôn luôn tồn tại như một phần của Object Oriented Design (OOD), thuật ngữ này lần đầu tiên được đưa ra bởi Robert Cecil Martin, trong bài báo "SRP: The Single Responsibility Principle". Martin tóm tắt nguyên tắc này như sau: "Không bao giờ có nhiều hơn 1 lý do cho việc thay đổi 1 class".
Có nhiều class đảm nhiệm nhiều hơn một nhiệm vụ và điều này dẫn tới code dễ bị ảnh hưởng khi yêu cầu thay đổi. Theo thời gian, nhiệm vụ của một class bắt đầu lẫn lộn, và thay đổi hành vi dẫn tới những thay đổi trong phần còn lại của class là tốt. Chúng ta có thể áp dụng SRP vào model Order bằng cách chia những phương thức chuyển đổi tới một class khác gọi là OrderConverter
# app/models/order.rb
class Order < ActiveRecord::Base
def converter
OrderConverter.new(self)
end
end
# app/models/order_converter.rb
class OrderConverter
attr_reader :order
def initialize(order)
@order = order
end
def to_xml
# ...
end
def to_json
# ...
end
def to_csv
# ...
end
def to_pdf
# ...
end
end
Bằng cách này, chúng ta đã đưa những hàm chuyển đổi về đúng nơi, bên trong một class riêng biệt và dễ dàng kiểm chứng. Tạo ra một PDF từ một order chỉ cần gọi như sau: @order.converter.to_pdf
Trong vòng đời object-oriented, điều này được biết tới là composition. Order object bao gồm một OrderConverter object và những object khác mà nó cần. Trong Rails association giữa các model (has_one, has_many, belongs_to) luôn tạo composition một cách tự động cho models.
Breaking the Law of Demeter
Mặc dù chuỗi được giới thiệu phần trước đề cao SRP, nó lại vi phạm Law of Demeter. Chúng ta có thể xử lý bằng cách sử dụng delegate như sau:
# app/models/order.rb
class Order < ActiveRecord::Base
delegate :to_xml, :to_json, :to_csv, :to_pdf, to: :converter
def converter
OrderConverter.new(self)
end
end
Bây giờ chúng ta có thể gọi như sau: @order.to_pdf
. Law of Demeter và Rails delegate chúng ta đã thảo luận trong chương này ở phần giải pháp "Follow the Law of Demeter".
Crying All the Way to the Bank Kỹ thuật này chỉ để mô tả việc phân thành một class chức năng riêng biệt nằm trong class đầu tiên thông qua composition, là cơ sở để đảm bảo một ví dụ thứ 2. Class BankAccount như sau:
# app/models/bank_account.rb
class BankAccount < ActiveRecord::Base
validates :balance_in_cents, presence: true
validates :currency, presence: true
def balance_in_other_currency(currency)
# currency exchange logic...
end
def balance
balance_in_cents / 100
end
def balance_equal?(other_bank_account)
balance_in_cents == other_bank_account.balance_in_other_currency(currency)
end
end
Để một bank account hoạt động bình thường, giống như là chuyển tiền, nhận tiền, đóng, mở tài khoản, class này cần có những hàm để trả về số tiền, việc so sánh và chuyển sang những đơn vị tiền khác. Rõ ràng, class này đang làm quá nhiều việc. Nó tham gia vào các hành vi của bank account và tiền nói chung. Để thực hiện theo lý luận của Martin, chúng ta cần chỉnh sửa lại bằng cách chuyển phần tính toán, so sánh tới một lớp đại diện cho tiền. Trong Rails cung cấp một phương phức composed_of
. Và áp dụng chúng ta có được như sau:
# app/models/bank_account.rb
class BankAccount < ActiveRecord::Base
validates :balance_in_cents, presence: true
validates :currency, presence: true
composed_of :balance, class_name: "Money",
mapping: [%w(balance_in_cents amount_in_cents), %w(currency currency)]
end
# app/models/money.rb
class Money
include Comparable
attr_accessor :amount_in_cents, :currency
def initialize(amount_in_cents, currency)
self.amount_in_cents = amount_in_cents
self.currency = currency
end
def in_currency(other_currency)
# currency exchange logic...
end
def amount
amount_in_cents / 100
end
def <=>(other_money)
amount_in_cents <=> other_money.in_currency(currency).amount_in_cents
end
end
Việc gọi những phương thức cũng khá đơn giản, ví dụ để xem tiền theo đơn vị tiền tệ là usd, chúng ta gọi như sau: @bank_account.balance.in_currency(:usd)
. Hoặc để so sánh chúng ta dùng: @bank_account.balance > @other_bank_account.balance
.
Kết luận Tóm lại, nó là chưa đủ để làm giảm độ phức tạp của 1 model bằng cách di chuyển những phương thức tới module. Nếu 1 class có nhiều hơn một phần chức năng hoặc có nhiều hơn 1 lý do để thay đổi thì chúng ta cần phần chia thành nhiều class khác nhau. Mỗi class sẽ đảm nhiệm những công việc chuyên biệt và điều này làm tăng khả năng bảo trì.
Chúng ta sẽ tiếp tục phần sau với giải pháp: Make Use of Modules, tạm dịch là sử dụng module hóa
All rights reserved