Dostęp do repozytoriów z domeny

14

Załóżmy, że mamy system rejestrowania zadań. Gdy zadanie jest rejestrowane, użytkownik określa kategorię, a zadanie domyślnie ma status „Zaległy”. Załóżmy w tym przypadku, że kategorię i status należy zaimplementować jako byty. Normalnie zrobiłbym to:

Warstwa aplikacji:

public class TaskService
{
    //...

    public void Add(Guid categoryId, string description)
    {
        var category = _categoryRepository.GetById(categoryId);
        var status = _statusRepository.GetById(Constants.Status.OutstandingId);
        var task = Task.Create(category, status, description);
        _taskRepository.Save(task);
    }
}

Jednostka:

public class Task
{
    //...

    public static void Create(Category category, Status status, string description)
    {
        return new Task
        {
            Category = category,
            Status = status,
            Description = descrtiption
        };
    }
}

Robię to w ten sposób, ponieważ konsekwentnie mówi mi się, że podmioty nie powinny uzyskiwać dostępu do repozytoriów, ale byłoby to dla mnie znacznie rozsądniejsze, gdybym to zrobił:

Jednostka:

public class Task
{
    //...

    public static void Create(Category category, string description)
    {
        return new Task
        {
            Category = category,
            Status = _statusRepository.GetById(Constants.Status.OutstandingId),
            Description = descrtiption
        };
    }
}

Repozytorium statusu i tak jest wstrzykiwane zależnie, więc nie ma żadnej rzeczywistej zależności, a to wydaje mi się bardziej, że to domena decyduje, że zadanie domyślnie zalega. Poprzednia wersja wydawała się być warstwową aplikacją podejmującą tę decyzję. Jakieś powody, dla których umowy repozytorium są często w domenie, jeśli nie powinno to być możliwe?

Oto bardziej ekstremalny przykład, tutaj domena decyduje o pilności:

Jednostka:

public class Task
{
    //...

    public static void Create(Category category, string description)
    {
        var task = new Task
        {
            Category = category,
            Status = _statusRepository.GetById(Constants.Status.OutstandingId),
            Description = descrtiption
        };

        if(someCondition)
        {
            if(someValue > anotherValue)
            {
                task.Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.UrgentId);
            }
            else
            {
                task.Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.SemiUrgentId);
            }
        }
        else
        {
            task.Urgency = _urgencyRepository.GetById
                (Constants.Urgency.NotId);
        }

        return task;
    }
}

Nie ma możliwości, abyś przeszedł we wszystkie możliwe wersje Pilności, i nie ma możliwości obliczenia tej logiki biznesowej w warstwie aplikacji, więc na pewno byłby to najbardziej odpowiedni sposób?

Czy to jest ważny powód, aby uzyskać dostęp do repozytoriów z domeny?

EDYCJA: Może tak być również w przypadku metod niestatycznych:

public class Task
{
    //...

    public void Update(Category category, string description)
    {
        Category = category,
        Status = _statusRepository.GetById(Constants.Status.OutstandingId),
        Description = descrtiption

        if(someCondition)
        {
            if(someValue > anotherValue)
            {
                Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.UrgentId);
            }
            else
            {
                Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.SemiUrgentId);
            }
        }
        else
        {
            Urgency = _urgencyRepository.GetById
                (Constants.Urgency.NotId);
        }

        return task;
    }
}
Paul T. Davies
źródło

Odpowiedzi:

8

Mieszasz się

podmioty nie powinny uzyskiwać dostępu do repozytoriów

(co jest dobrą sugestią)

i

warstwa domeny nie powinna mieć dostępu do repozytoriów

(co może być złą sugestią, o ile repozytoria są częścią warstwy domeny, a nie warstwy aplikacji). W rzeczywistości twoje przykłady nie pokazują przypadku, w którym jednostka uzyskuje dostęp do repozytorium, ponieważ używasz metod statycznych , które nie należą do żadnej encji.

Jeśli nie chcesz umieszczać tej logiki tworzenia w statycznej metodzie klasy encji, możesz wprowadzić osobne klasy fabryki (jako część warstwy domeny!) I umieścić tam logikę tworzenia.

