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.

venerdì 28 settembre 2012

Non modificare il codice

Seguendo la O dei S.O.L.I.D. Principles, il codice dovrebbe essere chiuso alla modifica ma aperto all'evoluzione. Ma cosa vuole dire nel concreto? La questione può essere semplificata se ragioniamo per comportamenti. 
Il nostro product owner chiede di mostrare a video tutti gli alunni con una età maggiore di 9 anni.

Scriviamo il test che verifica l'oggetto FilterStudentsByAge:

   [SetUp]
   public void SetUp()
   {
      sut = new FilterStudentsByAge();
   }

   [Test]
   public void Test()
   {
      sut.Filter(10.To<Age>(),
                       new[] { new Student { Age = 10.To<Age>()}});
      // completiamo il test...
   }

Come procediamo?

Potremmo far restituire la lista filtrata; é una strada, sconsiglio quando é possibile di usare valori di ritorno.

Se seguiamo questo concetto l'unico modo per testare l'oggetto é verificare la sua interazione con un altro oggetto.

Modifichiamo il test per aggiungere la dipendenza ad un'altra interfaccia.

   [SetUp]
   public void SetUp()
   {
      useStudent = new Mock<IUseStudent>();
      sut = new FilterStudentsByAge(useStudent.Object);
   }
   
   [Test]
   public void Test()
   {
      var student = new Student {Age = 10.To<Age>()};
      sut.Filter(10.To<Age>(), new[] { student });
      useStudent.Verify(m=>m.Do(student));
   }

Ora il test ci permette di capire se Filter ha il comportamento che desideriamo e interagisce con l' interfaccia IUseStudents. Implementiamo Filter nell' oggetto FilterStudentsByAge.

   public class FilterStudentsByAge
   {
      private readonly IUseStudent useStudent;

      public FilterStudentsByAge(IUseStudent useStudent)
      {
         this.useStudent = useStudent;
      }

      public void Filter(Age age, IEnumerable<Student> students)
      {
         foreach (var student in students)
         {
            if (student.Age < age)
               continue;
            useStudent.Do(student);
         }
      }
   }

Il test passa ma il codice non può andare in produzione, non esiste nessun oggetto che implementa IUseStudents. Ora creiamo l'oggetto che mostra gli studenti a video come richiesto dal product owner.

   public class DisplayStudentAtVideo : IUseStudent
   {
      public void Do(Student student)
      {
         if (student == null)
            return;
         Console.WriteLine("Studente {0} {1}"
                           student.Lastname, 
                           student.Name);
      }
   }

Ora il product owner ci chiede di scrivere su un file gli studenti oltre a mostrarli a video. La prima reazione è di modificare l' oggetto DisplayStudentAtVideo e aggiungere il codice per scrivere il file. 

Respiriamo e proviamo a seguire la strada che vi consiglio. Scriviamo un oggetto per aggiungere il nuovo comportamento.

   public class WriteStudentsIntoFile : IUseStudent
   {
      private readonly TextWriter stream;

      public DisplayStudentAtVideo(string filename)
      {
         stream = new FileInfo(filename).AppendText();
      }

      public void Do(Student student)
      {
         if (student == null)
            return;
         stream.WriteLine("Studente {0} {1} [età: {2}]",
                          student.Lastname,
                          student.Name,
                          student.Age);
         stream.Flush();
      }
   }

Bene, ma ora come facciamo ad usarle insieme?

Creiamo un' altro oggetto che ha il ruolo di gestire l'utilizzo di un elendo di oggetti che rispettano il contratto IUseStudent.

   public class UseStudentsComposite : IUseStudent
   {
      private readonly IUseStudent[] inners;

      public UseStudentsComposite(IUseStudent[] inners)
      {
         this.inners = inners;
      }
      
      public void Do(Student student)
      {
         foreach (var item in inners)
            item.Do(student);
      }
   }

A questo punto il test continua a passare perche non abbiamo modificato il comportamento di FilterStudentsByAge, ma dobbiamo aggiungere i test per i nuovi oggetti. Vediamo il codice di produzione per la prima richiesta:

   new FilterStudentsByAge(
         new DisplayStudentAtVideo())
      .Filter(10.To<Age>(), new[]
            {
               new Student(),
               new Student(),
               new Student()
            });

Dopo la seconda richiesta, il codice di produzione diventa:

   new FilterStudentsByAge(
         new UseStudentsComposite(
               new IUseStudent[]
                  {
                     new DisplayStudentAtVideo(),
                     new WriteStudentsIntoFile(
                           @"c:\temp\dumpStudents.txt")
                  }))
      .Filter(10.To<Age>(), new[]
            {
               new Student(),
               new Student(),
               new Student()
            });

Si può notare che una successiva richiesta di aggiungere un nuovo comportamento per condividere le informazioni degli studenti, si tramuterebbe in un nuovo oggetto da istanziare nell'array gestito da UseStudentsComposite.
Concludendo:
  • aggiungete oggetti per definire nuovi comportamenti e separare le responsabilità;
  • cercate di modificare il codice solo per iniettare i nuovi comportamenti;
  • fate refactoring per rispettare questi principi.