lunedì 8 ottobre 2012

Ereditarietà vs Composizione (il simpatico product owner)

Vi ricordate la richiesta iniziale: "esportare la lista degli studenti in tre formati diversi e gli elementi devono essere separati ogni 10 elementi".

Ora quel simpaticone del product owner ci chiede: "vorrei che la lista a video non sia separata, invece la lista esportata in xml sia paginata a 10 elementi e la lista esportata su csv deve avere una paginazione di 20 elementi".

Inizialmente usando l'ereditarietà avevamo scritto questa classe base:

   public abstract class ExportStudentsWithSeparatorEachTenLines
   {
      public void Save(Student[] students)
      {
         var linesCounter = 0;
         if (students != null)
            foreach (var student in students)
            {
               if (++linesCounter%10 == 0)
                  WriteSeparator();
               Export(student);
            }
      }

      protected abstract void Export(Student student);
      protected abstract void WriteSeparator();
   }

E avevamo creato tre classi che si dovevano occupare di esportare i dati nei tre formati richiesti.
Ora abbiamo un comportamento che rimane comune: l'iterazione dell' array di studenti.
Purtroppo la logica per inserire il separatore é cambiata.

Come procediamo?

Rinominiamo la classe base in ExportStudendsWithSeparator.

Facciamo una prova, inseriamo nel costruttore della classe base un parametro che indica il numero di paginazione per le classi derivate:

   protected ExportStudentsWithSeparator(int pageSize)
   {
      this.pageSize = pageSize;
   }

Modifichiamo il codice per capire come dobbiamo paginare:

   if (++linesCounter % pageSize == 0)

Vediamo il codice modificato delle sotto classi:

   public class ExportStudentsToXml : ExportStudentsWithSeparator
   {
      public ExportStudentsToXml() : base(10) { }
      // aggiungere il codice per implementare il formato
      // [...]
   }

   public class ExportStudentsToCsv : ExportStudentsWithSeparator
   {
      public ExportStudentsToCsv() : base(20) { }
      // aggiungere il codice per implementare il formato
      // [...]
   }

Bene bene, sembra funzionare, ma ho un dubbio:

   public class ExportStudentsToVideo : ExportStudentsWithSeparator
   {
      public ExportStudentsToVideo() : base(/* e qui cosa ci metto?*/) { }
      // aggiungere il codice per implementare il formato
      // [...]
   }

Per uniformità dovremmo inserire zero, ma avremmo un risultato inaspettato con il codice esistente, una bella Division by zero.

Ok sono in crisi, modifico la classe base, inserisco un bel IF, se il parametro ha valore zero allora salto il comportamento di inserire il separatore.

   if (pageSize !=0 && ++linesCounter % pageSize == 0)

Questa soluzione non mi piace, proviamo un'altra strada e creiamo un nuovo template metodo che hai il compito di indicare quando è il momento di inserire un nuovo separatore.

   public abstract class ExportStudentsWithSeparator
   {
      private readonly int pageSize;

      protected ExportStudentsWithSeparator()
      {
         this.pageSize = pageSize;
      }

      public void Save(Student[] students)
      {
         var linesCounter = 0;
         if (students != null)
            foreach (var student in students)
            {
               if (NewSeparator(++linesCounter))
               {
                  WriteSeparator();
                  linesCounter = 0;
               }
               Export(student);
            }
      }

      protected abstract void Export(Student student);
      protected abstract void WriteSeparator();
      protected abstract NewSeparator();
   }

Ora vediamo come modificare le classi derivate:

   public class ExportStudentsToXml : ExportStudentsWithSeparator
   {
      protected override bool NewSeparator(int lineCount)
      {
         return lineCount%10 == 0;
      }
      // aggiungere il codice per implementare il formato
      //[...]
   }

La classe che stampa a video ha una forma più elenegate di prima:

   public class ExportStudentsToVideo : ExportStudentsWithSeparator
   {
      protected override bool NewSeparator(int lineCount)
      {
         return false;
      }
      // aggiungere il codice per implementare il formato
      //[...]
   }

Con questa scelta abbiamo evitato di inserire un IF, ma abbiamo comunque duplicato del codice quando modifichiamo la classe che esporta in CSV:

   public class ExportStudentsToCsv : ExportStudentsWithSeparator
   {
      protected override bool NewSeparator(int lineCount)
      {
         return lineCount%20 == 0;
      }
      // aggiungere il codice per implementare il formato
      //[...]
   }

