Oskar Dudycz

Pragmatic about programming

Przedstawiam "Smart Constructor" - jeden z moich ulubionych wzorców

2019-10-14 oskar dudyczWzorce

front

Cześć!

Różnorodność. Nasza droga programistyczna często przypomina tunel bobslejowy. Nie mamy lewego ani prawego pasa, suniemy po jednym wytyczonym torze. Z naszych dyskusji można by zrobić nową część Street Fightera - C# vs Java, JavaScript kontra reszta świata, Perl jako niepokonany boss.

Legendarna książka The Pragmatic Programmer napisana przez Andrew Hunta i Davida Thomasa obchodzi dwudziestą rocznicę wydania. Jedną z jej myśli przewodnich jest to, żeby uczyć się różnych paradygmatów programowania, różnych języków oraz różnych technologii. Kiedy pierwszy raz to czytałem to pomyślałem - fajnie, fajnie, ale kto na to znajdzie czas - powinienem być jak najlepszym specjalistą w wąskiej dziedzinie, którą robię - w domyśle .NET. Lata później gdy obskoczyłem już praktycznie wszystkie popularne techonologie, robiłem projekty klasyczne full-stackowe, tylko frontowe, w ogóle bez frontu (czysty SQL), natywne aplikacje mobilne przyznaję chłopakom rację. Podsumowałem to nieco przekornie 2 lata temu we wpisie przekornie zatytułowanym - O tym, co gra na gitarze może dać programiście. W jednym z punktów napisałem:

“Otwarta głowa i próbowanie wielu rzeczy. Nigdy nie interesowało mnie ogniskowe granie na chwytach, chciałem od razu walić solówy jak John Petrucci. Whiskey moja żono jest dla słabych. Kumacie, melodia, Czerwone Gitary. Potem okazało się jednak, że znając akordy, jakoś tak łatwiej manewrować po gryfie i solówki jakoś tak łatwiej wchodzą. Podobnie rozgrzanie palców na solówkach rozciąga palce i ułatwia łapanie chwytów. Tak samo w programowaniu – nawet jak nienawidzisz JavaScriptu i uważasz, że aplikacje SPA to wymysł szatana, przemóż się i spróbuj. Jeśli uważasz, że programowanie funkcyjne to wymysł dla frajerów, spróbuj i wtedy oceń. Warto obserwować, testować różne koncepty programowania, różne platformy. Nawet jeśli żadnego komercyjnego projektu się w nich nie zrobi, to wracając do swojej ukochanej technologii zapewniam, że spojrzysz już na nią trochę szerzej.”

Po co ten wstęp? Bo chciałbym przybliżyć Ci wzorzec z programowania funkcyjnego - ”Smart Constructor”. Fajny, mały ale niezywkle przydatny wzorzec, bardzo podobny do wzorca fabryka. Do konkretów!

Załóżmy, że mamy taką klasę reprezentujacą dane użytkownika:

public class User
{
    public Guid Id { get; set; }

    public string UserName { get; set; }

    public string Email { get; set; }
}

oraz metodę

public class EmailNotifier
{
    public void SendRegistrationConfirmation(User user)
    {
        // (...) - some logic for sending email to the user
    }
}

Zgodnie z zasadami defensive programming powinniśmy teraz w metodzie SendRegistrationConfirmation:

  • sprawdzić czy parametr user nie jest nullem,
  • czy Email jest podany oraz ma odpowiedni format adresu e-mail,
  • itd.

Dodatkowo powinniśmy dopisać testy jednostkowe dla tych przypadków upewniając się czy te nieoczekiwane przypadki są poprawnie obsłużone.

Dla jednej metody to nie będzie taki specjalny wysiłek, ale co jeśli obiekt tej klasy będzie używany w wielu miejscach? Klasa User zdecydowanie wygląda jak taka, która będzie. Wtedy w każdym z miejsc musimy powtórzyć te same czynności. Niepotrzebna strata czasu, rozmywanie logiki biznesowej oraz wyciek abstrakcji. W tego typu sytuacjach pomaga wzorzec Smart Constructor.

Oto kroki, które musimy zrobić:

  1. Zdefiniować wszystkie konstruktory klasy jako prywatne. A najlepiej zostawić jeden, który będzie inicjował wszystkie pola klasy.
public class User
{
    public Guid Id { get; set; }

    public string UserName { get; set; }

    public string Email { get; set; }

    private User(
        Guid id,
        string userName,
        string email
    )
    {
        Id = id;
        UserName = userName;
        Email = email;
    }
}
  1. Utworzyć statyczną metodę w klasie, która zwaliduje poprawność podanych parametrów oraz utworzy jej obiekt:
