Oskar Dudycz

Pragmatic about programming

Odpowiedź na pytanie o "Smart Constructor" dla kontraktów oraz cotygodniowa porcja newsów

2019-10-21 oskar dudyczWzorce

front

Cześć!

W zeszłym tygodniu pisałem o wzorcu Smart Constructor, dzisiaj chciałbym dorzucić małe post striptum i zrobić kontynuację tematu, ale najpierw garść linków z ostatniego tygodnia:

  1. Microsoft wypuścił nowy framework do Microservice’ów - Dapr. Zerknij tutaj: https://cloudblogs.microsoft.com/opensource/2019/10/16/announcing-dapr-open-source-project-build-microservice-applications/. Powiesz, że to nie nowość i już jest kilka lat? To przetrzyj oczy, tak jest to nie Dapper, to Dapr. Jak to z Microsoftem bywa nic nie może się obejść bez dramy, jak już się wydaje, że czasy starego złego MS minęły, to muszą zrobić jakiś ruch w stylu dupka. Wykazali się klasyczną ignorancją, mimo ostrzeżeń. No ale wszystko jest niby ok, buziaczki, te sprawy. Odstawiając dramę to framework jest zdaje się dosyć ciekawy, opiera się o aktorach, napisany jest, co ciekawe, w GO. Sam nie miałem jeszcze czasu go lepiej przeanalizować, jak to zrobię - opiszę szerzej. Póki co zachęcam do zgłębienia tematyki Aktorów - tutaj np. wprowadzenie zrobione przez jednego z twórców Akka.NET: https://www.youtube.com/watch?v=0KnIMDoJpZs
  2. Mojemu repozytorium o Event Sourcing w .NET stuknęło już 300 gwiazdek! Tak wiem, nic wielkiego, ale cieszę się, że ludzie uznają to za pomocne. Mam dużo planów z tym związanych, ale niestety ciągle za mało czasu…
  3. Trwają warsztaty o Event Sourcingu z Gregiem Youngiem, Adamem Dymitrukiem i Rafałem Maciągiem (https://eventmodeling.konfeo.com/pl/groups). Kwota konkretna, ale i prelegenci konkretni. Myślę, że Rafał się nie obrazi jeśli powiem, że to dopiero początek inicjatyw związanych z Event Modellingiem - nie tylko płatnych. Event Modelling to konkurencyjną dla Event Stormingu idea https://eventmodeling.org/posts/what-is-event-modeling/. Warto zaznajomić się i poczytać co to jest i jak może Ci ta idea pomóc w modelowaniu domeny biznesowej. Ja sam nie mam jeszcze w pełni wyrobionego zdania. Event Storming robiłem w praktyce i wiem, ze się sprawdza. Idea Event Modellingu jest podobna, ale wydaje się bardziej konkretnie nastawiona na konkretne elementy systemu, więc też powinna dawać radę, ale nie sprawdziłem jeszcze w praktyce, więc nie wiem
  4. No i garść linków o .NET Core 3.0.
  5. Co nowego w .NET Core 3.0 na Linux
  6. Niezawodny Andrew Lock eksploruje .NET Core 3.0
  7. Hardware intrinsics
  8. C#, Span and async

Wracając do Smart Constructor.

Seba zwrócił mi uwagę na to, że wszystko pięknie ładnie z tym Smart Constructorem, tylko jak zastosować go do kontraktów - np. do WebApi. No i czy faktycznie musimy rzucać te wyjątki?

Dla przypomnienia - Smart Constructor, to wariacja wzorca fabryka, która pozwala nam mieć pewność, że utworzony obiekt będzie poprawnie zainicjowany. Dzięki czemu np. wiemy, że pole email nie jest nullem, ma poprawny format itd. Nie musimy tego potem sprawdzać w innych miejscach, możemy tak utworzonemu obiektowi w pełni zaufać. Aby to osiągnąć:

  • definiujemy konstruktory jako prywatne,
  • ustawiamy settery propertiesów jako prywatne,
  • tworzymy publiczną statyczną metodę, która weryfikuje poprawność danych wejściowych a potem tworzy obiekt.

Przykładowo efekt końcowy wygląda tak:

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;
    }
}

Co do pytania Seby to faktycznie to jest element nad którym nie jest łatwo zapanować. Jeśli chodzi o deserializację kontraktów (przyjmijmy, że mówimy o WebApi) to jest kilka tematów, które trzeba uwzględnić:

  1. Typy otrzymywane często nie zgadzają się z typami, do których deserializujemy: np:
  2. Enumy są przesyłane jako stringi lub inty
  3. DateTime jest deserializowany ze stringa
  4. C# typy liczbowe jak long czy inne zmienno przecinkowe nie mają odzwierciedlenia w typach JavaScript (czyli JSON).
  5. Pytanie co wtedy powinniśmy zrobić gdy otrzymamy dane w kontrakcie, których się nie spodziewamy:
  6. np. błędny format,
  7. brak wartości gdy jest wymagalna,
  8. błędny typ (np. spodziewamy się inta, dostajemy string, lub odwrotnie)

