11 de noviembre de 2010

Delegación simple con Python

Al estudiar el libro "Design Patterns: Elements of Reusable Object-Oriented Software", nos damos cuenta que frecuentemente debemos delegar métodos desde una clase "envoltorio" hacia una instancia de una clase "envuelta" (o "sujeto", para usar la terminología de los GOF). Por ejemplo los patrones decorador y proxy son dos ejemplos en los que se requiere hacer esto.

Sin embargo, una situación recurrente al usar estos patrones es que en realidad no queremos redefinir o controlar todos los métodos del sujeto. Algunas veces, solo queremos "decorar" o controlar ciertos métodos y dejar el resto en paz. En lenguajes como Java, C# o (Dios nos guarde) Visual Basic, esto implica escribir una larga lista de métodos que lo único que hacen es delegar hacia el sujeto.

Así, si tenemos una clase a la que nos gustaría decorar, pero únicamente queremos decorar un único método, en Java tendríamos algo así:

interface MySubject
{
  void decorated(char x);
  void foo();
  long bar(int blah);
  int baz(int x, float y, char z)
  ...
}

class MyDecorator implements MySubject
{
  public MyDecorator(MySubject subject)
  {
     this.subject = subject;
  }

  public void decorated(char x) {
    if (x != 'x') {
      self.addBehaviour();
    }
    this.subject.decorated();
    self.addMoreBehaviour();
  }

  private void addBehaviour() { ... }

  private void addMoreBehaviour() { ... }

  public void foo() {
    this.subject.foo();
  }

  public long bar(int blah) {
    return this.subject.bar(bla);
  }
  ....
}

etc.

En definitiva es mucho trabajo. En python es tan sencillo como definir un solo método (aunque, hay que decirlo, se trata de un método "mágico"). Dicho método es __getattr__. Este método es llamado por python cada vez que la búsqueda de un atributo en un objeto falla. Es decir, si nosotros escribimos:

foo.bar()

Pero el método bar no existe en la clase del objeto foo, entonces obtendremos una linda excepción AttributeError. Sin embargo, python nos da una oportunidad de atrapar estos "eventos" antes de levantar la excepción. Si nuestra clase define el método mágico __getattr__, python lo mandará llamar cada vez que no pueda encontrar el atributo solicitado.

Esto significa que nuestro decorador en java se simplifica significativamente al implementarlo en python:

class MyDecorator(object):
    def __init__(self, subject):
        self._subject = subject

    def decorated(self, x):
        if x != 'x':
            self._addBehaviour()
        self._subject.decorated()
        self.:addMoreBehaviour()

    def _addBehaviour(self):
        ...

    def _addMoreBehaviour(self):
        ...

    def __getattr__(self, name):
        return getattr(self._subject, name)

¡Eso es todo!

  1. En primer lugar en python no necesitamos definir una interfase ya que utiliza duck typing.

  2. En segundo lugar, con solo definir __getattr__, automáticamente estamos delegando todos los métodos que no definimos explícitamente hacia _subject,

De esta forma, si escribimos:

myobj = MyDecorator( SomeObject() )
myobj.decorated('x')

pasará exactamente lo mismo que en la versión Java. Sin embargo, si escribimos esto:

myobj = MyDecorator( SomeObject() )
print myobj.bar(25)

python tratará de invocar el método bar en myobj. Como ese método no existe en la clase MyDecorator, python seguidamente buscará el método __getattr__. En este caso, la búsqueda tiene éxito por lo que __getattr__ es invocado. Este a su vez nos regresa una referencia al método bar de la clase SomeObject y finalmente python puede llamar al método.

¿Pero, qué pasa si el método llamado tampoco existe en el sujeto? Bueno, el contrato para __getattr__ indica que de no encontrar el atributo solicitado, se debe levantar una excepción AttributeError. Y eso es exactamente lo que hace la función getattr si solicitamos un atributo que no existe, por lo tanto, cumplimos al 100% con nuestro contrato, tanto con python como con el código cliente, que ni siquiera se entera que no está hablando con una instancia de SomeObject real.

Esto es tan útil que he hecho una pequeña clase de utilería para usarla una y otra vez:

class SimpleDecorator( object ):
    u'''
    Es un "mixin" que ayuda en la implementación de patrones de diseño, al 
    permitir la delegación automática de los mensajes que el objeto no 
    entienda hacia otro objeto "subject".
    '''
    def __init__( self, subject ):
        self._subject = subject

    def __getattr__( self, name ):
        return getattr( self._subject, name )

Saludos