Monkey Patching without a mess
1. Tản mạn
Monkey Patching
là 1 cách cho phép lập trình viên có thể mở rộng hoặc chỉnh sửa 1 hệ thống phần mềm 1 cách tạm thời và cục bộ. Ruby là 1 ngôn ngữ linh hoạt, điều này được thể hiện rõ qua Monkey Patching.
Tất cả các class trong ruby đều là Open Class
, do đó chúng ta có thể thay đổi bất cứ class naof ở bất cứ nơi đâu: thêm mới, override các methods đã tồn tại. Nghe có vẻ rất hay, mềm dẻo, linh hoạt. Tuy nhiên chính điều này làm cho Monkey path trong Ruby trở thành 1 con dao 2 lưỡi. Chúng ta sẽ tự tay cắt mình nếu như không control được tầm ảnh hưởng của nó.
Bài viết dưới đây sẽ đưa ra 1 vài hướng dẫn để giúp bạn kiểm soát tốt hơn các monkey patching methods của mình.
2. Monkey patching without making a mess
2.1 Đặt các monkey patching methods vào trong module
Ví dụ bạn muốn check 1 ngày có phải là cuối tuần hay không, bạn có thể viết:
class DateTime
def weekday?
!sunday? && !saturday?
end
end
Tuy nhiên, lời khuyên là: Khi bạn monkey patch 1 class, không nên chỉ đặt methods vào trong tên class như vậy.
Lý do:
- Nếu 2 thư viện cùng monkey-patch cùng 1 method, bạn sẽ không thể nào sử dụng được 1 trong 2 methods: Ví dụ: Trong gem rubocop có monkey-patch một method strip_indent. Nếu bạn cũng có ý định monkey-patch 1 method cho
String
và tình cờ cũng đặt tên method đúng như vậy thì sao? Method được monkey-patch trước sẽ bị ovverwritten và biến mất :D - Nếu như có lỗi xảy ra, log sẽ báo ra lỗi giống như lỗi đó xảy ra trong class
DateTime
-> Khó debug hơn. - Sẽ khó để bỏ require cho các monkey patches: Nếu bạn viết code như trên, và muốn loại bỏ methods đã được monkey patches, bạn chỉ có cách comment toàn bộ file, hoặc skip việc require files đó.
- Nếu bạn quên thêm dòng
require 'date'
trước khi chạy monkey patch này, bạn sẽ vô tình định nghĩa lại classDateTime
thay vì patching nó.
Thay vào đó, hãy đặt các monkey patches vào trong 1 module:
module CoreExtensions
module DateTime
module BusinessDays
def weekday?
!sunday? && !saturday?
end
end
end
end
Bằng cách này, bạn có thể nhóm các monkey patches lại với nhau. Nếu có lỗi xảy ra, nó sẽ chỉ chính xác xem vấn đề nằm ở đâu. Và bạn có thể include/ exclude chúng 1 cách đơn giản: comment or uncomment 1 dòng.
# Actually monkey-patch DateTime
DateTime.include CoreExtensions::DateTime::BusinessDays
2.2 Đặt các monkey patching methods cạnh nhau
Khi bạn muốn monkey patch các core classes, thực tế là bạn đang add thêm core Ruby APIs. Do đó, khi bạn vừa vào 1 project, liệu có cách nào để biết được nó có monkey patch core API không? Thật may là phần lớn các ruby dev đều có conventions cho monkey patching.
Trong Rails app, các file monkey patch sẽ được đặt tại đường dẫn lib/core_extensions/class_name/group.rb
. Với đoạn code bên trên:
module CoreExtensions
module DateTime
module BusinessDays
def weekday?
!sunday? && !saturday?
end
end
end
end
sẽ được đặt trong: lib/core_extensions/date_time/business_days.rb
.
Do đó, bất cứ 1 new dev nào cũng có hể tìm kiếm Ruby files trong lib/core_extensions
và học cách để thêm mới 1 ruby APIs.
2.3 Sử dụng Refinements để giới hạn tầm ảnh hưởng của các methods monkey patches
Bạn có thể đọc thêm về refinements.
Với refinements, bạn có thể monkey-patch 1 class mà không làm ảnh hưởng tới mọi chỗ sử dụng class đó trong project. Refinement sẽ chỉ active trong 1 module khi nào bạn muốn và require nó vào.
module M
refine String do
def titleize
# Implementation
end
end
end
Nếu muốn sử dụng, bạn cần dùng hàm using
để gọi nó:
# In a file
# not activated here
using M
# activated here
class Foo
# activated here
def foo
# activated here
end
# activated here
end
# activated here
In a class:
# not activated here
class Foo
# not activated here
def foo
# not activated here
end
using M
# activated here
def bar
# activated here
end
# activated here
end
# not activated here
Như vậy, chúng ta có thể giới hạn tầm ảnh hưởng của 1 monkey patch methods. Tránh việc dính bug mà k hiểu nguyên nhân tới từ đâu.
2.4 Hãy nghĩ về các ảnh hưởng của monkey patching methods bạn viết
Khi bạn monkey patch 1 class, bạn thường implement những method phù hợp với hoàn cảnh sử dụng lúc đó. Điều này có thể hữu ích ngay lúc đó, tuy nhiên điều này có thể khiến bạn hoặc người đọc code bối rối nếu đọc lại sau này.
Hãy suy nghĩ về những side effections. Dưới đây là 1 số gợi ý cho bạn:
- Handle các input vào: Hãy luôn cẩn thận với các input đầu vào, Nó có thể là
nil
,string
,array
, … Cố gắng cover các trường hợp, raise exception khi cần thiết, hoặc đơn giản,to_s
nếu cần trước khi execute code của mình. - Handle error một cách rõ ràng: Chúng ta có thể dễ dàng throwing
ArgumentError
với message nếu thấy 1 input không đúng với expect. Cố gắng không để raiseNoMethodError
, nó sẽ khó cho người dùng khi debug. - Lưu lại comment về method bạn viết: Nên có comment cho các methods của bạn, nó dùng để làm gì, nhận vào tham số như thế nào, … để mọi nguời có thể dễ dàng sử dụng khi đã hiểu được bạn muốn làm gì với method đó.
3. Kết luận
Monkey patching core classes không phải lúc nào cũng là không tốt. Nếu bạn làm đúng cách, nó sẽ khiến code bạn trông sạch hơn, rõ ràng hơn khi đọc (và trông “ruby” hơn :D). Tuy nhiên, nó cũng có thể là con dao hai lưỡi khiến việc debug trở nên khó khăn.
Hãy đặt các patches cạnh nhau, nhóm trong 1 modules, handle các tác nhân không mong muốn để monkey patches giữ an toàn hơn cho code của bạn :D
References: