Promise Pattern

¡Buenas!

Este post está “inspirado” por un tweet de Gisela Torres. Posteriormente ella misma hizo un post en su blog sobre este mismo patrón que os recomiendo leer.

Ultimamente está muy de moda, al menos en el mundo de Javascript, hablar del Promise pattern (lo siento, pero llega un punto en que yo ya desisto de intentar encontrar traducciones para todo…). Ese patrón se asocia mucho con la realización de aplicaciones asíncronas, llegándose a ver como el mecanismo para la realización de este tipo de aplicaciones.

No. El promise pattern nada tiene que ver con la creación de aplicaciones asíncronas, aunque es en estas, por razones que ahora veremos, donde se puede explotar más. Pero es basicamente un patrón de organización de código. Tampoco es exclusivo de Javascript, por supuesto… De hecho, y para no ser clásicos, vamos a ver un ejemplo de su uso en C#.

El promise pattern nos ayuda a organizar nuestro código en aquellos casos en que un método recibe como parámetro otro método (en el caso de C# un delegate). Este otro método (o delegate) es usualmente una función de callback, aunque no tiene exactamente porque.

Vamos a verlo con un ejemplo muy tonto. Imaginad una clase como la siguiente:

class NoPromise

{

    public void Sumar (IEnumerable<int> data, Action<int> next )

    {

        var result = 0;

        foreach (var item in data) result += item;

        next(result);

    }

 

    public void Invertir(int value, Action<int> next)

    {

        var result = value * -1;

        next(result);

    }

 

    public void Dividir(int value, float divisor, Action<float> next)

    {

        var result = value/divisor;

        next(result);

    }

}

Esta clase expone tres métodos Sumar, Dividir e Invertir. Ambos aceptan un segundo parámetro (un delegate) llamado next que le dice al método que hacer con el resultado.

Así podemos invocar dichos métodos así:

np.Sumar(list, x =>

    np.Invertir(x, y=>

        np.Dividir(y, 4.0f, z =>

            Console.WriteLine("El resultado es " + z))));

Aquí no tenemos asincronidad alguna, simplemente estamos encadenando métodos. Fijaos en como tenemos una “cascada” de funciones encadeandas. La llamada a la primera función (Sumar) tiene en su interior la llamada a Invertir, que en su interior tiene la llamada a Dividir que en su interior tiene el Console.WriteLine. Tenemos pues “una cascada de funciones dentro de funciones”.

Este problema es el que el promise pattern intenta solucionar… Veamos primero como sería el resultado de las llamadas usando este patrón:

var prom1 = pd.Sumar(list);

var prom2 = prom1.Then(x => pd.Invertir(x));

var prom3 = prom2.Then(x => pd.Dividir(x, 4.0f));

prom3.Then(x => Console.WriteLine("El valor es " + x));

Si preferís podríais usar una sintaxis más compacta sin declarar las variables intermedias:

pd.Sumar(list).

    Then(x => pd.Invertir(x)).

    Then(x => pd.Dividir(x, 4.0f)).

    Then(x => Console.WriteLine("final: " + x));

La idea es que el método Sumar no recibe como un parámetro el método a invocar a continuación si no que nos devuelve un objeto (el promise). A este objeto le podremos indicar (usando el método Then) cual es el siguiente método a invocar. Además los promises se encadenan entre ellos. Fijaos como hemos pasado de una llamada a un método (que dentro tenía varias llamadas más) a cuatro llamadas separadas. Eso mejora mucho la legibilidad del código que es el objetivo principal de este patrón.

Por si os interesa aquí tenéis la implementación que he usado del patrón. Primero la interfaz:

public interface IPromise<T>

{

    IPromise<TR> Then<TR>(Func<T, TR> action);

    void Then(Action<T> action);

    T Value { get; }

}

Y luego la clase que lo implementa:

class Promise<T> : IPromise<T>

{

    private T _data;

 

    public Promise(T data)

    {

        _data = data;

    }

    public IPromise<TR> Then<TR>(Func<T, TR> action)

    {

        return new Promise<TR>(action(_data));

    }

    public T Value

    {

        get{ return _data; }

    }

    public void Then(Action<T> action)

    {

        action(_data);

    }

}

Finalmente necesitamos que el método Sumar (que es el iniciador de la cadena de promises) me devuelva un IPromise:

class PromiseDemo

{

    public IPromise<int> Sumar(IEnumerable<int> data)

    {

        var result = 0;

        foreach (var item in data) result += item;

        return new Promise<int>(result);

    }

    public int Invertir(int value)

    {

        return value * -1;

    }

    public float Dividir(int value, float div)

    {

        return value / div;

    }

}

El resto de métodos son métodos “normales”.

¿Y porqué (casi) todo el mundo asocia el promise pattern con asincronidad?

Bueno… si os fijáis el promise pattern permite organizar mejor nuestro código en aquellos casos en que un método espera a otro método como parámetro. Y eso es un caso habitual en… las funciones asíncronas que, por norma general, esperan un parámetro adicional que es el callback.

Ahora imaginad la situación clásica de que el callback de una función asíncrona es a su vez una función asíncrona que espera otro callback que es a su vez otra función asíncrona con otro callback… Llegamos así a la cascada de callbacks dentro de callbacks que es justo el caso que soluciona este patrón. De ahí que la gente asocie promise pattern a asincronidad aunque no tengan nada que ver.

De hecho .NET, en la TPL tiene una implementación ya realizada del promise pattern… Os suena el método ContinueWith de la clase Task? Pues viene a ser una implementación del promise pattern (en este caso la propia clase Task actúa como promise).

¿Y porque se habla tanto del promise pattern en Javascript?

De hecho más que en javascript deberíamos decir en lenguajes funcionales donde las funciones son ciudadanos de primer orden. En C# hasta la aparición de las expresiones lambda y los delegados genéricos no podíamos implementar este patrón (de una forma cómoda). Por eso en lenguajes no funcionales no se habla mucho de este patrón ya que en estos lenguajes no puede pasarse “una función entera” como parámetro a una función.

Pero en Javascript si se puede, y la popularidad que está adquiriendo el lenguaje junto con muchos frameworks que tienen este tipo de llamadas hace que la gente empiece a hablar de este patrón.

Pero insisto: no tiene nada que ver con asincronidad (aunque ahí se use mucho). P.ej. el siguiente código jQuery no tiene nada de asíncrono pero sería un candidato perfecto a utilizarel promise pattern:

$(document).ready(function() {

    $("#cmdOpen").click(function() {

        $("#divData").fadeIn("slow", function() {

            $("#divOther").show();

        });

    });

});

¡Espero que este post sobre este patrón os haya parecido interesante!

Un saludo!

Un comentario sobre “Promise Pattern”

  1. Me ha gustado. No conocia este patron. Gracias Eduard!!!

    Un inciso. No me gusta la manera en el que la clase de «negocio» tiene que ser «promise aware». Para evitarlo habia pensado el algo asi. ¿Que te parece? ¿Pros? ¿Contras?

    Siento que este en VB pero es lo que tenia abierto en este momento en el VS, y encima el 2008.

    Sub Main()

    Dim numeros As New List(Of Integer)
    numeros.Add(1)
    numeros.Add(2)
    Dim p As New Promise(Of IEnumerable(Of Integer))(numeros)
    p.Do(Function(x) Calc.Sumar(x)).Then(Function(x) Calc.Invertir(x)).Then(Function(x) Calc.Dividir(x, 4F)).Then(AddressOf Console.Write) ‘No exite sub(x) en 3.5 🙁

    End Sub

    Public Interface IPromise(Of T)

    Function [Then](Of TR)(action As Func(Of T, TR)) As IPromise(Of TR)
    Sub [Then](action As Action(Of T))
    ReadOnly Property Value() As T

    End Interface

    Class Promise(Of T)
    Implements IPromise(Of T)

    Private _data As T

    Public Sub New(data As T)
    _data = data
    End Sub

    Public Function [Then](Of TR)(action As Func(Of T, TR)) As IPromise(Of TR) Implements IPromise(Of T).Then
    Return New Promise(Of TR)(action(_data))
    End Function

    Public Sub [Then](action As Action(Of T)) Implements IPromise(Of T).Then
    action(_data)
    End Sub

    Public ReadOnly Property Value() As T Implements IPromise(Of T).Value
    Get
    Return _data
    End Get
    End Property

    Public Function [Do](Of TR)(action As Func(Of T,TR)) As IPromise(Of TR)
    Return New Promise(Of TR)(action(_data))
    End Function

    End Class

    Class Calc

    Public Shared Function Sumar(data As IEnumerable(Of Integer)) As Integer
    Dim result As Integer = 0
    For Each item As integer In data
    result += item
    Next
    Return result
    End Function

    Public shared Function Invertir(value As Integer) As Integer
    Return value * -1
    End Function

    Public shared Function Dividir(value As Integer, div As Single) As Single
    Return value / div
    End Function

    End Class

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *