domingo 16 de noviembre de 2008

Fluent Interfaces

Navegando un poco e investigando sobre ideas y tecnología a usar en mi PFC llegué a uno de los muchos artículos de Martin Fowler donde se discute una forma de generar una API, que aunque ya conocía en esencia, no sabía que tenía un nombre: Fluent Interfaces

Básicamente cuando se crea un "interface fluido" se pretende que las operaciones que puedes realizar en un objeto dependan del contexto generado por la operación previa. Con un ejemplo es mucho más sencillo, y corto y pego el que aparece en la wikipedia:
GlutApp app(argc, argv);
app.setDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_ALPHA|GLUT_DEPTH); // Set framebuffer params
app.setWindowSize(500, 500); // Set window params
app.setWindowPosition(200, 200);
app.setTitle("My OpenGL/GLUT App");
app.create();
El código de arriba crea una ventana de visualización OpenGL usando GLUT. Bien, pues el mismo código utilizando el tipo de interface que discutimos sería:
FluentGlutApp app(argc, argv)
.withDouble().withRGBA().withAlpha().withDepth()
.at(200, 200).across(500, 500)
.named("My OpenGL/GLUT App");
app.create();

Personalmente creo que el último ejemplo expresa de forma más clara las operaciones a realizar (inicialización), y sobre todo a qué objeto están dirigidas. Al respecto, y como curiosidad, si habéis utilizado Rhino Mocks como librería de Mocks para vuestros casos de prueba, comprobaréis que a partir de la versión 3.2 añade una sintaxis fluida


Dándole vueltas al asunto, me di cuenta que tenía un proyecto previo que podría beneficiarse -y mucho- de este tipo de interfaces: mi librería para simplificar llamadas utilizando Late Binding en C#, de la que hablé en una entrada anterior. Así que dejando aparcado el PFC un tiempo y dedicándole un par de días, he creado la versión 2.0 2.1 de la librería, cuyo código está subido ya al repositorio.


Una de las mejoras que provoca el nuevo interface es la llamada de métodos con múltiples parámetros. Por ejemplo, supongamos una operación que recibe 2 parámetros, uno de ellos por referencia:

void MyClass.MyOperation(string command, ref int result);
Con la versión 1.* tendríamos que hacer :

ILateBindingFacade lb = LateBindingFactory.CreateObject(typeof(MyClass) );
string cmd = "myCommand";
int result;
object[] args = Args.Build(cmd, result);
lb.Call("MyOperation", Args.ByRefIndexs(1), args);
result = args[1];
Con la nueva versión sin embargo el código queda así:
IInvoker invoker = LateBindingFactory.CreateObject(typeof(MyClass) );
object result;
invoker.Method("MyOperation")
.AddArgument("myCommand")
.AddRefArgument(result)
.Invoke();

result = invoker.LastCallArguments[1];

Vale que es "mi" librería y que los niños de uno nunca son feos, pero a mi me parece muchísimo más elegante y simple que la primera versión, y no sólo se queda en elegancia, sino que las operaciones permitidas dependen del contexto como había hablado anteriormente. Por ejemplo, si en un objeto IInvoker llamamos a la operación Method para especificar el nombre de la operación a invocar sobre él, los únicas operaciones que pueden ejecutarse son AddParameter, AddRefParameter e Invoke, que son las únicas que tienen sentido.


La legibilidad aumenta según más complejo es el uso. Esto se puede ver si retomamos el código para controlar Microsoft Word que tenía en la versión 1.3 de la librería, y lo comparamos con la nueva versión:

ILateBindingFacade word = LateBindingFactory.CrateAutomationBinding("Word.Application");

//Get Word object
ILateBindingFacade wordDoc = word.Get<ILateBindingFacade>("Documents").Call<ILateBindingFacade>("Add");
ILateBindingFacade selection = word.Get<ILateBindingFacade>("Selection");

string str = "Hello World!";

word.Set("Visible", true);

selection.Call("BoldRun");

foreach (char c in str)
{
selection.Call("TypeText", Args.Build(c.ToString()));
System.Threading.Thread.Sleep(200);
}

word.Call("Quit", Args.Build(0) );

Y la nueva versión 2.1 que lava más blanco:
IInvoker wordApp = BindingFactory.CreateAutomationBinding("Word.Application");
//Get Word object
IInvoker document = wordApp.Property("Documents").Get<IIinvoker>();

document
.Method("Add")
.Invoke();

IInvoker selection = wordApp
.Property("Selection")
.Get<IInvoker>();

string str = "Hello World!";

//Make workd visible
wordApp
.Property("Visible")
.Set(true);

//Activate bold
selection
.Method("BoldRun")
.Invoke();


foreach (char c in str)
{
selection
.Method("TypeText")
.AddParameter(c.ToString())
.Invoke();

System.Threading.Thread.Sleep(200);
}

//Quit
wordApp
.Method("Quit")
.AddParameter(0)
.Invoke();
Sólo queda hacer la pregunta de rigor, ¿qué interface creeis que sería más cómodo de utilizar?

2 Comentarios:

Zorro dijo...

Lo de la sintaxis fluida lo tiene Visual Basic. ¿Por qué tenía que ser tan sensual un lenguaje como ése? ;_;

Está muy chula tu librería. Simplificar el código hace más llevadero picar una masa enorme de código, de ahí que por ejemplo Python reduzca tanto estrés (véase python.com y python.org).

Por cierto, si algún día vuelves al lado oscuro del C++ (suponiendo que sea ése el lado oscuro/reverso tenebroso), deberías ver esto. Igual ya lo conoces; es la Boost library, utilizada en productos comerciales como Photoshop, FEAR o VirusScan, e incorporada parcialmente al nuevo estándar C++0x. Tiene una brutal y asquerosísima cantidad de utilidades (expresiones regulares, datetime's, hilos, smart pointers, serialización, tipos any, MPI para supercomputación, ...) que me hacen sufrir arcadas de rabia por no haberla descubierto antes.

Ricky dijo...

Gracias por los comentarios.

Sobre el VB, de hecho la motivación original de la librería era simplificar estas llamadas que en VB eran tan simples como poner .Operacion() y automáticamente el compilador, si a la hora de enlazar no encontraba el método, auto-generaba el código de una llamada con late binding, sin que te enteres. El problema de eso es que como algo pete, puede que no tengas ni puta idea de por qué :P

Con C# 4.0 añadirán la palabra clave "dynamic" que servirá para justamente esto, lo que creo que es el mejor término medio entre el inmenso dolor de C# y la promiscua vida alegre de VB.


Claro que volveré al C++, de hecho tengo un libro por leer, y voy mirando algún que otro artículo chulo como:
http://aigamedev.com/programming-tips/research-object-attribute-systems

Y la boost ya la conozco peeero no la he usado. Tengo ganas de echarle un vistazo porque todo el mundo dice que es la ostia, y además como es modular puedes utilizar sólo lo que te interesa.

A ver si me pongo a hacer un par de entradas con lo nuevo de nocturnal y los smart ptr de boost