Zine.net online

Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc
w Szukaj

arkadiusz.wasniewski

Typy wyliczane czy klasy

Tak to już jest, iż programując bardzo często stajemy przed koniecznością wyboru rozwiązania, będąc  gdzieś w połowie drogi pomiędzy "najlepszymi technikami". Jeden z takich przypadków, ale bez wybrania najlepszej drogi, chciałbym opisać poniżej.

Załóżmy, iż budujemy aplikację służącą sprzedaży  Na początek będziemy wykorzystywać dwa typy dokumentów: paragon i fakturą.  W kodzie tworzymy odpowiadający temu typ wyliczany:

    internal enum DocumentType

    {

        Receipt,

        Invoice

    }

Następnie pojawiają się nowe wymagania. Potrzebujemy powiązać z typami dokumentów skrót, czyli kilkuznakowy identyfikator, który będzie wykorzystywany:

  • W czasie budowania numeru dokumentu;
  • Być może w czasie wyświetlania informacji o dokumentach;
  • Być może w czasie konwersji danych do/z systemów zewnętrznych;

oraz nazwę dokumentu, która potrzebna będzie przy:

  • Być może drukowaniu dokumentów;
  • Być może wyświetlaniu informacji o dokumentach.

Jak widzimy wymagania te wychodzą z różnych części programu. Słowa "być może" są użyte powyżej ponieważ to klient (użytkownik oprogramowania) będzie decydował (przy zakupie?) co i gdzie ma mu się wyświetlać.

Pierwsze rozwiązanie polega na zastosowaniu specjalnej klasy pomocniczej,  która przyjmując w wywołaniach metod typ dokumentu zwracałaby właściwe wartości. Prosty przykład użycia:

            DocumentType documentType = DocumentType.Invoice;

            Console.WriteLine(DocumentTypeHelper.GetCode(documentType));

            Console.WriteLine(DocumentTypeHelper.GetName(documentType));

Oczywiścia klasa pomocnicza nie musi być statyczna. Do dyspozycji mamy również nowości języka C# w wersji trzeciej czyli metody rozszerzające (extension methods):

            Console.WriteLine(documentType.GetCode());

            Console.WriteLine(documentType.GetName());

Możemy również zastosować refaktoryzację naszego typu wyliczanego do klasy. Rozwiązanie to jest bardzo popularne zwłaszcza w językach, które nie umożliwiają definiowania dozwolonych zakresów wartości dla zmiennych i parametrów metod.

    internal class DocumentType

    {

        public static readonly DocumentType Receipt =

            new DocumentType("PA", "Paragon");

 

        public static readonly DocumentType Invoice =

            new DocumentType("FK", "Faktura VAT");

 

        public string Code { get; private set; }

        public string Name { get; private set; }

 

        public DocumentType(string code, string name)

        {

            Code = code;

            Name = name;

        }

    }

Jako ciekawostkę można w tym miejscu powiedzieć, iż kompilator C# zamienia typy enumerowane na poniższe rozwiązanie  (mniej więcej):

    internal struct DocumentType : System.Enum

    {

        public const DocumentType Receipt = (DocumentType) 0;

        public const DocumentType Invoice = (DocumentType) 1;

    }

Oczywiście powyższy kod (czy też raczej pseudokod) się nie skompiluje (nie można dziedziczyć po System.Enum), ale tak to wewnątrz CLR wygląda.

Wracając do naszej aplikacji. A co począć jeśli pojawiają nam się dodatkowe wymagania w projekcie dotyczące typów dokumentów:

  • Co w przypadku jeśli użytkownik aplikacji zażyczy sobie aby zamiast skrótu FK był kod FS;
  • Co jeśli chcemy dodać nowy typ dokumentu;
  • Co jeśli musimy dodać nową właściwość opisującą typ dokumentu.

W którą stronę podążać i jakie zmiany w kodzie przeprowadzić? Pytania te wydały mi się na tyle ciekawe, iż postanowiłem na spotkaniu Warszawskiej Grupy .NET spróbować przeprowadzić małą prezentację problemu wraz dyskusją na ten temat. Całość trwała około 20 minut i oto proponowane przez uczestników dyskusji rozwiązania:

  1. Powrót do typów wyliczanych i klas pomocniczych ponieważ umieszczanie właściwości opisujących dany typ w klasie jako pól statycznych narusza zasadę pojedynczej odpowiedzialności;
  2. Pobieranie wartości łańcuchowych w momencie tworzenia instancji klas z zasobów zewnętrznych. Tudzież tworzenie klas dla danych typów w konstruktorze statycznym, który wiedziałby (zasoby zewnętrzne) z jakimi wartościami je kreować.
  3. Rezygnacja ze statycznych pól tylko do odczytu. Przeniesienie typu dokumentu do klasy:

        internal class DocumentTypeValue

        {

            public DocumentType DocumentType { get; private set; }

            public string Code { get; private set; }

            public string Name { get; private set; }

     

            public DocumentTypeValue(DocumentType documentType,

                string code, string name)

            {

                DocumentType = documentType;

                Code = code;

                Name = name;

            }

        }

  4. Opisanie elementów typu wyliczanego utworzonymi atrybutami:

        internal enum DocumentType

        {

            [Code("PA")]

            [Name("Paragon")]

            Receipt,

     

            [Code("FK")]

            [Name("Faktura VAT")]

            Invoice

        }

Moje subiektywne odczucia z dyskusji są takie, iż minimalnie wygrały rozwiązania numer 2 i 3. A co Wy o tym sądzicie?

Opublikowane 24 października 2008 12:20 przez arkadiusz.wasniewski

Komentarze:

 

apl said:

4. Rozwiązanie z pogranicza czarnej magii. Według mnie nie najlepsze, gdyż możliwości atrybutów trochę zbyt nas ograniczają, poza tym trzeba jeszcze napisać kod, który odzyska zawarte w nich informacje.

3. Metoda pozwala tworzyć egzotyczne kombinacje trzech przekazywanych w konstruktorze parametrów. Stosując ją można doprowadzić do sytuacji, w której mamy w systemie dwa dokumenty tego samego typu, lecz o różnych identyfikatorach/nazwach.

2. Bez przykładu trochę ciężko mi sobie wyobrazić, o co tak naprawdę chodzi. Dyskutujemy tutaj na temat modelu obiektowego, a to, skąd pochodzą dane, jest sprawą drugorzędną. Jeśli cały sens tego rozwiązania polega na tym, że dane pobierane są z zasobów, to według mnie ciężko ustawić je w jednym rzędzie z pozostałymi.

1. Rozwiązanie proste i efektywne, ku któremu się skłaniam. Typ wyliczeniowy w sposób symboliczny reprezentuje różne rodzaje dokumentów, a dodatkowa klasa dostarcza związane z nimi metadane. Osobiście zaimplementowałbym to trochę inaczej, niż zaproponowałeś na początku tekstu:

public class DocumentTypeInfo

{

  public DocumentType DocumentType { get; private set; }

  public DocumentType Code { get; private set; }

  public DocumentType Name { get; private set; }

  private static readonly ReceiptInfo = new DocumentTypeInfo(DocumentType.Receipt, "PA", "Paragon");

  private static readonly InvoiceInfo = new DocumentTypeInfo(DocumentType.Invoice, "FK", "Faktura VAT");

  // Nie zezwalaj na tworzenie instancji klasy z zewnątrz.

  private DocumentTypeInfo(DocumentType documentType, string code, string name)

  {

     this.DocumentType = documentType;

     this.Code = code;

     this.Name = name;

  }

  public static DocumentTypeInfo GetDocumentTypeInfo(DocumentType documentType)

  {

     switch (documentType) {

        case DocumentType.Receipt:

           return ReceiptInfo;

        case DocumentType.Invoice:

           return InvoiceInfo;

        default:

           throw new InvalidOperationException("Could not create DocumentTypeInfo for document type '" + documentType + "'.");

     }

  }

}

.

.

.

// Pobierz metadane opisujące fakturę.

DocumentTypeInfo info = DocumentTypeInfo.GetDocumentTypeInfo(DocumentType.Invoice);

Wspomnę jeszcze, że formalnie najlepszą metodą jest ta, w której całkowicie rezygnujemy z typów wyliczeniowych na rzecz w pełni obiektowej reprezentacji rodzaju dokumentu - otrzymujemy wówczas coś na wzór enuma z Javy 5. Takie podejście pozwala m.in. na dodawanie nowych typów dokumentów do systemu bez potrzeby rekompilacji kodu. Z drugiej strony typ reprezentujący rodzaj dokumentu wymaga starannego przygotowania, m.in. zaimplementowania IEquatable<T> czy metody GetHashCode. Może się okazać (znając życie - najprawdopodobniej się okaże), że nigdy nie wykorzystamy zalet wynikających ze stosowania tego modelu i tylko niepotrzebnie stracimy czas. Z tego względu jestem zwolennikiem rozwiązania pomostowego, które sprawdza się wystarczająco dobrze.

