Skip to content

Instantly share code, notes, and snippets.

@alessandro-fazzi
Created May 21, 2024 14:08
Show Gist options
  • Save alessandro-fazzi/6397107b8b354f2fbf7be079e6546073 to your computer and use it in GitHub Desktop.
Save alessandro-fazzi/6397107b8b354f2fbf7be079e6546073 to your computer and use it in GitHub Desktop.
[Ruby] Ancillary objects and inheritance: an alternative take
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Was reading about this super interesting article https://www.fullstackruby.dev/object-orientation/2024/05/20/more-robust-class-hierarchies-with-nested-support/ from https://ruby.social/@fullstackruby\n",
"\n",
"But since I actually do implement classes with ancillary classes with a different approach, I'd like to share my take to discover if it survives into the wild.\n",
"\n",
"Followings are classes copy-pasted from the article"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
":strategy"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"class WorkingClass\n",
" def perform_work\n",
" config = ConfigClass.new(self)\n",
"\n",
" do_stuff(strategy: config.strategy)\n",
" end\n",
"\n",
" def do_stuff(strategy:) = \"it worked! #{strategy}\"\n",
"\n",
" class ConfigClass\n",
" def initialize(working)\n",
" @working = working\n",
" end\n",
"\n",
" def strategy\n",
" raise NoMethodError, \"you must implement 'strategy' in concrete subclass\"\n",
" end\n",
" end\n",
"end\n"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
":strategy"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"class WorkingHarderClass < WorkingClass\n",
" class ConfigClass < WorkingClass::ConfigClass\n",
" def strategy\n",
" # a new purpose emerges\n",
" \"easy as pie!\"\n",
" end\n",
" end\n",
"end"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"NOTE: I'm wrapping the execution in a begin/rescue just in order to keep the notebook running despite the exception"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Actually got NoMethodError: you must implement 'strategy' in concrete subclass\n"
]
}
],
"source": [
"begin\n",
" WorkingHarderClass.new.perform_work\n",
"rescue NoMethodError => e\n",
" puts \"Actually got NoMethodError: #{e.message}\"\n",
"end"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And this is my alternative take on the matter, where I use a really standard DI pattern using parameterized constructor with default values"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
":strategy"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"class FooWorkingClass\n",
" def initialize(config: ConfigClass)\n",
" @config = config.new(self)\n",
" end\n",
"\n",
" def perform_work\n",
" do_stuff(strategy: @config.strategy)\n",
" end\n",
"\n",
" def do_stuff(strategy:) = \"it worked! #{strategy}\"\n",
"\n",
" class ConfigClass\n",
" def initialize(working)\n",
" @working = working\n",
" end\n",
"\n",
" def strategy\n",
" raise NoMethodError, \"you must implement 'strategy' in concrete subclass\"\n",
" end\n",
" end\n",
"end\n"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
":initialize"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"class FooWorkingHarderClass < FooWorkingClass\n",
" class ConfigClass < FooWorkingClass::ConfigClass\n",
" def strategy\n",
" # a new purpose emerges\n",
" \"easy as pie!\"\n",
" end\n",
" end\n",
"\n",
" def initialize(config: ConfigClass) = super\n",
"end"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"\"it worked! easy as pie!\""
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"FooWorkingHarderClass.new.perform_work"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This way, speaking about\n",
"\n",
"> What’s also nice about this pattern is you can easily swap out supporting classes on a whim, perhaps as part of testing (automated suite, A/B tests, etc.)\n",
"\n",
"you are able to (simply?) do something like this"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"\"it worked! It's mocked like a charm!\""
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
" class TestConfigClass < FooWorkingClass::ConfigClass\n",
" def strategy\n",
" # a new purpose emerges\n",
" \"It's mocked like a charm!\"\n",
" end\n",
" end\n",
"\n",
" FooWorkingHarderClass.new(config: TestConfigClass).perform_work"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"that's much easier than what's done in this snippet from the article:\n",
"\n",
"```ruby\n",
"# Save a reference to the original class:\n",
"_SavedClass = WorkingHarderClass::ConfigClass\n",
"\n",
"# Try a new approach:\n",
"WorkingHarderClass::ConfigClass = Class.new(WorkingClass::ConfigClass) do\n",
" def strategy = \"another strategy!\"\n",
"end\n",
"\n",
"WorkingHarderClass.new.perform_work # => \"it worked! another strategy!\"\n",
"\n",
"# Restore back to the original:\n",
"WorkingHarderClass::ConfigClass = _SavedClass\n",
"\n",
"WorkingHarderClass.new.perform_work # => \"it worked! easy as pie!\"\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And I have to be honest about what I think about the latter approach: that IS monkey patching in my feeling."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Ruby 3.3.0",
"language": "ruby",
"name": "ruby"
},
"language_info": {
"file_extension": ".rb",
"mimetype": "application/x-ruby",
"name": "ruby",
"version": "3.3.0"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment