Formatowanie tekstu oraz atrybuty IFormatProvider, IFormattable, and ICustomFormatter

Dziś trochę o formatowaniu tekstu. Można je wykonać na wiele sposób. Osoby nie znające powyższych interfejsów zwykle tworzą własne metody zwracające wynik w odpowiednim formacie. Załóżmy, że mamy następującą klasę:

class PhoneNumber
{
   private readonly string _extension;
   private readonly string _phoneNumber;

   public PhoneNumber(string extension,string phoneNumber)
   {
       _extension = extension;
       _phoneNumber = phoneNumber;
   }
}

Na przykładzie powyższej klasy będę starał się po kolei przedstawić zastosowanie wspomnianych interfejsów. Proszę nie zwracać uwagi na strukturę wewnętrzną klasy – oczywiście można to lepiej zaimplementować niż przekazanie numeru w konstruktorze za pomocą string – nie  w tym jednak rzecz. Załóżmy, że użytkownik chce wyświetlić na ekranie kierunkowy i numer telefonu w formacie (extension)number. Brzydkim rozwiązaniem byłoby:

class PhoneNumber
{
   private readonly string _extension;
   private readonly string _phoneNumber;

   public PhoneNumber(string extension,string phoneNumber)
   {
       _extension = extension;
       _phoneNumber = phoneNumber;
   }
   public string GetFormattedNumberWithExt()
   {
       return string.Format("({0}){1}", _extension, _phoneNumber);
   }
}

private static void Main(string[] args)
{
   var phoneNumber=new PhoneNumber("95","2142");
   Console.WriteLine(phoneNumber.GetFormattedNumberWithExt());
}

Oczywiście jest to mieszanie logiki ze sposobem wyświetlenia oraz nie spełnia to zasady Open\Closed principle. Za każdym razem gdy chcemy zmodyfikować sposób formatowania zmuszeni jesteśmy do modyfikowania klasy. Zacznijmy od implementacji interfejsu IFormattable:

class PhoneNumber:IFormattable
{
   private readonly string _extension;
   private readonly string _phoneNumber;

   public PhoneNumber(string extension,string phoneNumber)
   {
       _extension = extension;
       _phoneNumber = phoneNumber;
   }
   private string GetFormattedNumberWithExt()
   {
       return string.Format("({0}){1}", _extension, _phoneNumber);
   }

   public string ToString(string format, IFormatProvider formatProvider)
   {
       if (string.Equals(format, "full", StringComparison.OrdinalIgnoreCase))
           return GetFormattedNumberWithExt();

       return _phoneNumber;
   }
}

private static void Main(string[] args)
{
   var phoneNumber=new PhoneNumber("95","2142");
   Console.WriteLine("{0:full}", phoneNumber);
}

Rozwiązanie trochę lepsze – wykorzystujemy standard .NET do formatowania tekstu. Niestety wciąż łamie to zasadę Open\Closed principle. Co jeśli chcemy np. wyświetlić sam kierunkowy lub kierunkowy oraz numer w postaci gwiazdek? Do tego służą interfejsy ICustomFormatter oraz IFormatProvider.

IFormatProvider jest klasą przekazywaną do IFormattable.ToString, która potrafi zwrócić klasę, która odpowiedzialna jest z kolei za formatowanie. Można oba interfejsy zaimplementować w tej samej klasie np.:

internal class Program
{
    class PhoneFormatter:ICustomFormatter,IFormatProvider
    {
        private string GetFormattedNumberWithExt(PhoneNumber phoneNumber)
        {
            return string.Format("({0}){1}", phoneNumber.Extension, phoneNumber.Number);
        }
        public string Format(string format, object arg, IFormatProvider formatProvider)
        {
            PhoneNumber phoneNumber = arg as PhoneNumber;
            if(phoneNumber==null)
                throw new ArgumentException();

            if (string.Equals(format, "full", StringComparison.OrdinalIgnoreCase))
                return GetFormattedNumberWithExt(phoneNumber);

            return phoneNumber.Number;
        }

        public object GetFormat(Type formatType)
        {
            return this;
        }
    }
    public class PhoneNumber
    {    
        public PhoneNumber(string extension,string phoneNumber)
        {
            Extension = extension;
            Number = phoneNumber;
        }
        public string Extension { get; private set; }
        public string Number { get; private set; }
    }
  
    private static void Main(string[] args)
    {
        var phoneNumber = new PhoneNumber("95", "2142");
        Console.WriteLine(string.Format(new PhoneFormatter(), "{0:full}",phoneNumber));
    }
}

W takim sposób odizolowaliśmy, logikę od prezentacji. Co jest gdy chcemy aby PhoneNumber zawsze miał jakiś domyślny formatter tak, że użytkownik nie musi za każdym razem tworzyć instancji PhoneNumberFormatter. Innymi słowy chcemy aby użytkownik mógł wykonać następujący kod:

var phoneNumber = new PhoneNumber("95", "2142");
Console.WriteLine(string.Format("{0:full}",phoneNumber));

Wystarczy, że PhoneNumber.ToString sprawdzi czy Provider jest równy NULL:

public class PhoneNumber:IFormattable
{    
   public PhoneNumber(string extension,string phoneNumber)
   {
       Extension = extension;
       Number = phoneNumber;
   }
   public string Extension { get; private set; }
   public string Number { get; private set; }

   public object GetFormat(Type formatType)
   {
       return new PhoneFormatter();
   }

   public string ToString(string format, IFormatProvider formatProvider)
   {
       IFormatProvider provider;

       if (formatProvider == null)
           provider = new PhoneFormatter();
       else
           provider = formatProvider;

       var formatter = provider.GetFormat(typeof(PhoneNumber)) as ICustomFormatter;
       
       if (formatter != null)
           return formatter.Format(format, this, formatProvider);
       
       return ToString();
   }
}

Na zakończenie chciałbym zaznaczyć, że CultureInfo implementuje IFormatProvider, który zwraca np. NumberFormatInfo czy DatetimeFormatInfo .

Leave a Reply

Your email address will not be published.