października 25, 2008 02:57
 

Wojciech Gebczyk said:

Arek:

1. Jesli typ dokumentu jest poprostu jednym z atrybutow dokumentu jak data, kolor czy wartosc, czyli wplyw na zmiennosc widoku dokumentu jest znikomy (mowiac prosciej nie masz IFa na typie dokumentu, ktory powoduje zmiane sposoby wyswietlania dokumentu) - to nie zawracaj sobie glowy enumen bo to nic nie daje. Dla uzytkownika wartosc to czesc procesu biznesowego, a od strony kodu to poprostu kolejny dropek.

2. Jesli masz w kodzie jednak troche tych IFow czy na przyklad rodzaj/typ widoku zmienia sie pod wplywem Invoice/Receipt i masz logike w stylu: "jesli paragon to drukuj tekstowo i nie dodawaj do magicznej tabeli, a jak masz fakture to drukujgraficznie i zapisuje PDF", to uzyj enuma wszedzie gdzie sie da, a w "UI" dorob mapowanie enum<->info{ name,desc }. Dlatego ze jesli bedziesz chcial dodac nowy TYP dokumentu, to ... bedziesz musial zmienic logike lub dodac nowe IF/SWITCH dla nowego pola enuma.

3. Co do zmiany per klient wartosci tego czy owego, to wbrew pozorom to wygodna rzecz nie rekompilujac aplikacji, bo... po to powstaly resource aby mozna bylo je zmieniac (oryginalnie tlumaczyc, ale co przeszkadza tlumaczyc per lient a nie jezyk? ;-) ) bez kompilowania od nowa. To opcja najprostsza i najszybsza. Ja jednak jak bym mial czas i fanaberie, to zdefiniowalbym to jako staly slownik (staly wzgledem klienta), wrzucil do bazy i przy starcie ladowal takie mapowanie wszystkich slownikow (bo pewnienie jeden by sie znalazl w systemie). Rozwiazanie sprawdzone wielokrotnie (co najmniej 4 razy ;-) ). Zauwaz ze takei sliwniki sa "malo zmienialne" wiec mozna taka mape/cache uczynic persistowalna... (deserializacje pewnei bedzie szybsza niz pierwsze wzbudzenie strzalu do SQL :-) )

października 27, 2008 00:01
 

arkadiusz.wasniewski said:

Dzięki za wszystkie wypowiedzi. Przepraszam, że tak późno odpowiadam, ale choroba jakowaś znów mnie powaliła.

Zastanawiając się nad najlepszym rozwiązaniem doszedłem do wniosku, iż warto zrefaktoryzować program: z typem dokumentu powiązany jest również szablon, według którego budowany jest numer oraz liczba używana do generowania unikalnego numeru dokumentu. Z tych też przyczyn odrzuciłem rozwiązanie z atrybutami oraz rozwiązanie z typem wyliczanym i samodzielnymi klasami pomocniczymi - nie zgadzam się też z opinią, iż umieszczenie właściwości opisujących dokument w jednej klasie oznacza mieszanie odpowiedzialności.

Wydaje mi się, iż w tym przypadku najlepiej będzie dane składować w tabeli - jeden wiersz dla każdego typu dokumentu. Trochę mi szkoda rozwiązania gdzie dokumenty były zdefiniowane jako pola statyczne. Ale cóż. Oczywiście typ wyliczeniowy pozostanie, ot choćby w celu porównań i składowania danych w bazie. Do tworzenia obiektów użyje fabryki, dzięki czemu będę mógł zadbać aby tylko jedna instancja danego typu istniała (co ma znaczenie, jeśli będę chciał tam pamiętać licznik dla numeru dokumentu).

Jeszcze raz dzięki za wszystkie wypowiedzi.

Pozdrawiam

Arek

października 30, 2008 13:35
Komentarze anonimowe wyłączone
W oparciu o Community Server (Personal Edition), Telligent Systems