giovedì 6 dicembre 2012

Come evitare gli enum per indicare lo stato di un'operazione


Qualche volta sarà capitato di dover sapere se la chiamata di un metodo è andata a buon fine, oppure un metodo potrebbe restituirvi stati diversi in base alle operazioni che può eseguire internamente.
Normalmente potete restituire un booleano oppure un enum per indicare lo stato, esempio:

   public bool DoSomething()
   {
      // ...
   }

oppure

   public Status DoSomething()
   {
      // ...
   }

con Status così definito:

   public enum Status
   {
      Low,
      High,
      Medium
   }

Pensate al caso che l'operazione che invocate oltre ad indicare il suo esito dovesse restituire un risultato. Esempio il TryParse di .NET:

   int result;
   if (int.TryParse("1", out result))
   {
      Console.WriteLine("Il numero è {0}", result);
   }
   else
   {
      Console.WriteLine("La stringa passata non è un numero");
   }

Se poi pensate al caso di stati multipli in base al valore dopo siete costretti ad inserire il risultato dentro uno switch per decidere che operazione eseguire:

   switch (new Buzz().DoSomething())
   {
      case Status.High:
         Console.WriteLine("Level 100");
         break;
      case Status.Low:
         Console.WriteLine("Level 0");
         break;
      case Status.Medium:
         Console.WriteLine("Level 50");
         break;
      default:
         throw new InvalidOperationException();
   }

Come potete notare ci sono un po' di problemini a gestire una situazione di questo tipo. Ora vi chiedo, non sarebbe più semplice avere una sintassi di questo tipo?

   new Buzz()
      .DoSomething()
      .Accept(new StatusVisitor());

dove la class StatusVisitor è definita come:

   public class StatusVisitor : IVisitor
   {
      public void Visit(Low status)
      {
         Console.WriteLine("Level 0");
      }

      public void Visit(High status)
      {
         Console.WriteLine("Level 100");
      }

      public void Visit(Medium status)
      {
         Console.WriteLine("Level 50");
      }
   }

e quindi, in base al risultato di DoSomething viene eseguita una delle tre istruzioni sopra indicate. Per gestire il tutto dobbiamo creare la seguente struttura:

   public class Buzz
   {
      // per ora impostiamo Medium come valore di default
      public IStatus DoSomething()
      {
         return new Medium();
      }
   }

   // creiamo l'interfaccia status che accetta di essere visitata da un
   // visitor
   public interface IStatus
   {
      void Accept(IVisitor visitor);
   }

   // di seguito implementiamo i tre stati
   public class Medium : IStatus
   {
      public void Accept(IVisitor visitor)
      {
         visitor.Visit(this);
      }
   }

   public class High : IStatus
   {
      public void Accept(IVisitor visitor)
      {
         visitor.Visit(this);
      }
   }

   public class Low : IStatus
   {
      public void Accept(IVisitor visitor)
      {
         visitor.Visit(this);
      }
   }

   // per finire definiamo un'interfaccia che conosce tutti gli stati
   public interface IVisitor
   {
      void Visit(Low status);
      void Visit(High status);
      void Visit(Medium status);
   }

A questo punto quando eseguiamo:

   new Buzz()
      .DoSomething()
      .Accept(new StatusVisitor());

l'output è: Level 50

Se non vogliamo creare una classe StatusVisitor che si occupa di gestire l'output possiamo demandare anche al chiamante l'onere di gestire l'output come nel caso dello switch:

   public class Main : IVisitor
   {
      public void main()
      {
         new Buzz()
            .DoSomething()
            .Accept(this);
      }

      void IVisitor.Visit(Low status)
      {
         Console.WriteLine("Level 0");
      }

      void IVisitor.Visit(High status)
      {
         Console.WriteLine("Level 100");
      }

      void IVisitor.Visit(Medium status)
      {
         Console.WriteLine("Level 50");
      }
   }

Se avete la necessità di produrre anche dei dati che vanno propagati oltre allo stato, basta aggiungere tali informazioni alle varie classi che derivano da IStatus.

Per chiarire le idee, vediamo come poteva essere scritto il TryParse seguendo il pattern appena spiegato:

   // definiamo la nostra class Int
   public static class Int
   {
      public static IResult TryParse(string value)
      {
         return IsNumber(value)
            ? (IResult)new IsNumber(Convert.ToInt32(value))
            : new IsNotNumber();
      }
   }


   // definiamo l'interfaccia per accetare i visitatori
   public interface IResult
   {
      void Accept(INumberVisitor visitor);
   }

   // definiamo l'interfaccia che conosce gli stati IsNumber e
   // IsNotNumber
   public interface INumberVisitor
   {
      void Visit(IsNumber result);
      void Visit(IsNotNumber result);
   }

   public class IsNotNumber : IResult
   {
      public void Accept(INumberVisitor visitor)
      {
         visitor.Visit(this);
      }
   }

   public class IsNumber : IResult
   {
      public int Value { get; private set; }

      public IsNumber(int value)
      {
         Value = value;
      }

      public void Accept(INumberVisitor visitor)
      {
         visitor.Visit(this);
      }
   }

ora proviamo il tutto

   public class Main : INumberVisitor
   {
      public void main()
      {
         Int.TryParse("1").Accept(this);
      }

      void INumberVisitor.Visit(IsNumber result)
      {
         Console.WriteLine("Il numero è {0}",result.Value);
      }

      void INumberVisitor.Visit(IsNotNumber result)
      {
         Console.WriteLine("La stringa passata non è un numero");
      }
   }

contro:

   public class Main
   {
      public void main()
      {
         int result;
         if (int.TryParse("1", out result))
         {
            Console.WriteLine("Il numero è {0}", result);
         }
         else
         {
            Console.WriteLine("La stringa passata non è un numero");
         }
   }

1 commento: