Udostępnij przez


Implementacja obiektów wartościowych

Wskazówka

Ta treść jest fragmentem eBooka "Architektura mikrousług .NET dla konteneryzowanych aplikacji .NET", dostępnego na .NET Docs lub jako bezpłatny plik PDF do pobrania i czytania w trybie offline.

Miniatura okładki eBooka „Architektura mikrousług platformy .NET dla konteneryzowanych aplikacji platformy .NET”.

Jak opisano we wcześniejszych sekcjach dotyczących jednostek i agregacji, tożsamość jest podstawowa dla jednostek. Istnieje jednak wiele obiektów i elementów danych w systemie, które nie wymagają tożsamości ani jej śledzenia, takich jak obiekty wartości.

Obiekt wartości może odwoływać się do innych jednostek. Na przykład w aplikacji, która generuje trasę opisującą sposób dotarcia z jednego punktu do drugiego, ta trasa byłaby obiektem wartości. Byłaby to migawka punktów na określonej trasie, ale sugerowana trasa nie będzie miała tożsamości, mimo że wewnętrznie może odnosić się do jednostek, takich jak City, Road itp.

Rysunek 7–13 przedstawia obiekt wartości Address w agregacji Order.

Diagram przedstawiający obiekt Address value-object wewnątrz elementu Order Aggregate.

Rysunek 7–13. Obiekt wartości adresu w agregacji Order

Jak pokazano na rysunku 7–13, jednostka zwykle składa się z wielu atrybutów. Na przykład Order jednostka może być modelowana jako jednostka z tożsamością i składać się wewnętrznie z zestawu atrybutów, takich jak OrderId, OrderDate, OrderItems itp. Ale adres, który jest po prostu złożoną wartością składającą się z kraju/regionu, ulicy, miasta itp., i nie ma tożsamości w tej domenie, musi być modelowany i traktowany jako obiekt wartości.

Ważne cechy obiektów wartości

Istnieją dwie główne cechy obiektów wartości:

  • Nie mają tożsamości.

  • Są one niezmienne.

Pierwsza cecha została już omówiona. Niezmienność jest ważnym wymaganiem. Wartości obiektu wartości muszą być niezmienne po utworzeniu obiektu. W związku z tym podczas konstruowania obiektu należy podać wymagane wartości, ale nie można zezwolić na ich zmianę w okresie istnienia obiektu.

Obiekty wartości umożliwiają wykonywanie pewnych trików zwiększających wydajność dzięki ich niezmiennej naturze. Dotyczy to szczególnie systemów, w których mogą istnieć tysiące wystąpień obiektów wartości, z których wiele ma te same wartości. Ich niezmienny charakter pozwala na ich ponowne użycie; mogą być obiektami zamiennymi, ponieważ ich wartości są takie same i nie mają tożsamości. Ten typ optymalizacji może czasami mieć różnicę między oprogramowaniem, które działa wolno, a oprogramowaniem o dobrej wydajności. Oczywiście wszystkie te przypadki zależą od środowiska aplikacji i kontekstu wdrożenia.

Implementacja obiektu wartości w języku C#

Jeśli chodzi o implementację, można mieć klasę bazową obiektu wartości, która ma podstawowe metody narzędziowe, takie jak równość, na podstawie porównania wszystkich atrybutów (ponieważ obiekt wartości nie może być oparty na tożsamości) i innych podstawowych cech. W poniższym przykładzie przedstawiono klasę bazową obiektu wartości używaną w mikrousłudze zamówień z eShopOnContainers.

public abstract class ValueObject
{
    protected static bool EqualOperator(ValueObject left, ValueObject right)
    {
        if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
        {
            return false;
        }
        return ReferenceEquals(left, right) || left.Equals(right);
    }

    protected static bool NotEqualOperator(ValueObject left, ValueObject right)
    {
        return !(EqualOperator(left, right));
    }

    protected abstract IEnumerable<object> GetEqualityComponents();

    public override bool Equals(object obj)
    {
        if (obj == null || obj.GetType() != GetType())
        {
            return false;
        }

        var other = (ValueObject)obj;

        return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
    }

    public override int GetHashCode()
    {
        return GetEqualityComponents()
            .Select(x => x != null ? x.GetHashCode() : 0)
            .Aggregate((x, y) => x ^ y);
    }
    // Other utility methods
}

ValueObject jest typu abstract class, ale w tym przykładzie nie przeciąża operatorów == i !=. Możesz zdecydować się na to, co spowoduje delegowanie porównań do przesłonięcia Equals. Rozważmy na przykład następujące przeciążenia operatora dla ValueObject typu:

public static bool operator ==(ValueObject one, ValueObject two)
{
    return EqualOperator(one, two);
}

public static bool operator !=(ValueObject one, ValueObject two)
{
    return NotEqualOperator(one, two);
}

Możesz użyć tej klasy podczas implementowania swojego rzeczywistego obiektu wartości, tak jak w przypadku obiektu wartości Address pokazanego w poniższym przykładzie.

public class Address : ValueObject
{
    public String Street { get; private set; }
    public String City { get; private set; }
    public String State { get; private set; }
    public String Country { get; private set; }
    public String ZipCode { get; private set; }

    public Address() { }

    public Address(string street, string city, string state, string country, string zipcode)
    {
        Street = street;
        City = city;
        State = state;
        Country = country;
        ZipCode = zipcode;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        // Using a yield return statement to return each element one at a time
        yield return Street;
        yield return City;
        yield return State;
        yield return Country;
        yield return ZipCode;
    }
}

Ta implementacja Address obiektu wartości nie ma tożsamości, i dlatego nie zdefiniowano dla niego pola identyfikatora ani w Address definicji klasy, ani w ValueObject definicji klasy.

Brak pola identyfikatora w klasie do użycia przez program Entity Framework (EF) nie był możliwy do czasu, gdy program EF Core 2.0 znacznie ułatwia implementowanie obiektów o lepszej wartości bez identyfikatora. To właśnie wyjaśnienie następnej sekcji.

Można argumentować, że obiekty wartości, będąc niezmienne, powinny być tylko do odczytu (czyli mają właściwości tylko do odczytu), i to rzeczywiście prawda. Jednak obiekty wartości są zwykle serializowane i deserializowane podczas przechodzenia przez kolejki komunikatów. Bycie tylko do odczytu uniemożliwia deserializatorowi przypisanie wartości, więc wystarczy pozostawić je jako private set, co jest wystarczająco ograniczone do odczytu, aby było praktyczne.

Semantyka porównania obiektów wartości

Dwa wystąpienia Address typu można porównać przy użyciu wszystkich następujących metod:

var one = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
var two = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");

Console.WriteLine(EqualityComparer<Address>.Default.Equals(one, two)); // True
Console.WriteLine(object.Equals(one, two)); // True
Console.WriteLine(one.Equals(two)); // True
Console.WriteLine(one == two); // True

Gdy wszystkie wartości są takie same, porównania są poprawnie oceniane jako true. Jeśli nie wybrano przeciążenia operatorów == i !=, wtedy ostatnie porównanie one == two zostanie ocenione jako false. Aby uzyskać więcej informacji, zobacz Przeciążenie operatorów równości valueObject.

Jak utrwalać obiekty wartości w bazie danych za pomocą programu EF Core 2.0 lub nowszego

Pokazano, jak zdefiniować obiekt wartości w modelu domeny. Ale jak można rzeczywiście utrwalić go w bazie danych przy użyciu programu Entity Framework Core, ponieważ zwykle jest ona przeznaczona dla jednostek z tożsamością?

Podstawowe i starsze podejścia korzystające z programu EF Core 1.1

Na początku ograniczeniem przy korzystaniu z EF Core 1.0 i 1.1 było to, że nie można było używać typów złożonych, jak to definiuje EF 6.x w tradycyjnym .NET Framework. W związku z tym w przypadku korzystania z programu EF Core 1.0 lub 1.1 należy przechowywać obiekt wartości jako jednostkę EF z polem identyfikatora. Następnie wyglądało to bardziej jak obiekt wartości bez tożsamości, można ukryć jego identyfikator, aby wyjaśnić, że tożsamość obiektu wartości nie jest ważna w modelu domeny. Możesz ukryć ten identyfikator, używając go jako właściwości ukrytej. Ponieważ konfiguracja ukrywania identyfikatora w modelu jest ustawiona na poziomie infrastruktury EF, będzie to niepostrzegalne dla modelu domeny.

W początkowej wersji aplikacji eShopOnContainers (.NET Core 1.1) ukryty identyfikator wymagany przez infrastrukturę platformy EF Core został zaimplementowany w następujący sposób na poziomie DbContext przy użyciu interfejsu API Fluent w projekcie infrastruktury. W związku z tym identyfikator był ukryty z punktu widzenia modelu domeny, ale nadal obecny w infrastrukturze.

// Old approach with EF Core 1.1
// Fluent API within the OrderingContext:DbContext in the Infrastructure project
void ConfigureAddress(EntityTypeBuilder<Address> addressConfiguration)
{
    addressConfiguration.ToTable("address", DEFAULT_SCHEMA);

    addressConfiguration.Property<int>("Id")  // Id is a shadow property
        .IsRequired();
    addressConfiguration.HasKey("Id");   // Id is a shadow property
}

Jednak trwałość tego obiektu wartości w bazie danych została wykonana jak zwykła jednostka w innej tabeli.

W przypadku programu EF Core 2.0 lub nowszego istnieją nowe i lepsze sposoby utrwalania obiektów wartości.

Utrwalanie obiektów wartości jako należących do niego typów jednostek w programie EF Core 2.0 lub nowszym

Nawet w przypadku niektórych luk między wzorcem obiektu wartości kanonicznej w DDD a typem jednostki posiadanej w EF Core, jest to obecnie najlepszy sposób utrwalania obiektów wartości z EF Core 2.0 lub później. Ograniczenia można zobaczyć na końcu tej sekcji.

Funkcja typu jednostki własności została dodana do platformy EF Core od wersji 2.0.

Typ jednostki należącej umożliwia mapowanie typów, które nie mają własnej tożsamości jawnie zdefiniowanej w modelu domeny i są używane jako właściwości, takie jak obiekt wartości, w ramach dowolnej jednostki. Typ jednostki będącej własnością ma taki sam typ CLR jak inny typ jednostki (czyli jest to zwykła klasa). Jednostka zawierająca definiowaną nawigację jest jednostką właściciela. Podczas wykonywania zapytań względem właściciela, domyślnie są uwzględniane jego typy.

Patrząc na model domenowy, typ posiadany wydaje się, jakby nie miał żadnej tożsamości. Jednak w rzeczywistości typy właściciela mają tożsamość, ale właścicielska właściwość nawigacyjna jest częścią tej tożsamości.

Tożsamość wystąpień typów należących do użytkownika nie jest całkowicie własna. Składa się z trzech składników:

  • Tożsamość właściciela

  • Właściwość nawigacji wskazująca na nie

  • W przypadku kolekcji typów będących własnością niezależny składnik (obsługiwany w programie EF Core 2.2 lub nowszym).

Na przykład w modelu domeny Ordering w modelu eShopOnContainers, w ramach jednostki Order obiekt wartości Address jest implementowany jako typ jednostki posiadanej w jednostce nadrzędnej, czyli w jednostce Order. Address jest typem bez właściwości tożsamości zdefiniowanej w modelu domeny. Jest on używany jako właściwość typu Zamówienie, aby określić adres wysyłki dla określonego zamówienia.

Zgodnie z konwencją tajny klucz podstawowy jest tworzony dla typu będącego w posiadaniu i zostanie przypisany do tej samej tabeli co właściciel przy użyciu podziału tabeli. Dzięki temu można używać typów własnościowych podobnie jak typów złożonych w EF6 w tradycyjnej platformie .NET Framework.

Należy pamiętać, że typy własności nigdy nie są odnajdywane zgodnie z konwencją w programie EF Core, dlatego należy je jawnie zadeklarować.

W eShopOnContainers, w pliku OrderingContext.cs, w metodzie OnModelCreating() stosowane jest wiele konfiguracji infrastruktury. Jedna z nich jest powiązana z jednostką Order.

// Part of the OrderingContext.cs class at the Ordering.Infrastructure project
//
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new ClientRequestEntityTypeConfiguration());
    modelBuilder.ApplyConfiguration(new PaymentMethodEntityTypeConfiguration());
    modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
    modelBuilder.ApplyConfiguration(new OrderItemEntityTypeConfiguration());
    //...Additional type configurations
}

W poniższym kodzie infrastruktura trwałości jest definiowana dla jednostki Order:

// Part of the OrderEntityTypeConfiguration.cs class
//
public void Configure(EntityTypeBuilder<Order> orderConfiguration)
{
    orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA);
    orderConfiguration.HasKey(o => o.Id);
    orderConfiguration.Ignore(b => b.DomainEvents);
    orderConfiguration.Property(o => o.Id)
        .ForSqlServerUseSequenceHiLo("orderseq", OrderingContext.DEFAULT_SCHEMA);

    //Address value object persisted as owned entity in EF Core 2.0
    orderConfiguration.OwnsOne(o => o.Address);

    orderConfiguration.Property<DateTime>("OrderDate").IsRequired();

    //...Additional validations, constraints and code...
    //...
}

