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;

Nessun commento:

Posta un commento