domenica 30 settembre 2012

Come e perché creare un tipo "primitivo"

Come illustrato in altri post, sostengo che l'utilizzo dei tipi "primitivi" per identificare dei concetti, può portare a problemi di design e manutenzione, oltre ad insidiosi bug.
Prendiamo ad esempio l'oggetto persona così definito:

   public class Person
   {
      public string Name { getset; }
      public string Lastname { getset; }
      public double Height { getset; }
      public double Weight { get; set; }
      public int Age { getset; }
   }

Vediamo un primo possibile problema, definiamo una Factory per istanziare un oggetto Person:

   public static Person Create(string name,
                               string lastname,
                               double height,
                               double weight,
                               int age)
   {
      return new Person
         {
            Name = name,
            Weight = height,
            Lastname = lastname,
            Height = height,
            Age = age
         };
   }

Non notate nulla di strano? Se cerchiamo di compilare, non abbiamo nessun problema. Solo se scriviamo un test oppure eseguiamo il codice, ci potremmo accorgere di un problema. Allora non ci resta che scrivere un test.

   [Test]
   public void CreatePersonTest()
   {
      var person = Person.Create("Claudio", "Pattarello", 179, 95, 36);

      Assert.AreEqual("Claudio", person.Name);
      Assert.AreEqual("Pattarello", person.Lastname);
      Assert.AreEqual(179, person.Height);
      Assert.AreEqual(95, person.Weight);
      Assert.AreEqual(36, person.Age);
   }

Se eseguiamo il test otteniamo questo risultato:

   Test 'CreatePersonTest' failed:
   Expected: 95
   But was: 179.0d

Questo accade perché abbiamo usato lo stesso tipo per indicare due concetti diversi, se le proprietà Height e Weight fossero dei tipi che rappresentano coerentemente i valori che contengono, ci saremmo accorti di un problema direttamente a compile-time.

Vediamo cosa succede se definiamo i tipi Height e Weight e li sostituiamo ai double.

   public class Height
   {
      public double Value { get; set; }
   }

   public class Weight
   {
      public double Value { get; set; }
   }

ora modifichiamo la classe e la factory:

   public class Person
   {
      public string Name{get;set;}
      public string Lastname{get;set;}
      public Height Height{get;set;}
      public Weight Weight { get; set; }
      public int Age{get;set;}
   }

   public static Person Create(string name,
                         string lastname,
                         Height height,
                         Weight weight,
                         int age)
   {
      return new Person
         {
            Name = name,
            Weight = height,
            Lastname = lastname,
            Height = height,
            Age = age
         };
   }

Se compiliamo otteniamo ben tre errori:
  • Argument 3: cannot convert from 'double' to 'Height'
  • Argument 4: cannot convert from 'double' to 'Weight'
  • Cannot implicitly convert type 'Height' to 'Weight'
I primi due errori indicano l'impossibilità nel test di convertire 179 in Height e 95 in Weight; l'ultimo errore ci avvisa del problema che ci saremmo accorti solo in fase di test, ovvero che abbiamo assegnato alla property Weight il contenuto del parametro height.

Come potete notare, se ci sforziamo di definire i tipi necessari e non di utilizzare solo quelli disponibili, possiamo prevenire molti problemi.

Ora vediamo come poter creare i nostri tipi “primitivi”, definiamo un oggetto generic che utilizzeremo per creare i nostri tipi:

   public class ValueObject<T>
   {
      public T Value { get; set; }
      public static implicit operator T(ValueObject<T> obj)
      {
         return obj.Value;
      }
   }

Ora, Height e Weight possono essere implementati facilmente:

   public class Weight : ValueObject<double> {}
   
   public class Height : ValueObject<double> {}

Ora per utilizzare la factory dovremmo scrivere:

   var person = Person.Create("Claudio",
                              "Pattarello",
                              new Height { Value = 179 },
                              new Weight { Value = 95 },
                              36);

Trovo questo codice troppo verboso, ed è per questo che preferisco, in questo caso, usare un extension method:

   public static class TypeExtensions
   {
      public static T To<T>(this double value)
         where T : ValueObject<double>, new()
      {
         return new T { Value = value };
      }
   }

ora la sintassi per utilizzare la factory diventa:

   var person = Person.Create("Claudio",
                              "Pattarello",
                              179d.To<Height>(),
                              95d.To<Weight>(),
                              36);

Potete per tutti gli altri tipi primitivi creare l'extension method per avere una sintassi più compatta.

Cercate di non essere pigri e definite tutti i tipi che vi servono, nel lungo termine otterrete solo benefici da questa pratica.

Nessun commento:

Posta un commento