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.

Nessun commento:

Posta un commento