Factory pattern in .Net 2.0

Il problema

Il problema che volevo affrontare è di accedere ad un altro sistema in modo astratto con un pattern di tipo Factory che istanzi un provider specificato dalla configurazione. In più ogni chiamata all'altro sistema poteva richiedere un provider specifico per la chiamata, mentre per le altre chiamate il provider può essere generico.

Naturalmente mi son dato l'obiettivo di essere quanto più tipizzato possibile perché, in parte per l'esperienza con il C++, son convinto che una forte tipizzazione riduca il numero di errori e il costo del test.

Il passato

Il framework .NET 1.1 non permetteva di avvicinarsi troppo a questo obiettivo. L'interfaccia del provider poteva essere definita in questo modo:

 public interface IOperation
{
  object Execute(object request);
}

ma abbiamo a che fare con degli oggetti generici costringendo l'utilizzatore a dei cast:

 Factory factory = new Factory();
IOperation op = factory.Create(this);
XyzResponse response = (XyzResponse) op.Execute(request);

Si potrebbe imporre una classe base diversa da object per i parametri della Execute ma spesso rende inutilizzabile una soluzione.

Come usare un provider e come scriverlo in .NET 2.0

Finalmente abbiamo i Generics, cosicché possiamo evitare l'uso di object e sostituirlo con dei template. La nostra interfaccia diventa così

 public interface IOperation<RequestType, ResponseType>
    where RequestType : IRequestMessage
    where ResponseType : IResponseMessage
{
    ResponseType Execute(RequestType request);
}

ed il suo impiego è assai naturale

 Factory factory = new Factory();
IOperation<XyzRequest, XyzResponse> op = factory.Create<XyzRequest, XyzResponse>(this);
XyzResponse response = op.Execute(request);

I cast sono spariti e il codice è fortemente tipizzato.

La soluzione

Vediamo ora come realizzare la classe Factory e troveremo alcune cose interessanti.

 public IOperation<RequestType, ResponseType> Create(object caller)
    where RequestType   : class, IRequestMessage
    where ResponseType : class, IResponseMessage
{
    MappingInfo mappingInfo = GetMapping(caller);
    Assembly providerAssembly = Assembly.Load(mappingInfo.AssemblyName);
    Type providerType = providerAssembly.GetType(mappingInfo.ClassName);
    Type targetType = null;
    if (providerType.IsGenericType) {
        Type[] typeArgs = { typeof( RequestType ), typeof( ResponseType ) };
        targetType = providerType.MakeGenericType( typeArgs );
    } else {
        targetType = providerType;
    }
    object operation = Activator.CreateInstance(targetType);
    IOperation<RequestType, ResponseType> targetObject = operation as IOperation<RequestType, ResponseType>;
    return targetObject;
}

Factory non è una classe generic, ma solo il metodo Create lo è. MappingInfo è una semplice struttura che mi dice il tipo da istanziare (ClassName) e in quale assembly è contenuto (AssemblyName). Con queste informazioni ottengo con facilità il Type: codice tipico di un modello a provider per .NET.

Se la classe che implementa il provider è 'tradizionale', ossia non è generic, si passa alla CreateInstance ed abbiamo istanziato il provider e ne effettuiamo il cast. Se invece IsGenericType restituisce true vuol dire che nella configurazione è stato indicato un tipo generic che assumiamo abbia esattamente due parametri generic corrispondenti ai parametri generic della Create, ossia RequestType e ResponseType.

Non ha però senso istanziare una classe generic, bisogna chiuderla, compito svolto dalla MakeGenericType che con un array di Type costruisce il tipo chiuso: ad esempio da

 class MyProvider<RequestType, ResponseType>

chiusa con RequestType pari a MyRequest e ResponseType pari a MyResponse, abbiamo

 class MyProvider<MyRequest, MyResponse>

In questo modo posso realizzare sia provider non generic (chiusi) che provider generici (aperti). Vediamo i due tipi di implementazioni.

 public class MyGenericProvider<RequestType, ResponseType> : IOperation<RequestType, ResponseType>
    where RequestType :class, IRequestMessage
    where ResponseType :class, IResponseMessage
{
    public MyGenericProvider()
    {
        //...
    }
    public ResponseType Execute( RequestType request )
    {
        //...
    }
}

La classe specifica è

 public class MySpecificProvider1 : IOperation<DemoRequest1,DemoResponse1>
{
    public DemoResponse1 Execute( DemoRequest1 request )
    {
        //...
    }
}

Ma che nomi hanno le due classi (ossia cosa scriviamo nella configurazione)?

 MyGenericProvider`2
MySpecificProvider1

ossia il nome di una classe generic viene seguito da un carattere backquote (U+0060) e dal numero di parametri template, nel nostro caso 2.

Quando si chiude la classe generic il nome del tipo viene esteso con i nomi delle classi racchiuse tra parentesi.

 MyGenericProvider`2[DemoRequest2,DemoResponse2]

Spero di esser stato sufficientemente chiaro per oggi.