Created
May 21, 2024 14:08
-
-
Save alessandro-fazzi/6397107b8b354f2fbf7be079e6546073 to your computer and use it in GitHub Desktop.
[Ruby] Ancillary objects and inheritance: an alternative take
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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