Як застосувати деякі поняття DDD до фактичного коду? Конкретні питання всередині


9

Я вивчав DDD, і в даний час намагаюся знайти спосіб застосування понять у фактичному коді. Я маю близько 10-річного досвіду роботи з N-рівнем, тому дуже ймовірно, що причина, з якою я боюсь, полягає в тому, що моя ментальна модель занадто поєднана з цим дизайном.

Я створив веб-додаток Asp.NET і починаю з простого домену: веб-програми для моніторингу. Вимоги:

  • Користувач повинен мати можливість зареєструвати нову веб-програму для моніторингу. Веб-додаток має дружнє ім’я та вказує на URL-адресу;
  • Веб-додаток періодично опитуватиме статус (онлайн / офлайн);
  • Веб-додаток періодично проводитиме опитування щодо його поточної версії (очікується, що веб-додаток матиме "/version.html", який є файлом, який оголошує його системну версію в певній розмітці).

Мої сумніви стосуються головним чином розподілу обов'язків, пошуку належного місця для кожної речі (перевірка, ділове правило тощо). Нижче я написав деякий код та додав коментарі із запитаннями та міркуваннями.

Будь ласка, критикуйте та дайте поради . Спасибі заздалегідь!


ДОМЕЙНА МОДЕЛЬ

Моделюється для інкапсулювання всіх правил бізнесу.

// Encapsulates logic for creating and validating Url's.
// Based on "Unbreakable Domain Models", YouTube talk from Mathias Verraes
// See https://youtu.be/ZJ63ltuwMaE
public class Url: ValueObject
{
    private System.Uri _uri;

    public string Url => _uri.ToString();

    public Url(string url)
    {
        _uri = new Uri(url, UriKind.Absolute); // Fails for a malformed URL.
    }
}

// Base class for all Aggregates (root or not).
public abstract class Aggregate
{
    public Guid Id { get; protected set; } = Guid.NewGuid();
    public DateTime CreatedAt { get; protected set; } = DateTime.UtcNow;
}

public class WebApp: Aggregate
{
    public string Name { get; private set; }
    public Url Url { get; private set; }
    public string Version { get; private set; }
    public DateTime? VersionLatestCheck { get; private set; }
    public bool IsAlive { get; private set; }
    public DateTime? IsAliveLatestCheck { get; private set; }

    public WebApp(Guid id, string name, Url url)
    {
        if (/* some business validation fails */)
            throw new InvalidWebAppException(); // Custom exception.

        Id = id;
        Name = name;
        Url = url;
    }

    public void UpdateVersion()
    {
        // Delegates the plumbing of HTTP requests and markup-parsing to infrastructure.
        var versionChecker = Container.Get<IVersionChecker>();
        var version = versionChecker.GetCurrentVersion(this.Url);

        if (version != this.Version)
        {
            var evt = new WebAppVersionUpdated(
                this.Id, 
                this.Name, 
                this.Version /* old version */, 
                version /* new version */);
            this.Version = version;
            this.VersionLatestCheck = DateTime.UtcNow;

            // Now this eems very, very wrong!
            var repository = Container.Get<IWebAppRepository>();
            var updateResult = repository.Update(this);
            if (!updateResult.OK) throw new Exception(updateResult.Errors.ToString());

            _eventDispatcher.Publish(evt);
        }

        /*
         * I feel that the aggregate should be responsible for checking and updating its
         * version, but it seems very wrong to access a Global Container and create the
         * necessary instances this way. Dependency injection should occur via the
         * constructor, and making the aggregate depend on infrastructure also seems wrong.
         * 
         * But if I move such methods to WebAppService, I'm making the aggregate
         * anaemic; It will become just a simple bag of getters and setters.
         *
         * Please advise.
         */
    }

    public void UpdateIsAlive()
    {
        // Code very similar to UpdateVersion().
    }
}

І клас DomainService для обробки Creates and Deletes, що, на мою думку, не є проблемою самого агрегату.

public class WebAppService
{
    private readonly IWebAppRepository _repository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IEventDispatcher _eventDispatcher;

    public WebAppService(
        IWebAppRepository repository, 
        IUnitOfWork unitOfWork, 
        IEventDispatcher eventDispatcher
    ) {
        _repository = repository;
        _unitOfWork = unitOfWork;
        _eventDispatcher = eventDispatcher;
    }

