Abstract classes in Ruby - losing the security blanket
I recently built a Sinatra app and used an object-relational mapping library (ORM) to persist my models. Since deploying the app, I have become concerned that the use of public methods added by the ORM to my model classes has bled in to my controllers. I've therefore created a dependency on this specific ORM throughout my code.
My initial reflex on my next project was to create an abstract class to represent a storage system and a descendant concrete class to use whatever ORM I chose today. My model classes would serve as input and output for concrete storage class and they would have no knowledge at all about ORMs nor about my own storage system.
Having decided that this was my preferred approach, I searched online for `ruby abstract class` and rapidly came to realise that this was not actually a feature of Ruby. I'm not the only person who has looked for this information. Stack Overflow has an impressive amount of activity on this topic.
What is an abstract class for?
Abstract classes are a tool to aid developers to "program to an interface", as the Gang of Four would put it. By eliminating any mention of concrete classes in client code, programmers are able to "swap out" the concrete class being used without rewriting that client code. To many programmers this is self-evidently A Good Thing. In many languages (my experience is in C-family languages) the compiler will not permit an abstract class to be instantiated. This provides a check at compile time that the programmer has implemented all the inherited abstract methods in their concrete class. The abstract class therefore serves as a contract to provide a certain set of methods.
Why doesn't Ruby have abstract classes?
The implication in my description of the purpose of an abstract class is that some aspect of the client code refers to the type of the objects that the client code is handling. Not so in Ruby where there are no variable type declarations. If a programmer decides to swap out their "concrete" class for any other class then this will not affect any client code so long as the replacement class implements public methods with compatible signatures to those of the original class. This is the reality of a duck-typed language.
Many of the stack exchange answers on this topic point out that Ruby is extraordinarily dynamic. Methods can be added to individual objects at run time, meaning that you can't rule out a given object's ability to respond to a given message until the point at which that message is sent to the object. An argument can be made that if a method's implementation can be delayed until just before the method is called then a mechanism for determining the completeness of an object's interface before this point is redundant. Of course, many of us rarely use such a high level of dynamism.
Would implementing abstract classes in Ruby be useful?
You might be able to implement something to warn you at object instantiation if the new object fails to implement an interface defined by a superclass. (If you actually call a non-existent method then Ruby will raise an exception.) The pertinent question is whether an abstract class would help you to program to an interface. A contract to honour a given interface sounds great but in the absence of variable type declarations I don't see what role this contract has. I'm still trying to get comfortable with this fact. Almost all my programming career has been spent working with explicitly typed languages and I'm finding it hard to let go of the security blanket of abstract types (which is why I wrote this post.)
So what should I use instead of abstract classes?
How about tests? It's often desirable for the public interface of a replacement class to behave the same way as that of the original class, e.g. for persistence classes talking to different data stores. Tests in this scenario should be directly transferable to the new class except, for example, if you are using mocks to verify messages sent by the objects under test. Sometimes we do want different public behaviour, e.g. when interfacing to strategy classes that encapsulate different algorithms but (in order to be a direct swap-out) the replacement class will still need to respond to the same method signatures, meaning that the existing tests will at least serve as a rough scaffold.
Edit: Having experimented with the approach I proposed to take with my new app, I decided I would be recoding too much of the ORM functionality. Instead I found I was able to find all the methods included in my models from the ORM module and make them private. Now my model classes can use all the functionality of the included ORM but my controllers are safe from evolving unwanted dependencies.
Edit to the edit: I hadn't fully understood one aspect of Ruby method access control: Class methods are unable to call protected (or private) methods on instances of the class. This defeats my attempt to hide the ORM functionality because it is implemented in a mixture of class and instance methods. Now I have to choose whether to implement my original idea or just cease to worry about this problem altogether. (I have to admit that no one else in the Ruby developement world seems to be worrying about it.)