EDYCJA: do twojego Updateprzykładu: biorąc pod uwagę, że _urgencyRepositoryi statusRepository są członkami klasy Task, zdefiniowanymi jako pewnego rodzaju interfejs, musisz teraz wstrzyknąć je do dowolnej Taskencji, zanim będziesz mógł ich użyć Update(na przykład w Konstruktorze zadań). Możesz też zdefiniować je jako elementy statyczne, ale uwaga, która z łatwością może powodować problemy z wielowątkowością lub po prostu problemy, gdy potrzebujesz różnych repozytoriów dla różnych jednostek zadania w tym samym czasie.

Taka konstrukcja utrudnia nieco tworzenie Taskencji w oderwaniu, dlatego trudniej jest pisać testy jednostkowe dla Taskencji, trudniej jest pisać testy automatyczne w zależności od encji Task, a produkuje się trochę więcej pamięci, ponieważ każda encja Task musi teraz utrzymują, że dwa odniesienia do repozytoriów. Oczywiście może to być tolerowane w twoim przypadku. Z drugiej strony utworzenie oddzielnej klasy narzędziowej, TaskUpdaterktóra przechowuje odniesienia do odpowiednich repozytoriów, może być często lub przynajmniej lepszym rozwiązaniem.

Ważną częścią jest: TaskUpdaterpozostanie częścią warstwy domeny! To, że umieściłeś ten kod aktualizacji lub tworzenia w osobnej klasie, nie oznacza, że ​​musisz przełączyć się na inną warstwę.

Doktor Brown
źródło
Edytowałem, aby pokazać, że dotyczy to zarówno metod niestatycznych, jak i statycznych. Nigdy tak naprawdę nie myślałem, że metoda fabryczna nie jest częścią bytu.
Paul T Davies
@PaulTDavies: patrz moja edycja
Doc Brown,
Zgadzam się z tym, co tu mówisz, ale dodam zwięzły kawałek wskazujący Status = _statusRepository.GetById(Constants.Status.OutstandingId)na zasadę biznesową , którą można przeczytać jako „Firma dyktuje, że początkowy status wszystkich zadań będzie Znakomity” i dlatego ta linia kodu nie należy do repozytorium, którego jedynym przedmiotem jest zarządzanie danymi za pośrednictwem operacji CRUD.
Jimmy Hoffa
@ JimmyHoffa: hm, nikt tutaj nie sugerował umieszczenia tego rodzaju linii w jednej z klas repozytorium, ani OP, ani ja - więc o co ci chodzi?
Doc Brown,
Bardzo podoba mi się pomysł TaskUpdater jako usługi domowej. Wydaje się, że to trochę krówka, aby zachować zasady DDD, ale oznacza to, że mogę uniknąć wstrzykiwania repozytorium za każdym razem, gdy korzystam z Zadania.
Paul T Davies,
6

Nie wiem, czy twój przykład statusu to prawdziwy kod, czy tutaj tylko dla celów demonstracyjnych, ale wydaje mi się dziwne, że powinieneś wdrożyć Status jako Entity (nie wspominając o Agregacji Korzenia), gdy jego identyfikator jest stałą zdefiniowaną w kodzie - Constants.Status.OutstandingId. Czy nie jest to sprzeczne z celem „dynamicznych” statusów, które możesz dodać tyle, ile chcesz w bazie danych?

Dodam, że w twoim przypadku konstrukcja Task(w tym w razie potrzeby uzyskanie odpowiedniego statusu z StatusRepository) może zasługiwać na to TaskFactory, aby nie pozostawać w Tasksobie, ponieważ jest to niebanalny zbiór obiektów.

Ale :

Konsekwentnie mówi mi się, że podmioty nie powinny uzyskiwać dostępu do repozytoriów

To stwierdzenie jest w najlepszym razie nieprecyzyjne i zbyt uproszczone, w najgorszym przypadku wprowadzające w błąd i niebezpieczne.