Wg mnie wtedy powinniśmy zwrócić odpowiedni status – w tym przypadku 400 – Bad Request. Najgorzej jakbyśmy rzucili 500 w przypadku NullPointera lub błędu deserializacji.

  1. Tak jak wiadomo, z danych, które dostajemy z zewnątrz naszego systemu możemy się spodziewać się wszystkiego. Jeżeli wartości pól w klasie nie są wygenerowane przez nasz kod, to nie możemy temu nigdy zaufać (niezależnie czy deserializujemy JSON/XML z request HTTP czy też nawet z bazy danych – tam tez często serializujemy dane w inny sposób niż przetwarzamy w kodzie).

Oczywiście nie ma na to złotego środka. Ale jest kilka sposobów jak to zapewnić, wg mnie najlepszym, który daje nam gwarancję to utworzenie klasy requestu, która ma dokałdnie takie typy jakie otrzymamy z api, publiczny konstruktor, i wszystkie publiczne gettery/settery. Ta klasa będzie naszym typowym DTO, który jedyne co ma robić to wstępnie przetransforomować nam dane z zewnętrznego systemu do kodu.

Wracając do naszego przykładu moglibyśmy zrobić np:

public class CreateUser
{
    public string UserName { get; set; }

    public string Email { get; set; }
}

I metoda w WebApi by wyglądała np.

[HttpPost]
public async Task Post([FromBody]CreateUser request)
{
    var user = User.Create(Guid.NewGuid(), request.UserName, request.Email);

    // no i potem już właściwa logika czy coś tam
    userRepository.Add(user);

    await userRepository.SaveChanges();
}

Dzięki temu podejściu, mamy lepszą kontrolę nad deserializacją i nieoczekiwanymi błędami, a każde miejsce w kodzie używa potem w kodzie obiektów klas, po których już dobrze wiemy czego się spodziewać. Oczywiście to daje nam dodatkowy kod, ale to tez jest dobre, bo zwykle request nie ma wszystkich pól, które ma sama klasa – np. przy tworzeniu użytkownika możemy nie znać jeszcze jego Id.

Co do samego mapowania możemy użyć spokojnie (jeśli lubimy) też rozwiązań typu Automapper, np. definiując je następująco:

class IssueMappings: Profile, IMappingDefinition
{
    public IssueMappings()
    {
        CreateMap<CreateUser User>().ConstructUsing(
            request=> User.Create(Guid.NewGuid(), request.UserName, request.Email));

        // (…) inne mapowania
    }
}

A potem np.:

mapper.Map<User>(request);

Inną metodą jest wymuszenie na deserializatorze użycia konstruktora parametrowego (i zostawienie tylko tego jednego). Większość bibliotek na to pozwala – np. JSON.NET, Dapper. Tam możemy wrzucić tę walidację, która normalnie by była w statycznej metodzie. Czyli naszym Smart Constructorem jest ten właściwy konstruktor. To tez wg mnie jest ok. Metody statyczne mają o tyle lepsze możliwości bo są bardziej opisowe. Np. W przykładzie powyższej moglibyśmy mieć metody “CreateNew”, która by generowała Id od razu w sobie i dodatkowej “CreateForUpdate”, która by była wywoływana dla PUT.

Jeśli chodzi o same błędy to mnie osobiscie kiedyś one też przeszkadzały, nazywałem to nawet Exception Driven Development. Teraz zmieniłem zdanie i jestem zwolennikiem tego typu rozwiązań. Z tym, że znowu, nie musimy to robić ręcznie. Możemy użyć w środku jakiś biblitek do weryfikacji kontraktów – np. Code Contracts (https://docs.microsoft.com/en-us/dotnet/framework/debug-trace-profile/code-contracts), lub też np. Lite Guard (https://github.com/adamralph/liteguard). Nic nie stoi też na przeszkodzie użyć np. FluentValidation do tego celu.

Jeżeli rzucimy wyjątkiem to możemy to potem zmapować w łatwy sposób na własciwy kod HTTP przy pomocy np. Exception Filter w “klasycznym” ASP.NET (https://docs.microsoft.com/pl-pl/aspnet/web-api/overview/error-handling/exception-handling) lub też w poprzez Middleware w .NET Core. Zerknij:

Co o tym sądzisz?

Mam nadzieję, że te rozwinięcie tematu Cię nie znudziło, ale pytanie było wartościowe, więc chciałem to uszczegółowić.

Czekam na Twoje sugestie, co byłoby dla Ciebie ciekawym tematem do poruszenia w Newsletter. Pozdrawiam Oskar

  • © Oskar Dudycz 2019-2020