W poprzednim kodzie metoda orderConfiguration.OwnsOne(o => o.Address) określa, że właściwość Address jest bytową jednostką typu Order.

Domyślnie konwencje EF Core nazywają kolumny bazy danych według właściwości typu jednostki będącej własnością jako EntityProperty_OwnedEntityProperty. W związku z tym wewnętrzne właściwości Address pojawią się w Orders tabeli z nazwami Address_Street, Address_City (i tak dalej dla State, Country i ZipCode).

Możesz dołączyć płynną metodę Property().HasColumnName() , aby zmienić nazwy tych kolumn. W przypadku, gdy Address jest właściwością publiczną, mapowania będą podobne do następujących:

orderConfiguration.OwnsOne(p => p.Address)
                            .Property(p=>p.Street).HasColumnName("ShippingStreet");

orderConfiguration.OwnsOne(p => p.Address)
                            .Property(p=>p.City).HasColumnName("ShippingCity");

Możliwe jest łączenie metody OwnsOne w płynne mapowanie. W poniższym hipotetycznym przykładzie OrderDetails jest właścicielem BillingAddress i ShippingAddress, które oba są typu Address. Następnie OrderDetails jest własnością Order typu.

orderConfiguration.OwnsOne(p => p.OrderDetails, cb =>
    {
        cb.OwnsOne(c => c.BillingAddress);
        cb.OwnsOne(c => c.ShippingAddress);
    });
//...
//...
public class Order
{
    public int Id { get; set; }
    public OrderDetails OrderDetails { get; set; }
}

public class OrderDetails
{
    public Address BillingAddress { get; set; }
    public Address ShippingAddress { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
}

Dodatkowe szczegóły dotyczące typów jednostek posiadanych przez użytkownika

  • Typy własności są definiowane podczas konfigurowania właściwości nawigacji do określonego typu przy użyciu interfejsu API OwnsOne fluent.

  • Definicja typu należącego w naszym modelu metadanych składa się z typu właściciela, właściwości nawigacji oraz typu CLR tego typu.

  • Tożsamość (klucz) wystąpienia typu będącego własnością w naszej architekturze jest złożona z tożsamości typu właściciela i definicji typu będącego własnością.

Możliwości posiadanych jednostek

  • Typy własne mogą odwoływać się do innych jednostek, zarówno będących typami własnymi (zagnieżdżone typy własne), jak i typami niewłasnymi (zwykłe właściwości nawigacyjne odwołujące do innych jednostek).

  • Można mapować ten sam typ CLR co różne typy należące do tej samej jednostki właściciela za pomocą oddzielnych właściwości nawigacji.

  • Dzielenie tabeli jest konfigurowane zgodnie z konwencją, ale można z tego zrezygnować, mapując typ do innej tabeli za pomocą metody ToTable.

  • Chętne ładowanie jest wykonywane automatycznie dla posiadanych typów, to znaczy, że nie ma potrzeby wywoływania .Include() w zapytaniu.

  • Można skonfigurować za pomocą atrybutu [Owned], przy użyciu programu EF Core 2.1 lub nowszego.

  • Może obsługiwać kolekcje typów należących do użytkownika (przy użyciu wersji 2.2 lub nowszej).

Ograniczenia dotyczące jednostek własnych

  • Nie można utworzyć DbSet<T> właścicielskiego typu (zgodnie z zamysłem).

  • Nie można wywołać ModelBuilder.Entity<T>() na posiadanych typach (obecnie zgodnie z założeniami projektu).

  • Brak obsługi opcjonalnych (tj. dopuszczających wartość null) typów powiązanych z właścicielem i mapowanych w tej samej tabeli (tj. przy użyciu podziału tabeli). Jest to spowodowane tym, że mapowanie jest wykonywane dla każdej właściwości, nie ma oddzielnego sentinela dla całej złożonej wartości null.

  • Brak obsługi mapowania dziedziczenia dla typów należących do właścicieli, ale powinno być możliwe mapowanie dwóch typów liści w tych samych hierarchiach dziedziczenia co różne typy własności. Program EF Core nie będzie wnioskował o tym, że są one częścią tej samej hierarchii.

Główne różnice w typach złożonych EF6

  • Podział tabeli jest opcjonalny, co oznacza, że można je mapować na oddzielną tabelę, a typy nadal pozostaną przypisane.

Dodatkowe zasoby