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.

Nessun commento:

Posta un commento