Monday, March 19, 2012

The Interface Segregation Principle in Dynamically Typed Languages

When I first heard that 'duck-typing is the interface' it only meant one thing to me; it meant that I could not explicitly use an interface. The ramifications of this were not immediately apparent, but what did that matter? I had duck-typing! Later on I started to understand what it meant to have implicit interfaces. Only after did I have a few 'fix this forward facing class and watch everything break' refactorings did it start to sink in. I needed to really treat certain classes and modules differently than the rest of my code. I needed to create pieces that I could depend on. I needed pieces that were static because they encapsulated ideas that should not be changing often. I wanted to switch the implementation at will and the only way I was going to accomplish this was to really understand that duck-typing is the interface.
When I say create a class to depend on it means a couple of things. First, it means that the class is for the client and not for the implementer. This if often said of interfaces in a statically typed language so of course it holds true for an implicit interface. An example would be any gem worth using (pick your favorite). They provide a forward facing interace that is not meant to change rapidly and does not change for weak reasons. Gems are to be used by clients and are meant to be predictable with each release. Imagine if you had to rewrite your code with each gem version because the implementer had a new idea for code arrangement. You would not do it, you would stick with the version you were previously using, missing out on the new tweaks and features. For this reason the interface we provide in dynamically typed languages needs to be static and only change for a very well thought out reason. This way the client can have a high confidence in their expectations of the code. Remember that pulling a method out from under a client in a dynamically types language causes run time exceptions (although, they ought to be testing).

What if the implicit interface being provided is staring to feel bloated and too big. It started out as one coherent idea but is now many fragmented ideas? Then, it is time to break it up into smaller interfaces. We do not want to sacrifice the dependability, but we also do want to bring our classes and modules back to a coherent state. It is then time to refactor to smaller classes, however, it is important to keep the client of the interfaces in mind. It is entirely possible for classes to 'implement' two or more interfaces keeping in mind that our implicit interfaces are forward facing for the client's use. Moving forward, however, it is important to remember that we have two or more ideas being implemented in this module and that these ideas do not always need to be implemented together.

When we provide a public API to our code we allow ourselves to change the implementation at will. What if, for example, we provide a class that talks to an external service. Behind this interface we manipulate the data we receive from the service and return only what is necessary to the client. We expose this to our client through an implicit interface. Then, one day, the backend service changes completely. We can still receive and use the same data but how we retrieve this data is completely different. So, of course, the implementation changes. But, our client of our interface never has to know about the implementation changing. We can pass a completely different class around, but since it conforms to the interface the client never knows, and frankly, never needs to. The client never noticed a disruption in service because the client's expectations of the interface were always met.

Uncle Bob writes about a copy program when discussing the Dependency Inversion Principle. I think it is a great read and it really helped me understand what it meant to invert dependencies when I was still struggling with the idea. What I wanted to highlight is not really the dependency inversion, but the interface segregation that also takes place. I'll rewrite the system in ruby to show what I mean.


When we look at this it looks trivial. Why not just call gets and puts directly instead of putting them in a class? Well, let's see what this looks like when the implementations change.


We want the client to use the file read and write instead of the standard IO read and write that we were using before. Notice that the client's implementation (the copy method) did not have to change at all. This is only accomplishable because we hid both implementations behind the Reader and Writer interfaces respectively. This is the power of thinking with interfaces in a dynamically types language.

Although it is not explicitly declared, it is important to remember the Interface Segregation Principle. With ISP we can create dependable interfaces for our clients. We can also switch implementations at will without having to wait and see what breaks. These are powerful tools to use and they ought to use them in our dynamic languages. Even if interfaces are not explicit we still have access to the idea.

1 comment:

  1. This is an interesting entry Patrick. Translating SOLID to dynamic languages is tricky. As you mentioned the other day, the examples in Uncle Bob's PPP book really depend on statically typed languages but the ideas are still of value in the dynamic world. This certainly helped me in understanding ISP.

    ReplyDelete