12/08/2018, 16:21

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.

0