Piszę o programowaniu w ASPNET CORE. Jaką architekturę wybrać dla swojego projektu

Programowanie 101. Enkapsulacja, hermetyzacja czy kapsułkowanie?

Przeglądając kod innych ludzi można napotkać (w deklaracjach klas) na private set. Zastanawiałeś się kiedyś do czego jest to używane? Czemu należy tak wszędzie pisać? A może nie wszędzie? Czym jest enkapsulacja?

Po co?

To co widzisz to zastosowanie zasady enkapsulacji – hermetyzacji -kapsułkowania (to ostatnie tłumaczenie jest dziwne no ale 😆). Mówi ona o tym by obiekty, będące modelami rzeczywistych bytów, odzwierciedlały również ich zachowania – nie tylko charakterystyki (properties).

Przykładowo:
Mamy obiekt (np. Bob) klasy HUMAN, która ma pole AGE (z private set). Każdy człowiek rodząc się ma AGE równy zero, po czym mija dzień/tydzień/rok i nasz licznik podnosi się o jeden dzień/tydzień/rok. Zatem jeśli pole AGE w klasie HUMAN wyglądało by tak:

public int Age { get; set; }

To możliwe by było ustawienie wieku na nierealną wartość – albo co gorsze, realną lecz nie poprawną.

bob.Age = 99;

Co możemy zrobić by się tego ustrzec?

Czym jest enkapsulacja?

Pominę definicje znane z wstępu do programowania i grubych książek do Cpp. Zatem, najprościej mówiąc: wszystko rozbija się o trzy słowa kluczowe (a dokładniej to modyfikatory dostępu)

  • public – dostęp do pól i typów oznaczonych tym modyfikatorem mamy z każdego miejsca w kodzie – nic nas nie ogranicza
  • private – dostęp do tych pól i typów mamy tylko z wnętrza klasy, bądź struktury w której się znajdują
  • protected – dostęp do tych pól i typów mamy tylko z wnętrza klasy w której się znajdują, bądź w klasie dziedziczącej po niej.

Podsumowując:

  • jeśli chcemy mieć pełną dowolność przypisywania i pobierania wartości zmiennej – używamy public
public type PropertyName;
  • jeśli chcemy ukryć dane pole – używamy private
private type PropertyName;
  • jeśli chcemy umożliwić pobieranie wartości, ale nie jej przypisywanie robimy
public type PropertyName {get; private set;}

A tu kilka dodatkowych modyfikatorów i ich reprezentacja graficzna

To jak to użyć?

Jedną z opcji jest taka implementacja:

public class Human
{
	public int Age { get; private set; }
	public Human()
	{
		Age = 0;
	}
	public void MakeOlder()
	{
		Age++;
	}
}
// i używamy tak
var bob = new Human();
bob.MakeOlder();
Console.WriteLine(bob.Age);

W większości przypadków pewnie stworzylibyśmy również konstruktor przyjmujący parametr określający początkowy wiek. Nie zrobiłem tego celowo, aby pokazać pewien problem.

Jak niby ma zadziałać serializacja?

Załóżmy za klasa HUMAN wygląda jak poprzednio i wykonujemy taki kod:

static void Main(string[] args)
{
	var bob = new Human();
	for (int i = 0; i < 10; i++)
	{
		bob.MakeOlder();
	}
	Console.WriteLine($"Bobs age is {bob.Age}!");

	var serializedBob = JsonConvert.SerializeObject(bob);
	Console.WriteLine($"Serialized bob looks like {serializedBob}");           

	var newBob = JsonConvert.DeserializeObject<Human>(serializedBob);
	Console.WriteLine($"The new Bobs age is {newBob.Age}!");

	Console.ReadKey();
}

Po wykonaniu tego kodu dostaniemy taki wynik:

enkapsulacja

Czemu? Odpowiedź jest stosunkowo prosta. Ponieważ biblioteka serializująca nie miała dostępu do konstruktora przyjmującego parametru wiek… I co teraz? Przecież nie chcemy takiego konstruktora tylko dla serializacji! Zaproponuję dwa rozwiązania:

  1. Atrybut [JsonProperty]
[JsonProperty]
public int Age { get; private set; }

