Ruby: Bindings Across Inheritance
One thing you will find yourself needing to do is work across different scopes and in different ways. I would like to show one way of modifying local variables by passing a Binding object.
Let’s say you’re going to write encryption classes and you’ll have different ways of encrypting and decrypting. For these you will have some common methods among them so we should create a common class to be inherited.
class EncryptionCore end class FluffyEncryption < EncryptionCore end class ChewyEncryption < EncryptionCore end
Now each of these classes will need to define an encypt and decrypt method and they will have unique code for their kind of encryption. But there will be some code that needs to be done in each object. Like verifying the input is appropriate.
Let’s say the input needs to be a string and at least 4 characters long. Now we can do part of the methods in the EncryptionCore and just super it like this…
class EncryptionCore def encrypt(str) raise(TypeError, "Needs to be a String") unless str.is_a? String raise(StandardError, "Needs to be at least 4 characters long") unless str.length > 3 # NO CODE HERE, LET INHERITED METHODS FINISH end end class FluffyEncryption < EncryptionCore def encrypt(str) super # do the verificaion stuff for input # CODE HERE FOR ENCRYPTING end end
And this would work. Since this is not the job of the method, as described by the method name, to validate input it would be wise to move it to a private method.
class EncryptionCore def encrypt(str) validate_string_input(str) # NO CODE HERE, LET INHERITED METHODS FINISH end private def validate_string_input(str) raise(TypeError, "Needs to be a String") unless str.is_a? String raise(StandardError, "Needs to be at least 4 characters long") unless str.length > 3 end end class FluffyEncryption < EncryptionCore def encrypt(str) super # do the verificaion stuff for input # CODE HERE FOR ENCRYPTING end end
Now since it is a private method we can reuse it for our decrypt method as well. But the EncryptionCore class doesn’t actually encrypt and decrypt objects. So having the methods encrypt and decrypt in it are misleading.
Also at this point we’ve moved the code to the private method and now have no need to call super since the private method is callable from the class that inherits it. Let’s look at an example where the private method “modifies” a local variable.
class EncryptionCore private def process_string_input(str) raise(TypeError, "Needs to be a String") unless str.is_a? String raise(StandardError, "Needs to be at least 4 characters long") unless str.length > 3 str += "valid" end end class FluffyEncryption < EncryptionCore def encrypt(str) process_string_input(str) puts str end end FluffyEncryption.new.encrypt(5) #TypeError: Needs to be a String FluffyEncryption.new.encrypt("hello") #hello # => nil
The validation code works, but the local variables have not changed. As you can see the error was raised properly but any local variables we try to modify aren’t being modified.
Now in comes Binding. With Binding we can pass a working scope somewhere else in our code base to allow access to local variables.
class EncryptionCore private def process_string_input(scope) str = scope.local_variable_get(:str) raise(TypeError, "Needs to be a String") unless str.is_a? String raise(StandardError, "Needs to be at least 4 characters long") unless str.length > 3 str += "valid" scope.local_variable_set(:str, str) end end class FluffyEncryption < EncryptionCore def encrypt(str) process_string_input(binding) puts str end end FluffyEncryption.new.encrypt("hello") #hellovalid # => nil
See! The local variable has been changed from the inherited class ! Now we can write as many different encryption type classes that we want that can reuse private methods in EncryptionCore and it can process our local variables within each methods scope! Viola! You’ve passed the binding from the inheriting class to the inherited one and set up whatever local variable changes you wanted.
Summary
It makes sense to separate parts that do and don’t belong. When you’re writing a class for common methods to be inherited but have some methods that don’t belong in it – it becomes very useful to work with bindings across classes. Each FluffyEncryption and ChewyEncryption classes define their own unique encrypt and decrypt methods which CoreEncryption doesn’t. And you have the full power to share common processing code between them with passing in a binding and setting/getting local variables. This keeps the code clean, methods are where they belong and people won’t get confused with deceptive methods in CoreEncryption.
As always I hope you’ve enjoyed this! Please comment, share, subscribe to my RSS Feed, and follow me on twitter @6ftdan!
God Bless!
-Daniel P. Clark
Image by Philippa Willitts via the Creative Commons Attribution-NonCommercial 2.0 Generic License
Dmitrii Krasnov
February 3, 2016 - 2:04 am
Local variable is changing inside validate_string_input, but method name says nothing about that change. I would say that this binding magic is even more confusing than write
Dmitrii Krasnov
February 3, 2016 - 2:06 am
This is how I see this code:
Daniel P. Clark
February 3, 2016 - 2:32 am
I like your use of raising a “not implemented” error.
dhalai
February 28, 2016 - 8:06 am
I definitely agree with you. In the first implementation `validate_string_input` has a side effect, which can be very confusing. By the way, thx Daniel for showing one more way.
Daniel P. Clark
February 3, 2016 - 2:38 am
Haha… magic 🙂 . I’ll change the name to reveal it’s function as you’ve indicated.
One thing I really don’t like about binding is that I have to use it in the scope it’s from as a parameter. What I really want is something like the gem binding_of_caller so that I can monkey with the caller’s scope. But that would probably be bad language design as far as Ruby is concerned. It’s best to protect scopes.
I like the assignment of
str = validate_string_input(str)
less thanvalidate_string_input(binding)
as far as appearance goes so that’s personal preference. Private methods are where the ugly looking code should go… not the front facing public methods.I’m merely demonstrating another part of the Ruby language available to us all.