Zwykle obiekty domeny mają właściwości, które mogą być reprezentowane przez wbudowany typ, ale których prawidłowe wartości stanowią podzbiór wartości, które mogą być reprezentowane przez ten typ.
W takich przypadkach wartość można zapisać za pomocą wbudowanego typu, ale należy upewnić się, że wartości są zawsze sprawdzane w punkcie wejścia, w przeciwnym razie możemy skończyć z nieprawidłową wartością.
Jednym ze sposobów rozwiązania tego problemu jest przechowywanie wartości jako niestandardowej, struct
która ma jedno private readonly
pole bazowe typu wbudowanego i której konstruktor sprawdza podaną wartość. Dzięki temu zawsze możemy być pewni, że użyjemy tylko zweryfikowanych wartości struct
.
Możemy również zapewnić operatory rzutowania zi do wbudowanego typu bazowego, aby wartości mogły bezproblemowo wchodzić i wychodzić jako typ bazowy.
Weźmy jako przykład sytuację, w której musimy reprezentować nazwę obiektu domeny, a poprawnymi wartościami są dowolne ciągi o długości od 1 do 255 znaków włącznie. Możemy to przedstawić za pomocą następującej struktury:
public struct ValidatedName : IEquatable<ValidatedName>
{
private readonly string _value;
private ValidatedName(string name)
{
_value = name;
}
public static bool IsValid(string name)
{
return !String.IsNullOrEmpty(name) && name.Length <= 255;
}
public bool Equals(ValidatedName other)
{
return _value == other._value;
}
public override bool Equals(object obj)
{
if (obj is ValidatedName)
{
return Equals((ValidatedName)obj);
}
return false;
}
public static implicit operator string(ValidatedName x)
{
return x.ToString();
}
public static explicit operator ValidatedName(string x)
{
if (IsValid(x))
{
return new ValidatedName(x);
}
throw new InvalidCastException();
}
public static bool operator ==(ValidatedName x, ValidatedName y)
{
return x.Equals(y);
}
public static bool operator !=(ValidatedName x, ValidatedName y)
{
return !x.Equals(y);
}
public override int GetHashCode()
{
return _value.GetHashCode();
}
public override string ToString()
{
return _value;
}
}
Przykład pokazuje To- string
obsada jak implicit
jak to nigdy nie może zabraknąć, ale od- string
obsada jak explicit
jak to będzie rzucać za nieprawidłowe wartości, ale oczywiście te mogą zarówno być implicit
albo explicit
.
Należy również zauważyć, że można zainicjować tę strukturę tylko za pomocą rzutowania z string
, ale można przetestować, czy rzutowanie zakończy się niepowodzeniem za pomocą tej IsValid
static
metody.
Wydaje się, że jest to dobry wzorzec wymuszania sprawdzania poprawności wartości domen, które mogą być reprezentowane przez proste typy, ale nie widzę, aby były często używane lub sugerowane i jestem zainteresowany, dlaczego.
Moje pytanie brzmi zatem: jakie są zalety i wady korzystania z tego wzoru i dlaczego?
Jeśli uważasz, że jest to zły wzór, chciałbym zrozumieć, dlaczego i co uważasz za najlepszą alternatywę.
Uwaga : Pierwotnie zadałem to pytanie na temat przepełnienia stosu, ale zostało ono wstrzymane jako oparte głównie na opiniach (ironicznie subiektywne samo w sobie) - mam nadzieję, że może się tutaj cieszyć większym sukcesem.
Powyżej znajduje się oryginalny tekst, poniżej kilka innych przemyśleń, częściowo w odpowiedzi na odpowiedzi otrzymane tam, zanim zostały zawieszone:
- Jednym z głównych argumentów podanych w odpowiedziach była ilość kodu płyty kotła niezbędna do powyższego wzoru, zwłaszcza gdy wymaganych jest wiele takich typów. Jednak w obronie tego wzoru można to w dużej mierze zautomatyzować za pomocą szablonów i właściwie nie wydaje mi się to takie złe, ale to tylko moja opinia.
- Z koncepcyjnego punktu widzenia nie wydaje się dziwne, że podczas pracy z silnie typowanym językiem, takim jak C #, stosowanie silnie typowanej zasady dotyczy tylko wartości złożonych, a nie rozszerzanie jej na wartości, które mogą być reprezentowane przez wystąpienie wbudowany typ?
Odpowiedzi:
Jest to dość powszechne w językach w stylu ML, takich jak Standard ML / OCaml / F # / Haskell, gdzie znacznie łatwiej jest tworzyć typy opakowań. Zapewnia dwie korzyści:
ValidatedName
kiedykolwiek zawiera niepoprawną wartość, wiesz, że błąd dotyczyIsValid
metody.Jeśli dobrze zastosujesz
IsValid
metodę, masz gwarancję, że każda funkcja, która ją otrzyma,ValidatedName
faktycznie otrzyma poprawną nazwę.Jeśli potrzebujesz wykonywać operacje na łańcuchach, możesz dodać metodę publiczną, która akceptuje funkcję, która pobiera Ciąg (wartość
ValidatedName
) i zwraca Ciąg (nowa wartość) i sprawdza poprawność wyniku zastosowania funkcji. To eliminuje podstawową wartość uzyskiwania wartości String i ponownego jej zawijania.Powiązanym zastosowaniem do zawijania wartości jest śledzenie ich pochodzenia. Np. Interfejsy API systemu operacyjnego oparte na C czasami dają uchwyty zasobów jako liczby całkowite. Możesz owinąć interfejsy API systemu operacyjnego, aby zamiast tego użyć
Handle
struktury i zapewnić dostęp do konstruktora tylko tej części kodu. Jeśli kod, który tworzyHandle
s jest poprawny, zostaną użyte tylko poprawne uchwyty.źródło
Dobrze :
ValidatedString
wyraźniejszego określenia semantyki wywołania.Źle :
IsValid
przed obsadą jest trochę niewygodne.ValidatedString
jest niepoprawna / zweryfikowana.Widziałem takie rzeczy częściej
User
iAuthenticatedUser
rzeczy, w których obiekt się zmienia. To może być dobre podejście, choć wydaje się nie na miejscu w C #.źródło
Twoja droga jest dość ciężka i intensywna. Zazwyczaj definiuję jednostki domeny takie jak:
W konstruktorze encji sprawdzanie poprawności jest uruchamiane przy użyciu FluentValidation.NET, aby upewnić się, że nie można utworzyć encji z niepoprawnym stanem. Pamiętaj, że wszystkie właściwości są tylko do odczytu - możesz je ustawić tylko za pomocą konstruktora lub dedykowanych operacji na domenie.
Walidacja tego bytu to osobna klasa:
Tych walidatorów można również łatwo użyć ponownie, a ty piszesz mniej kodu z szablonem. Kolejną zaletą jest to, że jest czytelny.
źródło
Podoba mi się to podejście do typów wartości. Pomysł jest świetny, ale mam kilka sugestii / skarg na wdrożenie.
Przesyłanie : W tym przypadku nie lubię używania przesyłania. Wyraźne rzutowanie z łańcucha nie jest problemem, ale nie ma dużej różnicy między
(ValidatedName)nameValue
nowym a nowymValidatedName(nameValue)
. Więc wydaje się to trochę niepotrzebne. Implikacja rzutowania na ciąg jest najgorszym problemem. Myślę, że uzyskanie rzeczywistej wartości ciągu powinno być bardziej wyraźne, ponieważ może przypadkowo zostać przypisane do ciągu, a kompilator nie ostrzeże cię o możliwej „utracie precyzji”. Ten rodzaj utraty precyzji powinien być wyraźny.ToString : Wolę używać
ToString
przeciążeń tylko do celów debugowania. I nie sądzę, aby przywrócenie wartości pierwotnej było dobrym pomysłem. Jest to ten sam problem, co w przypadku niejawnej konwersji na ciąg znaków. Uzyskanie wartości wewnętrznej powinno być jawną operacją. Wydaje mi się, że próbujesz sprawić, aby struktura zachowywała się jak normalny ciąg znaków do kodu zewnętrznego, ale myślę, że robiąc to, tracisz część wartości, którą otrzymujesz z implementacji tego typu.Równa się i GetHashCode : Struktury domyślnie używają równości strukturalnej. Więc
Equals
iGetHashCode
dublują to zachowanie domyślne. Możesz je usunąć, a będzie to prawie to samo.źródło