February 2, 2016 by Daniel P. Clark

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

#binding#lexical scope#local variables#ruby#scope

Comments

  1. 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

    
    def encrypt(str)
      str = validate_string_input(str)
      # ...
    end
    
    • Dmitrii Krasnov
      February 3, 2016 - 2:06 am

      This is how I see this code:

      
      class EncryptionCore
        def encrypt(str)
          validate_string_input!(str)
          str = modify_valid_string(str)
          perform_encription(str)
        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
      
        def modify_valid_string(str)
          str += 'valid'
        end
      
        def perform_encription(str)
          raise NotImplementedError
        end
      end
      
      class FluffyEncryption < EncryptionCore
        private
      
        def perform_encryption(str)
          puts str.reverse
        end
      end
      
      FluffyEncryption.new.encrypt "hello"
      
      • 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 than validate_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.

Leave a Reply

Your email address will not be published / Required fields are marked *