DEV Community

loading...

Crystal - Plugin Pattern

matthewmcgarvey profile image Matthew McGarvey ・2 min read

The Inspiration

I recently added Lucky to TechEmpower benchmarks and was looking around for articles/videos about what other frameworks are doing to perform so well. I came across this really good talk by Jeremy Evans called Optimization Techniques Used by the Benchmark Winners. Instead of carrying away a list of changes to make to Lucky, I came away with a fascination for how he designed his framework, Roda. Specifically, I loved the idea of entirely using plugins to extend and add behavior. If you ever get the chance to look at the source code for the framework you'll notice that the classes are almost empty. All the behavior is added in through plugins. I really wanted to see if this was possible in Crystal.

The Challenge

In Roda, when you add a plugin, it includes and extends nested modules.

plugin CustomRender

# becomes...

include(CustomRender::InstanceMethods) if defined?(CustomRender::InstanceMethods)
extend(CustomRender::ClassMethods) if defined?(CustomRender::ClassMethods)
# and more...
Enter fullscreen mode Exit fullscreen mode

It also adds modules to the request and response objects but that is enough code to convey the challenges with converting it to Crystal:

  • modules can't be included at runtime
  • there is no defined? method

The Solution

While Crystal can't include modules at runtime, we do have macros that can accomplish much of the same things. The original idea was to just have a macro like:

macro plugin(type)
  include {{ type }}::InstanceMethods
  extend {{ type }}::ClassMethods
end
Enter fullscreen mode Exit fullscreen mode

The only problem with that is the conditional aspect that Roda has. If the plugin doesn't need to add any class methods it shouldn't have to define that module and the framework should be able to handle it. The only solution I've found is to delegate to a second macro.

macro extend_self(instance_methods, class_methods)
  {% if ims = instance_methods.resolve? %}
    include {{ ims }}
  {% end %}
  {% if cms = class_methods.resolve? %}
    extend {{ cms }}
  {% end %}
end

macro plugin(type)
  extend_self({{ "#{type}::InstanceMethods".id }}, {{ "#{type}::ClassMethods".id }})
end
Enter fullscreen mode Exit fullscreen mode

The reason for the second macro is that I need the Path#resolve? method in order to check if the module is defined and this is the only way I know to dynamically create a Path object in a macro. Please let me know if you can think of a better way to do it!

plugin CustomRender

# becomes...

extend_self(CustomerRender::InstanceMethods, CustomRender::ClassMethods)

# becomes...

include CustomerRender::InstanceMethods
extend CustomRender::ClassMethods
Enter fullscreen mode Exit fullscreen mode

I haven't gotten to actually converting Roda over to Crystal yet but I hope to. I also hope to make more posts about it along the way.

Discussion (0)

pic
Editor guide