Fluently.Design().Me: inganniamo l’intellisense :-)

Print Content | More

Per la felicità di Raffaeu ;-)

Eravamo rimasti qua:

image_thumb[2]image_thumb[1]

Il giochetto è più semplice del previsto, avete quasi sempre due attori:

  • Un entry point che tipicamente è una classe statica non generica che espone dei metodi generici;
  • Una classe che viene istanziata e ritornata dall’entry point;

Una cosa del tipo:

static class MyFluentEntryPoint
{
    public static FluentEngine Having( T value )
    {
        return new FluentEngine( value );
    }
}

e un engine così definito:

class FluentEngine
{
    readonly T value;

    public FluentEngine( T value )
    {
        this.value = value;
    }
}

Adesso per semplicità immaginiamo di voler essere in grado di fare 2 operazioni:

  • configurare l’engine appena creato;
  • mandare in esecuzione l’engine;
class FluentEngine
{
    readonly T value;

    public FluentEngine( T value )
    {
        this.value = value;
    }

    FluentEngine Configure()
    {
        return this;
    }

    FluentEngine Execute()
    {
        return this;
    }
}

Funzionare funziona ma incappate nel solito problema della “coding experience”:

image

Allora fate un secondo tentativo… fallimentare :-), almeno io ci sono incappato:

  • togliete il metodo Execute da FluentEngine;
  • create un ConfiguredFluentEngine:
class ConfiguredFluentEngine
{
    readonly T value;

    public ConfiguredFluentEngine( T value )
    {
        this.value = value;
    }

    public ConfiguredFluentEngine Execute()
    {
        return this;
    }
}

e cambiate la firma di Configure():

public ConfiguredFluentEngine Configure()
{
    return new ConfiguredFluentEngine( this.value );
}

funzionare funziona… avete risolto un problema e ve ne siete creati altri 2, figo:

  • se l’engine deve poter essere mandato in esecuzione anche senza la configurazione il metodo Excute lo dovete lasciare anche su FluentEngine pagando lo scotto di dover:
    • o duplicare l’implementazione di Execute;
    • o introdurre una terza classe per gestire l’esecuzione;
  • dovete preoccuparvi del mantenimento dello stato nel passaggio da una classe all’altra, e il nostro esempio è triviale in questo senso ma complicarsi la vita è moltissimo facilissimo;

Possiamo quindi asserire che non va bene :-) In realtà la soluzione è molto ma molto più semplice e l’abbiamo sotto gli occhi tutte le volte che usiamo una Fluent Interface fatta come si deve: interface… appunto :-)

Desiderata

Definiamo quello di cui abbiamo bisogno:

interface IConfiguredFluentEngine
{
    IFluentEngine Execute();
}

interface IFluentEngine
{
    IFluentEngine Execute();
    IConfiguredFluentEngine Configure();
}

e modifichiamo l’entry point di conseguenza:

static class MyFluentEntryPoint
{
    public static IFluentEngine Having( T value )
    {
        return new FluentEngine( value );
    }
}

A questo punto non ci resta che fare la cosa più banale di tutte: implementare tutte i contratti sulla stessa classe:

class FluentEngine : IFluentEngine, IConfiguredFluentEngine
{
    readonly T value;

    public FluentEngine( T value )
    {
        this.value = value;
    }

    public IConfiguredFluentEngine Configure()
    {
        return this;
    }

    public IFluentEngine Execute()
    {
        return this;
    }
}

garantendoci questo:

image

è ovvio che se complichiamo/estendiamo/necessitiamo di dare al developer una experience più corposa le cose si complicano e non di poco, ma il concetto resta sempre lo stesso: una singola classe che implementa contratti diversi, del resto la vostra necessità in questo caso è “semplicemente” ingannare l’intellisense :-)

Non è tutto… stay tuned.

.m


Code Design , Fluent Interfaces

3 comments

Related Post

  1. #1 da raffaeu Wednesday January 2010 alle 05:22

    Che dire, sei il mio mito!

  2. #2 da raffaeu Wednesday January 2010 alle 06:23

    Ci ho mangiato 'sopra' la tastiera e devo dire che spesso la soluzione piu' semplice e' anche quella che non vai mai ad immaginare. Certo se dovessi applicare tutto cio' a qualcosa come LinQ, il numero di 'flow' da gestire e il numero di interfacce da scrivere sarebbe mastodontico ma lavorandoci un po' credo di poter ottere una soluzione pulita e riciclabile.
    Avendo un' ovveride di WF, con molte piu' futures e una fluent interface, per me e' fondamentale avere un meccanismo di scrittura della mia fluent interface che mi consenta di guidare il dev verso scelte forzate di sintassi.
    Ottimo!

  3. #3 da Mauro Servienti Wednesday January 2010 alle 07:42

    Mi trovi pienamente d'accordo: se una "Fluent Interface" non è semplice, intuitiva e screvra da possibilità di errori nel suo utilizzo tanto vale non usare una Fluent Interface.

    Grazie del feedback :-)
    .m