Inoltre, notate che abbiamo fatto molte modifiche, infatti per esaudire la richiesta sono state modificate ben 4 classi e abbiamo pensato ha due soluzioni prima di trovare la strada “giusta”.

Non so voi ma ho una brutta sensazione e se domani arriva il product owner con un'altra brillante idea?

Ok, ora vi propongo una strada diversa, vi ricordate questo codice?

   new ExporterStudentsList(
         new ExportStudentWithSeparator(
               new ExportStudentToVideo(),
               new SeparatorToVideo()))
      .Export(arrayOfStudents);

Proviamo a risolvere il problema di esportare la lista degli studenti senza separatore. Vediamo se è possibile con la composizione:

   new ExporterStudentsList(new ExportStudentToVideo())
      .Export(arrayOfStudents);

Cavoli, ho finito!!!

Per le altre due richieste, possiamo procedere come prima, aggiungendo il parametro per definire la dimensione delle pagine nel costruttore di ExportStudentWithSeparator.

   public class ExportStudentWithSeparator : IExportStudent
   {
      private int linesCounter;
      private IExportStudent inner;
      private ISeparator separator;
      private readonly int pageSize;
      private byte counter;

      public ExportStudentWithSeparator(IExportStudent inner,
                                        ISeparator separator,
                                        int pageSize)
      {
         this.inner = inner;
         this.separator = separator;
         this.pageSize = pageSize;
      }

      public void Export(Student student)
      {
         if (++counter % pageSize == 0)
         {
            separator.WriteSeparator();
            counter = 0;
         }
         inner.Export(student);
      }
   }

Il test che verifica il comportamento di EportStudentWithSeparator, continua a passare una volta aggiunto al costruttore il valore 10 per indicare la dimensione della pagina.

   [SetUp]
   public void SetUp()
   {
      expoter = new Mock<IExportStudent>();
      separator = new Mock<ISeparator>();
      sut = new ExportStudentWithSeparator(expoter.Object,
                                           separator.Object,
                                           10);
   }

   [Test]
   public void SeparatorTest()
   {
      var student = new Student();
      for (int i = 0; i < 10; i++)
         sut.Export(student);
      expoter.Verify(m => m.Export(It.IsAny<Student>()),
                                   Times.AtLeast(10));
      separator.Verify(m => m.WriteSeparator(),Times.Once());
   }

Per questa richiesta, la scelta della composizione ha dato dei vantaggi sulla evoluzione del codice, è probabile che ulteriori esigenze possano essere più facilmente esaudite visto che abbiamo separato il più possibile i comportamenti assegnandoli ad oggetti ben precisi.

Fate le vostre considerezioni.

sabato 6 ottobre 2012

Ereditarietà vs Composizione

Quando parliamo di oop la maggior parte dei programmatori cita "l'ereditarietà" come caratteristica principale di questo paradigma. Al contrario l'ereditarietà è solo una delle possibilità  che può indurre in errori e nella stesura di codice poco leggibile. Facciamo un esempio:

"Vogliamo creare tre classi che esportano una lista di studenti a video, csv, xml con una caratteristica in comune ogni 10 elementi deve essere inserito un separatore per creare una sorta di paginazione."

Cerchiamo di esaudire questa richiesta passando per la strada dell'ereditarietà  Per fare questo scriviamo la classe base che contiene il comportamento comune.

   public abstract class ExportStudentsWithSeparatorEachTenLines
   {
      public void Save(Student[] students)
      {
         var linesCounter = 0;
         if (students != null)
            foreach (var student in students)
            {
               if (++linesCounter%10 == 0)
                  WriteSeparator();
               Export(student);
            }
      }

      protected abstract void Export(Student student);
      protected abstract void WriteSeparator();
   }

Bene, ora passiamo alla classe che deve esportare a video:

   public class ExportStudentsToVideo :
         ExportStudentsWithSeparatorEachTenLines
   {
      protected override void Export(Student student)
      {
         Console.WriteLine(student.ToString());
      }

      protected override void WriteSeparator()
      {
         Console.WriteLine("");
      }
   }