public class User
{
    public Guid Id { get; set; }

    public string UserName { get; set; }

    public string Email { get; set; }

    private const int MinUserNameLength = 10;

    public static User Create(
        Guid id,
        string userName,
        string email
    )
    {
        if (id == default(Guid))
            throw new ArgumentException($"{nameof(Id)} cannot be empty");

        if (string.IsNullOrWhiteSpace(userName))
            throw new ArgumentException($"{nameof(UserName)} cannot be empty");

        if (userName.Length < MinUserNameLength)
            throw new ArgumentException($"{nameof(UserName)} needs have at least {MinUserNameLength} length.");

        if (string.IsNullOrWhiteSpace(email))
            throw new ArgumentException($"{nameof(Email)} cannot be empty");

        if (EmailValidator.IsValidEmail(email))
            throw new ArgumentException($"{nameof(Email)} needs have valid format.");

        return new User(
            id,
            userName,
            email
        );
    }

    private User(
        Guid id,
        string userName,
        string email
    )
    {
        Id = id;
        UserName = userName;
        Email = email;
    }
}
  1. Ustawić właściwości klasy jako prywatne, tak aby nie można było ich zmienić bez przejścia walidacji, lub w ogóle usunąć pola settera jeśli wartości są niezmienialne po zainicjowaniu obiektu.
public class User
{
    public Guid Id { get; }

    public string UserName { get; }

    public string Email { get; }

    private const int MinUserNameLength = 10;

    public static User Create(
        Guid id,
        string userName,
        string email
    )
    {
        if (id == default(Guid))
            throw new ArgumentException($"{nameof(Id)} cannot be empty");

        if (string.IsNullOrWhiteSpace(userName))
            throw new ArgumentException($"{nameof(UserName)} cannot be empty");

        if (userName.Length < MinUserNameLength)
            throw new ArgumentException($"{nameof(UserName)} needs have at least {MinUserNameLength} length.");

        if (string.IsNullOrWhiteSpace(email))
            throw new ArgumentException($"{nameof(Email)} cannot be empty");

        if (EmailValidator.IsValidEmail(email))
            throw new ArgumentException($"{nameof(Email)} needs have valid format.");

        return new User(
            id,
            userName,
            email
        );
    }

    private User(
        Guid id,
        string userName,
        string email
    )
    {
        Id = id;
        UserName = userName;
        Email = email;
    }
}

Dzięki tym zabiegom:

  • jedyną możliwością zainicjowania stanu obiektu jest wywołanie metody Create,
  • dzięki czemu wiemy, że zainicjowany obiekt tej klasy nigdy nie będzie miał niepoprawnych wartości. Dzięki czemu nie musimy np. sprawdzać czy UserName jest zdefiniowane i ma poprawną długość i czy Email ma poprawny format,
  • enkapsulujemy logikę walidacji biznesowej w jednym miejscu, dzięki czemu nie musimy robić walidacji w klasach używających obiekt User - np. w przywołanej wcześniej SendRegistrationConfirmation,
  • w związku powyższym wystarczy, że napiszemy testy jednostkowe do metody Create i nie musimy powielać testów w innych miejscach.

Języki funkcyjne takie jak F# lub Haskell mają wbudowane w sobie konstrukcje wspomagające użycie wzorca Smart Constructor. Ale jak widać powyżej to zaimplementowanie go w C# nie stanowi wielkiego problemu. Wzorzec ten jest również podstawowym i często spotykanym w Domain Driven Design oraz podstawą Type Driven Development.

Nawet jeśli nie lubisz programowania funkcyjnego, DDD to można jak widać z nich coś wyciągnąć i dorzucić do swojego ”toolboxa“.

Pragmatycznie.

Co o tym sądzisz? Daj znać - chętnie podyskutuję, nawet jak się nie zgadzasz. Szczególnie jak się nie zgadzasz. Czekam na Twój punkt widzenia.

That’s all folks!

Do napisania za tydzień!

Pozdrawiam Oskar

p.s. Jimmy Bogard 10 lat temu napisał, że Smart Constructor to Anty wzorzec, nie wiem co myśli aktualnie, ale możesz sobie sprawdzić co na ten temat napisał https://lostechies.com/joeybeninghove/2009/09/02/smart-constructor-anti-pattern/

p.s.2. Jeśli Cię zaciekawił ten wzorzec to polecam genialną książkę Scotta Wlaschina - Domain Modeling Made Functional - wspaniale opisuje tego typu wzorce, godna uwagi, nawet jeśli tak jak ja nie programujesz komercyjnie w języku funkcyjnym. Świetna tez do poznania mozliwości F#.

  • © Oskar Dudycz 2019-2020