| EN | ru |
|---|
is-dsl
The IS::DSL module implements the Facade design pattern and is primarily intended
for defining various kinds of DSL (Domain Specific Language).
How it works
The principle is simple: we define a module, execute extend IS::DSL within the module context
or externally, and specify a set of constants and methods that it will delegate to other modules and/or
singleton objects.
Why is this needed?
Primary use case: We have a set of logically related modules and classes, some of their methods
are needed for interaction between them (and should thus be marked as public), but are not needed
for the external library user. For this external user, we implement a "facade" — a module containing
only what's necessary.
This becomes especially useful for DSL, when we want to provide (via include) a set of methods
without prefixes and namespaces, while not exposing too much to reduce the likelihood of conflicts
and unwanted overrides.
Delegated methods are defined via module_function, allowing their use both as DSL methods through include
and as singleton methods of the module like Mod::my_method.
Shadow Module
Additionally, a shadow module is created, which is largely analogous to the main module and also serves
as a "facade", but is intended for name shortening INSIDE the library. Using the attach_to method,
it is assigned as a constant to some internal module or class.
Methods and constants delegated via encapsulate go to both the main module and the shadow,
while those via shadow_encapsulate go only to the shadow.
Lazy Methods
In Ruby, singleton logic can be implemented in two ways — through singleton methods of modules and classes,
or more traditionally (as in other languages) — through calling a single singleton method of the class
like instance, which always returns the same class instance. Lazy encapsulation is designed for the second approach.
Example
module MyLib
class Report
# Some stuff
class << self
def all_reports
# implementation
end
end
end
class ReportManager
class << self
def instance
@instance ||= new
end
private :new
end
def get_report name
MyLib::all_reports.find name
end
# Some stuff
end
end
module ML
extend IS::DSL
encapsulate MyLib::Report, new: :create_report
shadow_encapsulate MyLib::Report, :all_reports
lazy_encapsulate :get_report do
MyLib::ReportManager.instance
end
attach_to MyLib::ReportManager
end
Here we get two methods: create_report directly calls MyLib::Report::new, while get_report
calls MyLib::ReportManager.instance.get_report, with MyLib::ReportManager.instance being
called only once on the first access to ML::get_report.
The attach_to call allows us inside ReportManager to use ML::create_report instead of
MyLib::Report::new, and ML::all_reports instead of MyLib::Report::all_reports (in this example
it doesn't give much benefit, but in a large library with a complex class structure it can be very useful).