Enum e attributi

Recentemente ho avuto l’esigenza di gestire delle enumerazioni di dimensione considerevole (diverse decine di elementi), dove ad ogni valore dell’enum doveva essere associata una classe. Supponiamo ad esempio di avere un enum ZooEnum e una gerarchia di classi come segue:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public enum ZooEnum
{
    Dog,
    Cat,
    Tiger,
    Snake,
    Puma,
    ...
}
 
public abstract class Animal
{
    public Animal() { }
    public abstract void Eat();
    public abstract void Sleep();
    public abstract string Verse(); 
}
 
public class Dog: Animal
{
    public Dog() { }
 
    public override void Eat()
    {
        DoSomething();
    }
 
    ...
    // Resto dell'implementazione
}
 
public class Cat: Animal
{
    ...
}
 
...

A questo punto possiamo costruire un metodo AnimalBuilder con input un valore dell’enumerazione ZooEnum e per output una oggetto con classe derivata da Animal. La forma che potrebbe assumere una metodo del genere potrebbe essere la seguente:

1
2
3
4
5
6
7
8
9
10
11
12
13
Animal AnimalBuilder(ZooEnum animalValue)
{
    switch (animalValue)
    {
        case ZooEnum.Dog:
            return new Dog();
            break;
        case ZooEnum.Cat:
            return new Cat();
            break;
        ...
    }
}

Se gli elementi dell’enumerazione cominciano a diventare diverse decine, e magari oltre che ad un metodo si vogliono associare altre informazioni agli elementi dell’enum (ad esempio, una stringa da usare per funzioni di output), allora le cose si complicano. Il codice basato su switch comincia a diventare di difficile lettura e manutenzione.

Una soluzione più elegante, a mio avviso, è quella che utilizza gli attributi e la reflection. Per prima cosa si costruisce una classe derivata da Attribute che per memorizzare il tipo di oggetto da creare:

1
2
3
4
5
6
7
8
9
10
public class ZooAttribute : System.Attribute
{
    Type p_AnimalType;
 
    public ZooAttribute(Type type) { p_AnimalType = type; }
    public Type Value
    {
        get { return p_AnimalType; }
    }
}

Ora che abbiamo il nostro attributo, possiamo associarlo ai valori dell’enumerazione, indicando per ciascuno di essi che tipo di oggetto si vuole costruire:

1
2
3
4
5
6
7
8
9
public enum ZooEnum
{
    [Zoo(typeof(Dog))]Dog,
    [Zoo(typeof(Cat))]Cat,
    [Zoo(typeof(Tiger))]Tiger,
    [Zoo(typeof(Snake))]Snake,
    [Zoo(typeof(Puma))]Puma,
    ...
}

A questo punto si può riscrivere il metodo AnimalBuilder utilizzando Reflection e il metodo Activator.CreateInstance. Il nuovo codice sarà indipendente dal numero di elementi dell’enumerazione, e non utilizza né if (non almeno per scegliere quale oggetto costruire) né switch. Il suo funzionamento è abbastanza semplice: recupero eventuali attributi custom di tipo ZooAttribute associati al valore enum in input, ottengo il tipo di oggetto da istanziare, che si ottiene leggendo il campo ZooAttribute.Value, e lo istanzio invocando Activator.CreateInstance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Animal AnimalBuilder(ZooEnum animalValue)
{
    Animal result = null;
 
    Type type = animalValue.GetType();
 
    FieldInfo fieldInfo = type.GetField(animalValue.ToString());
    ZooAttribute[] attributes = (ZooAttribute[])fieldInfo.GetCustomAttributes(typeof(ZooAttribute), false);
    if (attributes.Length > 0)
    {
        result = (Animal)Activator.CreateInstance(attributes[0].Value);
    }
 
    return result;
}

A questo punto è facile estendere l’attributo custom in modo. Si può, ad esempio, associare una stringa ad uno dei nostri animali nel seguente modo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class ZooAttribute : System.Attribute
{
    Type p_AnimalType;
    string p_Description;
 
    public ZooAttribute(Type type, string description) 
    {
        p_AnimalType = type; 
        p_Description = description;
    }
 
    public Type Value
    {
        get { return p_AnimalType; }
    }
 
    public string Description
    {
        get { return p_Description; }
    }
}
 
public enum ZooEnum
{
    [Zoo(typeof(Dog), "Cane")]Dog,
    [Zoo(typeof(Cat), "Gatto")]Cat,
    [Zoo(typeof(Tiger), "Tigre")]Tiger,
    [Zoo(typeof(Snake), "Serpente")]Snake,
    [Zoo(typeof(Puma), "Puma")]Puma,
    ...
}
 
public string AnimalDescription(ZooEnum animalValue)
{
    string result = null;
 
    Type type = animalValue.GetType();
 
    FieldInfo fieldInfo = type.GetField(animalValue.ToString());
    ZooAttribute[] attributes = (ZooAttribute[])fieldInfo.GetCustomAttributes(typeof(ZooAttribute), false);
    if (attributes.Length > 0)
    {
        result = attributes[0].Description;
    }
 
    return result;
}
 
// Il seguente codice darà come output "Cane"
ZooEnum myAnimal = ZooEnum.Dog;
Console.Writeln(AnimalDescription(myAnimal));

About this entry