Passiamo per l'altra via della composizione tramite interfacce.

Ragioniamo su quanti comportamenti ci sono nella storia che dobbiamo realizzare :
  • dobbiamo esportare in vari formati la lista degli studenti;
  • dobbiamo esportare le informazioni di uno studente in una riga;
  • dobbiamo per ogni tipo di formato poter inserire un separatore per creare una paginazione;
  • dobbiamo esportare gli studenti a gruppi di 10 elementi;
Scriviamo il primo test:

   [SetUp]
   public void SetUp()
   {
      expoter = new Mock<IExportStudent>();
      sut = new ExporterStudentsList(expoter.Object);
   }

   [Test]
   public void ExportStudentListTest()
   {
      var student = new Student();
      sut.Export(new[] {student});
      expoter.Verify(m => m.Export(It.IsAny<Student>()));
   }

Definiamo la classe ExporterStudentsList che ha il compito di esportare la lista di studenti senza nessun'altro comportamento aggiuntivo. 

   public class ExporterStudentsList
   {
      private IExportStudent exporter;

      public ExporterStudentsList(IExportStudent exporter)
      {
         this.exporter = exporter;
      }

      public void Export(Student[] students)
      {
         foreach (var student in students)
            exporter.Export(student);
      }
   }

   public interface IExportStudent
   {
      void Export(Student student);
   }

Il test passa, ovvio che dobbiamo fare anche altri test, per ora passiamo avanti.
Ora creiamo la classe che esporta le informazioni dello studente a video.

   public class ExportStudentToVideo : IExportStudent
   {
      public void Export(Student student)
      {
         Console.WriteLine(string.Format("Studente {0} {1}"
                                         student.Name,
                                         student.Lastname));
      }
   }

Bene, ora passiamo all'aggiunta del comportamento che inserisce un separatore ogni 10 elementi.

Scriviamo il test:

   [SetUp]
   public void SetUp()
   {
      expoter = new Mock<IExportStudent>();
      separator = new Mock<ISeparator>();
      sut = new ExportStudentWithSeparator(expoter.Object,
                                                       separator.Object);
   }

   [Test]
   public void SeparatorTest()
   {
      var student = new Student();
      for (int i = 0; i < 10; i++)
         sut.Export(student);
      expoter.Verify(m => m.Export(It.IsAny<Student>()),
                                             Times.AtLeast(10));
      separator.Verify(m => m.WriteSeparator(),Times.Once());
   }

Ora creiamo le classi che hanno il ruolo di aggiungere il separatore:

   public class ExportStudentWithSeparator : IExportStudent
   {
      private int linesCounter;
      private IExportStudent inner;
      private ISeparator separator;
      private byte counter;

      public ExportStudentWithSeparator(IExportStudent inner,
                                                   ISeparator separator)
      {
         this.inner = inner;
         this.separator = separator;
      }

      public void Export(Student student)
      {
         if (++counter%10 == 0)
         {
            separator.WriteSeparator();
            counter = 0;
         }
         inner.Export(student);
      }
   }

   public interface ISeparator
   {
      void WriteSeparator();
   }

   public class SeparatorToVideo : ISeparator
   {
      public void WriteSeparator()
      {
         Console.WriteLine(string.Empty);
      }
   }

Confrontiamo le due soluzioni in produzione per esportare a video la lista paginata degli studenti:

   var arrayOfStudents = new[]
      {
         new Student(),
         new Student(),
         new Student()
      };

   //Ereditarietà
   new ExportStudentsToVideo()
      .Save(arrayOfStudents);
   // VS
   //Composizione
   new ExporterStudentsList(
      new ExportStudentWithSeparator(
            new ExportStudentToVideo(),
            new SeparatorToVideo()))
         .Export(arrayOfStudents);

Alcune considerazioni:
  • Usando l'ereditarietà il codice di produzione sembra più semplice, in realtà risulta essere solo più compatto;
  • Non sappiamo esattamente cosa fa la classe ExportStudentsToVideo invece la controparte tramite la sua verbosità ci permette di capire quali comportamenti sono in gioco;
  • Con  l'ereditarietà non potevamo scrivere test semplici (infatti non ho provato neppure a farlo);
  • le ulteriori richieste del product owner potrebbero essere un po' complicate da risolvere;