ADDING FUNCTIONALITY TO RUBY CLASSES WITH DECORATORS
Khi sử dụng một api của bên thứ 3 đôi khi chúng ta cần bổ sung thêm chức năng cho nó. Do vì nó là đối tượng đã được đóng gói nên không thể thêm chức năng bằng cách can thiệp vào bên trong nó. Chúng ta có thể không cần làm vậy mà đơn giản chỉ cần viết một đối tượng khác thực hiện các chức năng mở ...
Khi sử dụng một api của bên thứ 3 đôi khi chúng ta cần bổ sung thêm chức năng cho nó. Do vì nó là đối tượng đã được đóng gói nên không thể thêm chức năng bằng cách can thiệp vào bên trong nó. Chúng ta có thể không cần làm vậy mà đơn giản chỉ cần viết một đối tượng khác thực hiện các chức năng mở rộng mà ta đang cần thêm. Nhưng khi những chức năng mở rộng này có liên quan đến các chức năng của api hoặc được thực hiện đồng thời thì việc tách thành 2 đối tượng riêng biệt là rất tối nghĩa và phân tán. Trường hợp này chúng ra cần phải tạo ra một lớp bao hàm các chức năng của api và bổ sung thêm các chức năng mở rộng đang cần. Cần thực hiện việc này một cách tối ưu nhất để khi sử dụng sẽ thuận tiện và dễ hiểu.
Vấn đề đặt ra trong ví dụ cụ thể. Có một api của bên thứ 3 là Stripe(Stripe gem). Api này giúp thanh toán hóa đơn của khách hàng thồng qua phương thức thanh toán là thẻ visa hay mastercard. Chỉ thanh toán không là không đủ khi ta muốn lưu trữ thông tin khách hàng hay hóa đơn mỗi khi thanh toán được thực hiện. Chúng ta cần tạo ra một lớp có thể truy cập dữ liệu của Stripe và mở rộng thêm chức năng lưu trữ thông tin mà vẫn giữ được sự rõ ràng và xuyên suốt.
Giải pháp. Hãy bắt đầu với cách cơ bản nhất để truy cập vào dữ liệu Stripe: Stripe gem:
class AccountsController < ApplicationController before_action :require_authentication def show @customer = Stripe::Customer.retrieve(current_user.stripe_id) @invoices = @customer.invoices @upcoming_invoice = @customer.upcoming_invoice end end
Extract an Adapter Bởi vì chúng ta đang kết nối với hệ thống của bên thứ 3 nên cần thiết tạo ra một local adapter để truy cập vào các phương thức của Stripe, đối tượng này đóng vai trờ thay thế đối tượng stripe để sử dụng trong những nơi cần thiết. Điều này tương đương với việc loại bỏ sự tồn tại của khái niệm Stripe mà thay thê bằng cái gọi là Billing. Đây là các hàm cần thiết của Billing để sử dụng cho AccountsController.
class Billing attr_reader :billing_id def initialize(billing_id) @billing_id = billing_id end def customer Stripe::Customer.retrieve(billing_id) end def invoices customer.invoices end def upcoming_invoice customer.upcoming_invoice end end
Lớp Billing chứa các phương thức để sử dụng trong các trường hợp mà không làm thay đổi chức năng của Stripe một cách có tổ chức hơn, rõ ràng hơn. Và bây giờ thử dùng lớp Billing này trong controller.
class AccountsController < ApplicationController before_action :require_authentication def show billing = Billing.new(current_user.stripe_id) @customer = billing.customer @invoices = billing.invoices @upcoming_invoice = billing.upcoming_invoice end end
Như vậy là chúng ta đã cung cấp các chức năng có sẵn thông qua một lớp trung gian giữa controller và Stripe. Việc làm này hoàn toàn là hợp lý và đúng nghĩa.
Thêm chức năng cho Billing Sau khi đã setup được adapter là lớp Billing, chúng ta sẽ tiến hành cải thiện hiệu suất việc lưu trữ các hành vi của người dùng. Như vậy cần tạo ra một lớp mới có tên BillingWithCache. Lớp này đại diện cho một đối tượng mới có thêm chức năng mới phục vụ chức năng ở một mức cao hơn, được đóng gói cao hơn dưới cách nhìn của người sử dụng. Cách cơ bản để sử dụng decorator là đưa vào đối tượng mà chúng ta đang decorating, ở đây là Billing, và khai báo các phương thức giống với lớp billing nhưng thông qua đối tượng thực hiện được khai báo ở đầu của lớp. Code của lớp BillingWithCache .
class BillingWithCache def initialize(billing_service) @billing_service = billing_service end def customer billing_service.customer end def invoices customer.invoices end def upcoming_invoice customer.upcoming_invoice end private attr_reader :billing_service end
Ở đây mặc dù chúng ta chưa có thêm chức năng mở rộng nào, lớp này chỉ có khả năng gọi các phương thức của lớp Billing và nó trả về các kết quả của Stripe API (#customer, #invoices, #upcoming_invoice). Tích hợp lớp mới này vào AccountsController :
class AccountsController < ApplicationController before_action :require_authentication def show billing = BillingWithCache.new(Billing.new(current_user.stripe_id)) @customer = billing.customer @invoices = billing.invoices @upcoming_invoice = billing.upcoming_invoice end end
Như bạn thấy ta chỉ thay đổi dòng code mà chúng ta cần sử dụng lớp decorated class.
BillingWithCache.new(Billing.new(current_user.stripe_id))
Chắc bạn đang đặt ra câu hỏi là không hề liên quan gì đến việc lưu trữ dữ liệu, chức năng vẫn vậy mà chứ chưa có gì được mở rộng. Nào hãy cùng đào sâu vào lớp BillingWithCache đề thêm điều đó. Adding Caching Functionality Để cache dữ liệu sử dụng Rails.cache, chúng ta cần một cache một key duy nhất. May mắn là lớp Billing cung cấp cho người dùng billing_id nó cho phép tạo ra sự truy cập duy nhất tới người dùng.
def cache_key(item) "user/#{billing_id}/billing/#{item}" end
Trường hợp này là tham chiếu đến "customer", "invoices" hoặc "upcoming_invoice". Thêm vào các lời gọi để cache key.
class BillingWithCache def initialize(billing_service) @billing_service = billing_service end def customer key = cache_key("customer") Rails.cache.fetch(key, expires: 15.minutes) do billing_service.customer end end def invoices key = cache_key("invoices") Rails.cache.fetch(key, expires: 15.minutes) do customer.invoices end end def upcoming_invoice key = cache_key("upcoming_invoice") Rails.cache.fetch(key, expires: 15.minutes) do customer.upcoming_invoice end end private attr_reader :billing_service def cache_key(item) "user/#{billing_service.billing_id}/billing/#{item}" end end
Đoạn code trên cache mỗi 15 phút. Trên đây là một cách wrapped đối tượng của bên thứ 3 và bổ sung thêm chức năng một cách đơn giản nhưng cũng hữu hiệu.