    public OperationResult RegisterWebApp(NewWebAppDto newWebApp)
    {
        var webApp = new WebApp(newWebApp);

        var addResult = _repository.Add(webApp);
        if (!addResult.OK) return addResult.Errors;

        var commitResult = _unitOfWork.Commit();
        if (!commitResult.OK) return commitResult.Errors;

        _eventDispatcher.Publish(new WebAppRegistered(webApp.Id, webApp.Name, webApp.Url);
        return OperationResult.Success;
    }

    public OperationResult RemoveWebApp(Guid webAppId)
    {
        var removeResult = _repository.Remove(webAppId);
        if (!removeResult) return removeResult.Errors;

        _eventDispatcher.Publish(new WebAppRemoved(webAppId);
        return OperationResult.Success;
    }
}

ЗАЯВА ЗАЯВКА

Клас нижче надає інтерфейс для домену WebMonitoring до зовнішнього світу (веб-інтерфейси, відпочинок api та ін.). В даний момент це просто оболонка, яка перенаправляє дзвінки до відповідних служб, але в майбутньому вона зростатиме, щоб впорядкувати більше логіки (це завжди здійснюється за допомогою доменних моделей).

public class WebMonitoringAppService
{
    private readonly IWebAppQueries _webAppQueries;
    private readonly WebAppService _webAppService;

    /*
     * I'm not exactly reaching for CQRS here, but I like the idea of having a
     * separate class for handling queries right from the beginning, since it will
     * help me fine-tune them as needed, and always keep a clean separation between
     * crud-like queries (needed for domain business rules) and the ones for serving
     * the outside-world.
     */

    public WebMonitoringAppService(
        IWebAppQueries webAppQueries, 
        WebAppService webAppService
    ) {
        _webAppQueries = webAppQueries;
        _webAppService = webAppService;
    }

    public WebAppDetailsDto GetDetails(Guid webAppId)
    {
        return _webAppQueries.GetDetails(webAppId);
    }

    public List<WebAppDetailsDto> ListWebApps()
    {
        return _webAppQueries.ListWebApps(webAppId);
    }

    public OperationResult RegisterWebApp(NewWebAppDto newWebApp)
    {
        return _webAppService.RegisterWebApp(newWebApp);
    }

    public OperationResult RemoveWebApp(Guid webAppId)
    {
        return _webAppService.RemoveWebApp(newWebApp);
    }
}

Закриття питань

Зібравши відповіді тут і в цьому іншому запитанні , яке я відкрив з іншої причини, але в кінцевому рахунку дійшов до тієї ж точки, що і цей, я придумав це більш чисте і краще рішення:

Пропозиція рішення в Github Gist


Я багато читав, але не знайшов таких практичних прикладів, за винятком тих, що застосовують CQRS та інші ортогональні схеми та практики, але я зараз шукаю цю просту річ.
Левідад

1
Це питання може бути краще підходить для codereview.stackexchange.com
VoiceOfUnreason

2
Я сам, як ти, багато часу проводив із n-ярусними програмами. Я знаю про DDD лише з книг, форумів тощо, тому опублікую лише коментар. Існує два типи перевірки: перевірка вводу та перевірка правил бізнесу. Перевірка вводу проходить у рівні програми, а перевірка домену - у рівні домену. WebApp більше схожий на Entity, а не на агрегат, а WebAppService більше схожий на службу додатків, ніж на DomainService. Також ваш агрегат посилається на контейнер, який є об'єктом інфраструктури. Це також схоже на локатор обслуговування.
Адріан Іфтоде

1
Так, тому що це не моделює відношення. Сукупності моделюють відносини між об'єктами домену. WebApp має лише необроблені дані та певну поведінку, і може мати справу, наприклад, з наступним інваріантом: не нормально оновлювати такі версії, як божевільний, тобто перехід на версію 3, коли поточна версія 1.
Адріан Іфтоде

1
Поки ValueObject має метод, який реалізує рівність між примірниками, я думаю, це нормально. У вашому сценарії ви можете створити об'єкт значення Version. Перевірте семантичну версію, ви отримаєте масу ідей про те, як можна моделювати цей ціннісний об’єкт, включаючи інваріанти та поведінку. WebApp не повинен спілкуватися з сховищем, я вважаю, що безпечно не мати жодних посилань у вашому проекті, що містить дані про домен, на що-небудь інше, пов'язане з інфраструктурою (сховищами, одиницею роботи), прямо чи опосередковано (через інтерфейси).
Адріан Іфтоде

Відповіді:


1

Довгий ряд порад щодо вашої WebAppсукупності, я повністю погоджуюся з тим, що залучення до цього repositoryне є правильним підходом. На мій досвід, Агрегат прийме «рішення», чи буде дія нормальною чи не заснованою на власній державі. Таким чином, не в штаті, це може витягнути з інших служб. Якщо вам знадобиться така перевірка, я б взагалі перемістив її до служби, яка викликає сукупність (у вашому прикладі WebAppService).

Крім того, ви можете скористатися випадком використання, який кілька додатків хочуть одночасно викликати ваш агрегат. Якщо це станеться, хоча ви робите вихідні дзвінки, такі як це може зайняти багато часу, ви тим самим блокуєте свій агрегат для інших звичаїв. Це врешті-решт уповільнить обробку агрегатів, що, на мою думку, теж не бажане.

Тому, хоча може здатися, що ваш агрегат стає досить тонким, якщо ви перемістите цей біт валідації, я думаю, що краще перемістити його на WebAppService.

Я б також запропонував перенести публікацію WebAppRegisteredподії до вашої сукупності. Сукупність - це хлопець, який створюється, тому, якщо процес його створення буде успішним, є сенс дозволити йому опублікувати цю нотатку у світі.

Сподіваюсь, це допоможе вам @Levidad!


Привіт Стівен, дякую за ваш внесок. Тут я відкрив ще одне запитання, яке в кінцевому підсумку дійшло до тієї ж точки цього питання, і я нарешті зіткнувся з спробою Cleaner Solution для цієї проблеми. Чи не хотіли б ви поглянути і поділитися своїми думками? Я думаю, що це йде в напрямку ваших пропозицій вище.
Левідад

Звичайно, Левідад, я подивлюся!
Стівен

1
Я просто перевірив обидві відповіді - з "Голосу нерозуміння" та "Еріка Еддта". Обидва вони узгоджуються з тим, що я б прокоментував питання, яке у вас є, тому я не можу дійсно додати цінності. І, щоб відповісти на ваше запитання: те, як ви WebAppналаштовані на AR, у розділі «Очищення чистоти», яким ви ділитесь, насправді відповідає принципам того, що я вважаю б хорошим підходом до агрегату. Сподіваюсь, це допоможе тобі в Левідаді!
Стівен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.