W architekturach opartych na domenie jest powszechnie akceptowane, że jednostka nie powinna wiedzieć, jak się przechowywać - taka jest zasada niewiedzy uporczywości. Więc nie ma wywołań do jego repozytorium, aby dodać się do repozytorium. Czy powinien wiedzieć, jak (i ​​kiedy) przechowywać inne podmioty ? Ponownie, ta odpowiedzialność wydaje się należeć do innego obiektu - być może obiektu, który jest świadomy kontekstu wykonania i ogólnego postępu bieżącego przypadku użycia, takiego jak usługa warstwy aplikacji.

Czy jednostka może użyć repozytorium do pobrania innej jednostki ? 90% czasu nie powinno być konieczne, ponieważ jednostki, których potrzebuje, są zwykle w zakresie ich agregacji lub można je uzyskać przez przemieszczenie innych obiektów. Ale są chwile, kiedy tak nie jest. Jeśli na przykład przyjmujesz strukturę hierarchiczną, byty często muszą uzyskać dostęp do wszystkich swoich przodków, konkretnego wnuka itp. W ramach swojego wewnętrznego zachowania. Nie mają bezpośredniego odniesienia do tych odległych krewnych. Byłoby niewygodne przekazywanie im tych krewnych jako parametrów operacji. Dlaczego więc nie skorzystać z repozytorium, aby je zdobyć - pod warunkiem, że są one zagregowanymi źródłami?

Istnieje kilka innych przykładów. Chodzi o to, że czasami występuje zachowanie, którego nie można umieścić w usłudze domeny, ponieważ wydaje się idealnie pasować do istniejącej jednostki. A jednak ten byt musi uzyskać dostęp do repozytorium, aby nawodnić root lub zbiór korzeni, których nie można przekazać do niego.

Dostęp do repozytorium z jednostki nie jest sam w sobie zły , może przybierać różne formy, wynikające z różnych decyzji projektowych, od katastroficznych po akceptowalne.

guillaume31
źródło
Nie zgadzam się, że jednostka powinna użyć repozytorium, aby uzyskać dostęp do encji, z którą już ma relację - aby móc uzyskać dostęp do tej encji, należy przejść przez wykres obiektów. Korzystanie z repozytorium w ten sposób jest absolutnie nie nie. Mówię tutaj o tym, że jednostka nie ma jeszcze odniesienia, ale musi je utworzyć pod pewnymi warunkami biznesowymi.
Paul T Davies,
Cóż, jeśli dobrze mnie przeczytałeś, całkowicie się z tym zgadzamy ...
guillaume31,
2

