Applied Dynamism

../dinamismo_aplicado/fisanotti.jpg

Author: Juan Pedro Fisanotti

Translator: Claudio Freire

It often happens that when I comment on some superior feature of a language to another person that doesn't know it, the person reacts with phrases like "would you use that in real life?" or "very nice, but that's theory, in practice it doesn't work", etc...

It's normal, since when one's not used to thinking some way, one will never so candidly see the usefulness of some feature. Those aren't great musings of mine, they're ideas that Paul Graham explained very well in this article (recommended).

But what I can indeed give from is my humble experience, it's a good example of how a feature can sound "weird", but when it's well used, it can result "very practical".

There may be many places where what I'm about to say is wrong, and it would be great if you let me know.

The "weird" feature

So here is where Python's "weird" feature comes in. It will only sound "weird" for anyone not used to this kind of behavior, of course.

The feature is the getattr function: calling it, we can obtain an attribute or method of an object. For example:

me = "juan pedro"
met = getattr(me, "upper")
met()   #this returns "JUAN PEDRO"

Shedding some light:

With getattr(me, "upper") we effectively get the upper method of the me object. Attention! I said "get the method", not "the result of calling the method". They're very different things.

Obtaining a method is like giving it a new name with which to call it later, like we did with "met()". met is a new name for that same particular method, the upper method of me.

It's worth mentioning that using a variable (met in this case) is something I did only to make it more clear at first sight. But the earlier code can be rewritten seemly as:

me = "juan pedro"
getattr(me, "upper")()  # this returns "JUAN PEDRO"

We don't store the method in a variable, we just call it right away. We get it, and we call it, all in the same line.

The problem

We have a class Foo. This class Foo defines 5 different actions, each representing 5 methods: action1, action2, action3, action4, action5. The complexity arises from the fact that each of these actions is realized by communicating with a service, and there are 4 completely different services in which you can perform the actions: A, B, C and D.

Example: "Perform action 1 in service B" "Perform action 3 in service D" etc...

In the implementation, each service completely redefines the code executed for each action. That is to say, the code for action 1 in service A is completely different from the code of action 1 in service B, etc.

The Foo class then needs to take the name of the service as a parameter of every action, to know in which service to perform it. So we could use it the following way:

miFoo = Foo()   #we create a new object foo
miFoo.action1("A")  #we call action 1 in service A
miFoo.action1("C")  #we call action 1 in service C
miFoo.action3("B")  #we call action 3 in service B

First "non-dynamic" solution

To many one reading this, the first solution that will come to mind will be that each method (actionX...) must have within itself a big if, for each service. Something like:

class Foo:
  def action1(self, service):
      if service == "A":
          #code for action 1 in service A
      elif service == "B":
          #code for action 1 in service B
      elif service == "C":
          #code for action 1 in service C
      elif service == "D":
          #code for action 1 in service D

This will work, that I won't deny. But... what don't I like of this option? I don't like:

  1. That if will be repeated in each of the actions, of which there are 5. When we add or modify services, I have to maintain the same if in all 5 methods "actionX".
  2. The code quickly becomes unreadable when there's a lot to do per action.
  3. It gives the sensation that we're "mixing" apples and oranges, that this could be better organized.
  4. This ifs are two lines of code for each action and service, so with 5 actions and 4 services, that's 40 lines of code only in ifs, not including the code for the actions themselves. It's 40 lines of code that don't do what we want to do, that we need only to decide what to do.

Enhancing the "non-dynamic" solution

For the apples and oranges and organizational problem, more than one will have had the idea of something like this:

class Foo:
   def action1(self, service):
       if service == "A":
           self.action1_in_A()
       elif service == "B":
           self.action1_in_B()
       elif service == "C":
           self.action1_in_C()
       elif service == "D":
           self.action1_in_D()

   def action1_in_A(self):
           #code of action 1 in service A

   def action1_in_B(self):
           #code of action 1 in service B

   def action1_in_C(self):
           #code of action 1 in service C

   def action1_in_D(self):
           #code of action 1 in service D

I can't deny it, the separation in many methods does help legibility and maintainability a tiny bit. Considering that part resolved, we forget about methods "actionX_in_Y". But we still have this:

def action1(self, service):
   if service == "A":
       self.action1_in_A()
   elif service == "B":
       self.action1_in_B()
   elif service == "C":
       self.action1_in_C()
   elif service == "D":
       self.action1_in_D()

This is what I still dislike. Why? Because we still have the problem of horrible ifs spread everywhere. We still have to maintain those 40 lines of code that only serve for choosing which code to run. Mi opinion is that there has to be a better way.

Weird to the rescue: The dynamic solution

Well, in theory the weird thing should now help us resolve our problem. And how will it help us this weird Python feature? Lets remember now that we had forgotten about all the "actionX_in_Y" methods, those had been approved :). The ugly part of the code was the one choosing which method to run according to the given service.

Lets see, then, the "weird" version of the code:

def action1(self, service):
   getattr(self, "action1_in_" + service)()

See what's missing? No ifs!

Before we had those 40 lines of ifs, 8 lines for each action that only decided which code to run. Now that decision is taken with 1 line of code per action, which means (with 5 actions) a grand total of... 5 lines! 5 lines against 40 is 87% less code. Watch it. The issue isn't "having less lines of code is better". In this case, the advantage is not having to maintain that repetitive and unnecessary code.

And not only that, we also gained another very important advantage: if we added or removed services tomorrow, there's no need to touch the dispatch code. We only add the implementations (methods actionX_in_Y), and the class will know by itself how to call them, without us having to make any change. That's practical.

Conclusion

In a rather simple example, we can see how a "weird" feature correctly used can become a "practical" feature. And be careful, because when one starts using these features, it becomes very tedious going back to those languages that don't have them... it's addictive, hehe.

PS: credit due to César Ballardini who showed me Paul Graham's article :D

PDF version | reSt version

Help PET: Donate

blog comments powered by Disqus

Last change: Thu Sep 9 17:10:11 2010.  -  This magazine is under a Creative Commons license.