2. Wykorzystamy bibliotekę JsonNet.ContractResolvers. Dzięki niej nie będziemy zaśmiecać kodu domenowego atrybutami związanymi z konkretną biblioteką. Ps. Dla tych co używają ASP.NET (albo po prostu IOC) – można podać te ustawienia jako domyślne przy rejestracji serwisu. [LINK]

Install-Package JsonNet.ContractResolvers -Version 1.1.0
var settings = new JsonSerializerSettings
{
    ContractResolver = new PrivateSetterContractResolver()
};
    var newBob = JsonConvert.DeserializeObject<Human>(serializedBob, settings);

To utrudnia testy!

Niestety nie udało mi się znaleźć żadnego „dobrego” rozwiązania ale można np. użyć refleksji:

var bob = new Human();
typeof(Human).GetProperty(nameof(Human.Age)).SetValue(bob, 15);

Albo moim zdaniem lepsze rozwiązanie – zamiast private set, używamy protected set. Następnie w folderze testów tworzymy klasę:

class TestHuman : Human
    {
        public TestHuman(int age)
        {
            Age = age;
        }
    }

I rzutujemy ją na typ podstawowy (HUMAN) w zależności od przypadku, czy to w mokowanej metodzie czy jako parametr testowanej metody.

W taki sposób warstwa domenowa jest nienaruszona, a w testach też wszystko gra. No aleeeee, trochę boilerplate. Każda klasa której chcemy nadać specyficzną wartość musi mieć swój odpowiednik w testach.

Na koniec znalazłem jeszcze jedna możliwość – pretenduję ona do miana najlepszej. Właściwość AGE poprzedzamy słowem kluczowym virtual, przez co pozwalamy ją przysłaniać (w klasach dziedziczących).

public virtual int Age { get; private set; }

A następnie wykorzystujemy bibliotekę od mockowania (w moim przypadku Moq). Problem solved.

var bob = new Mock<Human>();
bob.SetupGet(x => x.Age).Returns(15);

A co z Automapperem?

Mimo że zdania „czy powinniśmy używać mapperów?” są podzielone, to jednak duża część społeczeństwa programistów nie waha się z nich korzystać. Sprawdźmy zatem jak się zachowa AutoMapper w przypadku private set.

public class Human
{
	public int Age { get; private set; }

	public Human()
	{
		Age = 0;
	}
	public void MakeOlder()
	{
		Age++;
	}
}
public class MappedHuman
{
	public int Age { get; private set; }

	public MappedHuman()
	{
		Age = 0;
	}
	public void MakeOlder()
	{
		Age++;
	}
}
static void Main(string[] args)
{
	var bob = new Human();
	for (int i = 0; i < 10; i++)
	{
		bob.MakeOlder();
	}
	var config = new MapperConfiguration(cfg =>
	{
		cfg.CreateMap<MappedHuman, Human>();
		cfg.CreateMap<Human, MappedHuman>();
	});
	var mapper = config.CreateMapper();

	var mappedBob = mapper.Map<MappedHuman>(bob);
	Console.WriteLine($"Mapped Bob's age is {mappedBob.Age}");

	var reMappedbob = mapper.Map<Human>(mappedBob);
	Console.WriteLine($"ReMapped Bob's age is {reMappedbob.Age}");
}

O dziwo – DZIAŁA!

Podsumowanie

Jak sam widzisz, trochę się trzeba narobić żeby móc wykorzystać zalety z private set. Jednak uważam że warto, głównie z racji że w dłuższym rozliczeniu ułatwi to debugowanie i refaktor kodu.

Jednak trzeba uważać na przesadne wykorzystywanie z tej zasady! Klasy typu DTO, Output, Result – nie potrzebują prywatnych pól / modyfikatorów dostępu. Trzeba pamiętać że te typy służą by przekazać dane, więc nie ma sensu je hermetyzować.

Previous

Clean Architecture 101. Tworzenie solucji w .NET CORE.

2 Comments

  1. Kamil

    Bardzo ciekawie, też jestem zwolennikiem takiego podejścia. Osobiście dla mnie największą korzyścią stosując takie podejście, jest porządek i spójność. Stosując private set i metody typu MakeOlder() z każdego miejsca aplikacji mamy pewność że tworząc obiekty będą one takie same i stworzone/edytowane wg pewnych założeń z każdego miejsca.

1 Pingback

  1. dotnetomaniak.pl

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Powered by WordPress & Theme by Anders Norén