Fluently.Design().Me: inganniamo l’intellisense :-)
Eravamo rimasti qua:
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;
static class MyFluentEntryPoint { public static FluentEngineHaving ( 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”:
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 ConfiguredFluentEngineConfigure() { 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 IFluentEngineHaving ( 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:
è 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