Fluent Api
Jest wiele czynników, które mówią o jakości kodu, jednym z nich jest jego czytelność. W jaki sposób można, bez dużego nakładu pracy, poprawić czytelność kodu? Czy wielkość i poziom skomplikowania projektu mają wpływ na czytelność kodu? Kto jest odpowiedzialny z powstawanie nieczytelnego kodu i czy czytelność kodu mówi coś o profesjonalizmie programisty? W jaki sposób można zapobiegać a w jaki sposób leczyć zainfekowany kod? O tym wszystkim w dzisiejszym poście.
„Lepszy jest kod dobrze napisany, ale niedziałający, niż kod działający, a źle napisany”
Nie pamiętam, kto jest autorem tych słów, ale osobiście się z nimi zgadzam. Smutne to, bo każdy programista chce by jego kod zrewolucjonizował świat, tak jak wprowadzony do użytku w roku 1978 system GPS zmienił sposób patrzenia na mapę.
Wiadomo, że dla „biznesu” świat się musi kręcić, kod pracować, a faktury wystawiać, dlatego warto by zadbać o czystość kodu, zanim jeszcze pojawią się problemy, a jednocześnie pamiętać, że stworzony przez nas kod z dużym prawdopodobieństwem będzie przez kogoś rozwijany. I o ile znajomość alfabetu jest niezbędna, o tyle nazywanie zmiennych dowolnymi, pojedynczymi literami nie doda nam punktów do respektu „na zakładzie”.
Dlaczego zwracam uwagę na czystość kodu? Bo jeśli czyta się kod, którego nie jest się autorem, albo, co gorsza pisało się ten kod samemu, tylko kilka miesięcy wcześniej, warto mieć możliwość szybkiego zorientowania się, co autor miał na myśli. Domyślanie się jest ciężkim procesem myślowym, który może skutecznie spożytkować zapasy glukozy we krwi, przez co wydajność mózgu spada w tempie wykładniczym, a to boli. Jak temu zaradzić?
Jest wiele technik dbania o czystość kodu: odpowiednie nadawanie nazw zmiennym/klasom/metodom, stosowanie konwencji odnośnie formatowania kodu, rozdzielenie funkcjonalności na mniejsze kawałki zgodnie z single responsibility principle itd. It.
Techniką nad którą chciałbym się dzisiaj pochylić jest tworzenie tzw. Fluent Api. Nie jest to technika nowa, bo pisał o niej Martin Fowler w 2005 roku, tym niemniej warto ją znać. Prosty przykład. Ustawianie stanu w obiekcie klasy Task, niby prosta domenowo klasa. Jakiś temat, jakiś opis itp.
internal class Task { public string Subject { get; set; } public DateTime StartDateTime { get; set; } public TimeSpan Duration { get; set; } public string Description { get; set; } public ICollection<Comment> Comments { get; set; } }
I od razu mały konkurs: co czyta się lepiej? To?
public static void MakeNormal(Task task) { task.Subject = "Fluent Api"; task.Description = "Write blog post about Fluent Api"; task.StartDateTime = new DateTime(2016, 01, 28); task.Duration = new TimeSpan(1, 15, 0); var comment1 = new Comment(); comment1.Body = "First Comment"; comment1.CreationDateTime = DateTime.Now; task.Comments.Add(comment1); var comment2 = new Comment(); comment2.Body = "Second Comment"; comment2.CreationDateTime = DateTime.Now; task.Comments.Add(comment2); }
Czy może to?
public static void MakeFluent(Task task) { task.Set .Subject("Fluent Api") .Description("Write blog post about Fluent Api") .StartNow() .DurationInMinuts(75) .Comment("First Comment") .Comment("Second Comment"); }
Oczywiście znajdą się masochiści, który zagłosują na pierwszą opcję. Jak to pisał Kurt Vogenut w książce pt. „Rzeźnia numer pięć”, opisując dramat umierających w czasie nalotów Drezna przeprowadzonego w nocy 13-14 lutego 1945, „Zdarza się”.
Osiągnięcie efektu z drugiego przykładu ogranicza się jedynie do stworzenia klasy, która posiada referencję do obiektu klasy Task i upraszczając posiadała odpowiednie metody, zmieniające stan obiektu, które zwracają same siebie.
Poniżej pełen kod klasy Task
internal class Task { private readonly TaskFluentInterface _set; public Task() { Comments = new List(); _set = new TaskFluentInterface(this); } public string Subject { get; set; } public DateTime StartDateTime { get; set; } public TimeSpan Duration { get; set; } public string Description { get; set; } public ICollection Comments { get; set; } public TaskFluentInterface Set { get { return _set; } } }
Rozpoczęcie przypisania propertisów klasy Task, poprzez Fluent Api jest propertis „Set”, który zwraca nam referencję do obiektu klasy TaskFluentInterface. Żeby możliwe było składanie dalszych elementów zgodnie z zasadami Fluent Api, wszelkie kolejne wywołania api muszę zwracać ten sam typ, co przedstawię na przykładzie metody ustawiającej Temet w obiekcie klasy Task.
internal class TaskFluentInterface { private readonly Task _task; public TaskFluentInterface(Task task) { _task = task; } public TaskFluentInterface Subject(string subject) { _task.Subject = subject; return this; } ... }
Kolejne właściwości, które ustanawiają stan obiektu klasy Task tworzy się analogicznie. Dzięki temu, że obiekt klas Task posiada referencję do obiektu klasy TaskFluentInterface, który to otrzymuje w konstruktorze referencję do obiektu klasy Task, wszelkie zmiany dokonane przy użyciu Fluent Api zmieniają stan obiektu początkowego. Taka konstrukcja zwalnia nas z obowiązku pisania metody w klasie TaskFluentInterface zwracającej obiekt klasy Task. Jeżeli zrezygnujemy z przekazywania referencji do klasy Fluent Api, w celu uzyskania obiektu musimy napisać metodę, która ta takowy obiekt nam zwróci i co ważne taka metoda w ciągu wywołań Fluent Api musi się znajdować na samym końcu.
Podsumowując:
- Dbanie o czystość kodu jest ważna, bo pozwala szybko zrozumieć intencje autora,
- Tworzenie Fluent Api nie jest wyższą szkołą jazdy, ale w znaczący sposób ułatwia zrozumienie kodu
- Metody zmieniające stan klasy we Fluent Api muszą zwracać ten sam typ
- Przekazując referencję do Fluent Api nie musimy tworzyć metod zwracających obiekt „podstawowy”, jak to zrobimy to musimy stworzyć odpowiednią metodę i wywołać ją jako ostatnią w ciągu wywołań Fluent Api
Kod źródłowy z przykładów można pobrać z GitHuba https://github.com/kamiljozwiaknet/Blog.git