To jeden z powodów, dla których nie używam Enums ani czystych tabel odnośników w mojej domenie. Pilność i status są stanami i istnieje logika związana ze stanem, który bezpośrednio należy do tego stanu (np. Jakie stany mogę przejść do danego stanu). Ponadto, rejestrując stan jako czystą wartość, tracisz informacje, takie jak czas trwania zadania w danym stanie. Reprezentuję statusy jako taka hierarchia klas. (W C #)

public class Interval
{
  public Interval(DateTime start, DateTime? end)
  {
    Start=start;
    End=end;
  }

  //To be called by internal framework
  protected Interval()
  {
  }

  public void End(DateTime? when=null)
  {
    if(when==null)
      when=DateTime.Now;
    End=when;
  }

  public DateTime Start{get;protected set;}

  public DateTime? End{get; protected set;}
}

public class TaskStatus
{
  protected TaskStatus()
  {
  }
  public Long Id {get;protected set;}

  public string Name {get; protected set;}

  public string Description {get; protected set;}

  public Interval Duration {get; protected set;}

  public virtual TNewStatus TransitionTo<TNewStatus>()
    where TNewStatus:TaskStatus
  {
    throw new NotImplementedException();
  }
}

public class OutStandingTaskStatus:TaskStatus
{
  protected OutStandingTaskStatus()
  {
  }

  public OutStandingTaskStatus(bool initialize)
  {
    Name="Oustanding";
    Description="For tasks that need to be addressed";
    Duration=new Interval(DateTime.Now,null);
  }

  public override TNewStatus TransitionTo<TNewStatus>()
  {
    if(typeof(TNewStatus)==typeof(CompletedTaskStatus))
    {
      var transitionDate=DateTime.Now();
      Duration.End(transitionDate);
      return new CompletedTaskStatus(true);
    }
    return base.TransitionTo<TNewStatus>();
  }
}

Implementacja CompletedTaskStatus byłaby prawie taka sama.

Należy tutaj zwrócić uwagę na kilka rzeczy:

  1. Zabezpieczam domyślnych konstruktorów. Jest to tak, że środowisko może je wywoływać podczas wyciągania obiektu z trwałości (zarówno EntityFramework Code-first, jak i NHibernate używają proxy, które są uzyskiwane z obiektów twojej domeny, aby wykonywać swoją magię).

  2. Wiele podmiotów ustawiających właściwości jest chronionych z tego samego powodu. Jeśli chcę zmienić datę końcową interwału, muszę wywołać funkcję Interval.End () (jest to część projektowania opartego na domenie, zapewniającego znaczące operacje zamiast anemicznych obiektów domenowych.

  3. Nie pokazuję go tutaj, ale Zadanie również ukryłoby szczegóły dotyczące tego, jak przechowuje swój obecny status. Zwykle mam chronioną listę Historycznych stanów, które pozwalam opinii publicznej zapytać, czy są zainteresowani. W przeciwnym razie udostępniam bieżący stan jako moduł pobierający, który wysyła zapytanie do HistoricalStates.Single (state.Duration.End == null).

  4. Funkcja TransitionTo jest znacząca, ponieważ może zawierać logikę, które stany są ważne dla przejścia. Jeśli masz tylko wyliczenie, ta logika musi leżeć gdzie indziej.

Mamy nadzieję, że pomoże to nieco lepiej zrozumieć podejście DDD.

Michael Brown
źródło
1
Byłoby to z pewnością prawidłowe podejście, jeśli różne stany zachowują się inaczej, jak w przykładzie z wzorcem stanu, i z pewnością rozwiązuje również omawiany problem. Trudno byłoby mi jednak uzasadnić klasę dla każdego stanu, gdyby miały tylko inne wartości, a nie inne zachowanie.
Paul T Davies,
1

Od jakiegoś czasu próbuję rozwiązać ten sam problem, zdecydowałem, że chcę móc wywołać Task.UpdateTask () w ten sposób, chociaż wolałbym, aby był specyficzny dla domeny, w twoim przypadku może nazwałbym to Task.ChangeCategory (...) aby wskazać akcję, a nie tylko CRUD.

tak czy inaczej, próbowałem twojego problemu i wymyśliłem to ... zjedz moje ciasto i też jem. Chodzi o to, że działania mają miejsce na bycie, ale bez zastrzyku wszystkich zależności. Zamiast tego praca odbywa się metodami statycznymi, aby mogły uzyskać dostęp do stanu encji. Fabryka składa wszystko razem i zwykle będzie miała wszystko, czego potrzebuje do wykonania pracy, którą jednostka musi wykonać. Kod klienta wygląda teraz na czysty i przejrzysty, a twoja jednostka nie jest zależna od żadnego wstrzyknięcia repozytorium.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UnitTestProject2
{
    public class ClientCode
    {
        public void Main()
        {
            TaskFactory factory = new TaskFactory();
            Task task = factory.Create();
            task.UpdateTask(new Category(), "some value");
        }

    }
    public class Category
    {
    }

    public class Task
    {
        public Action<Category, String> UpdateTask { get; set; }

        public static void UpdateTaskAction(Task task, Category category, string description)
        {
            // do the logic here, static can access private if needed
        }
    }

    public class TaskFactory
    {      
        public Task Create()
        {
            Task task = new Task();
            task.UpdateTask = (category, description) =>
                {
                    Task.UpdateTaskAction(task, category, description);
                };

            return task;
        }

    }
}
Mikrofon
źródło