วิธีการนำแนวคิดของ DDD ไปใช้กับโค้ดจริง คำถามเฉพาะภายใน


9

ฉันกำลังศึกษา DDD อยู่และขณะนี้ฉันกำลังดิ้นรนเพื่อหาวิธีที่จะใช้แนวคิดในโค้ดจริง ฉันมีประสบการณ์เกี่ยวกับ N-Tier ประมาณ 10 ปีดังนั้นจึงเป็นไปได้อย่างมากว่าเหตุผลที่ฉันต้องดิ้นรนคือแบบจำลองทางจิตของฉันมีความสัมพันธ์กับการออกแบบนั้นมากเกินไป

ฉันสร้างแอปพลิเคชันเว็บ Asp.NET และฉันเริ่มต้นด้วยโดเมนง่ายๆ: แอปพลิเคชันตรวจสอบเว็บ ที่ต้องการ:

  • ผู้ใช้จะต้องสามารถลงทะเบียน Web App ใหม่เพื่อตรวจสอบ แอปพลิเคชันเว็บมีชื่อที่จดจำง่ายและชี้ไปที่ URL
  • แอปพลิเคชันบนเว็บจะทำการสำรวจสถานะเป็นระยะ ๆ (ออนไลน์ / ออฟไลน์)
  • แอปพลิเคชันเว็บจะสำรวจความคิดเห็นของเวอร์ชันปัจจุบันเป็นระยะ (คาดว่าแอพพลิเคชั่นเว็บจะมี "/version.html" ซึ่งเป็นไฟล์ที่ประกาศรุ่นของระบบในมาร์กอัพที่เฉพาะเจาะจง)

ข้อสงสัยของฉันส่วนใหญ่เกี่ยวกับการแบ่งหน้าที่ความรับผิดชอบการหาสถานที่ที่เหมาะสมสำหรับแต่ละสิ่ง (การตรวจสอบความถูกต้องกฎเกณฑ์ทางธุรกิจ ฯลฯ ) ด้านล่างฉันเขียนโค้ดและเพิ่มความคิดเห็นพร้อมคำถามและข้อควรพิจารณา

กรุณาวิพากษ์วิจารณ์และให้คำแนะนำ ขอบคุณล่วงหน้า!


MODEL DOMAIN

สร้างแบบจำลองเพื่อแค็ปซูลกฎธุรกิจทั้งหมด

// 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 เพื่อจัดการสร้างและลบซึ่งฉันเชื่อว่าไม่ใช่ความกังวลของการรวมตัวเอง

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);
    }
}

การปิดเรื่อง

หลังจากรวบรวมคำตอบที่นี่และในคำถามอื่นที่ฉันเปิดด้วยเหตุผลอื่น แต่ ultimatelly มาถึงจุดเดียวกันกับที่นี่ฉันมากับน้ำยานี้และทางออกที่ดีกว่า:

ข้อเสนอการแก้ปัญหาใน Github สรุปสาระสำคัญ


ฉันอ่านมาเยอะ แต่ฉันไม่ได้พบตัวอย่างที่ใช้งานได้จริงยกเว้นว่าจะใช้ CQRS และรูปแบบมุมฉากและแนวปฏิบัติอื่น ๆ แต่ตอนนี้ฉันกำลังมองหาสิ่งง่าย ๆ นี้อยู่
Levidad

1
คำถามนี้อาจเหมาะสมกว่าสำหรับ codereview.stackexchange.com
VoiceOfUnreason

2
ฉันเองชอบคุณมากที่ใช้เวลากับแอพ n-tier ฉันรู้เกี่ยวกับ DDD เท่านั้นจากหนังสือฟอรัมและอื่น ๆ ดังนั้นฉันจะโพสต์เฉพาะความคิดเห็นเท่านั้น การตรวจสอบมีสองประเภท: การตรวจสอบความถูกต้องของอินพุตและการตรวจสอบกฎธุรกิจ การตรวจสอบความถูกต้องของอินพุตจะอยู่ในเลเยอร์แอปพลิเคชันและการตรวจสอบความถูกต้องของโดเมนจะไปในเลเยอร์ของโดเมน WebApp ดูเหมือนเอนทิตีมากกว่า aggreagate และ WebAppService จะดูเหมือนบริการแอปพลิเคชั่นมากกว่า DomainService นอกจากนี้การรวมของคุณอ้างอิงคอนเทนเนอร์ซึ่งเป็นข้อกังวลของโครงสร้างพื้นฐาน นอกจากนี้ยังดูเหมือนบริการค้นหา
Adrian Iftode

1
ใช่เพราะมันไม่ได้สร้างความสัมพันธ์ มวลรวมกำลังสร้างแบบจำลองความสัมพันธ์ระหว่างวัตถุโดเมน WebApp มีเพียงข้อมูลดิบและพฤติกรรมบางอย่างและอาจจัดการตัวอย่างกับค่าคงที่ต่อไปนี้: ไม่เป็นไรที่จะอัปเดตเวอร์ชันอย่างบ้าคลั่งเช่นการก้าวไปสู่รุ่น 3 เมื่อรุ่นปัจจุบันคือ 1
Adrian Iftode

1
ตราบใดที่ ValueObject มีวิธีการที่ใช้ความเท่าเทียมกันระหว่างอินสแตนซ์ฉันก็ถือว่าใช้ได้ ในสถานการณ์ของคุณคุณสามารถสร้างวัตถุค่ารุ่น ตรวจสอบการกำหนดเวอร์ชันแบบ semantic คุณจะได้รับแนวคิดมากมายเกี่ยวกับวิธีที่คุณสามารถจำลองวัตถุค่านี้รวมถึงค่าคงที่และพฤติกรรม WebApp ไม่ควรพูดคุยกับที่เก็บข้อมูลจริง ๆ แล้วฉันเชื่อว่าปลอดภัยที่จะไม่มีการอ้างอิงใด ๆ จากโครงการของคุณที่มีเนื้อหาโดเมนกับสิ่งอื่นที่เกี่ยวข้องกับโครงสร้างพื้นฐาน (ที่เก็บหน่วยงาน) ทั้งทางตรงและทางอ้อม (ผ่านอินเทอร์เฟซ)
Adrian Iftode

คำตอบ:


1

บรรทัดคำแนะนำที่มียาวเกี่ยวกับการWebAppรวมของคุณฉันเห็นด้วยอย่างเต็มที่ว่าการดึงในrepositoryนั้นไม่ใช่วิธีการที่เหมาะสมที่นี่ จากประสบการณ์ของฉันผู้รวมจะตัดสินใจ 'ว่า' การกระทำไม่เป็นไรหรือไม่ขึ้นอยู่กับสถานะของมันเอง ดังนั้นจึงไม่ได้อยู่ในสถานะที่อาจดึงจากบริการอื่น ๆ หากคุณต้องการเช็คดังกล่าวโดยทั่วไปฉันจะย้ายไปที่บริการที่เรียกว่าการรวม (ในตัวอย่างของคุณWebAppService)

นอกจากนี้คุณอาจเข้าสู่กรณีการใช้งานที่หลาย ๆ แอพพลิเคชั่นต้องการโทรหากลุ่มของคุณพร้อมกัน หากสิ่งนี้เกิดขึ้นในขณะที่คุณกำลังโทรออกเช่นนี้ซึ่งอาจใช้เวลานานคุณจึงปิดกั้นการรวมสำหรับการใช้งานอื่น ๆ ในที่สุดจะทำให้การจัดการโดยรวมช้าลงซึ่งเป็นสิ่งที่ฉันคิดว่าไม่เป็นที่ต้องการ

WebAppServiceดังนั้นแม้ว่ามันอาจจะดูเหมือนว่ารวมของคุณจะกลายเป็นบางมากถ้าคุณย้ายบิตของการตรวจสอบนั้นผมไม่คิดว่ามันจะดีกว่าที่จะย้ายไปยัง

ฉันขอแนะนำให้ย้ายการเผยแพร่WebAppRegisteredกิจกรรมไปยังกลุ่มของคุณด้วย การรวมเป็นผู้ชายที่ถูกสร้างขึ้นดังนั้นหากกระบวนการสร้างประสบความสำเร็จมันก็สมเหตุสมผลที่จะปล่อยให้มันเผยแพร่ความรู้สู่โลก

หวังว่านี่จะช่วยคุณได้ @Levidad!


สวัสดีสตีเว่นขอบคุณสำหรับข้อมูลของคุณ ฉันได้เปิดคำถามอื่นที่นี่ที่ ultimatelly มาถึงจุดเดียวกันของคำถามนี้และในที่สุดฉันก็มากับความพยายามน้ำยาทำความสะอาดสำหรับปัญหานี้ คุณช่วยดูและแบ่งปันความคิดของคุณได้ไหม ฉันคิดว่ามันเป็นไปตามคำแนะนำของคุณด้านบน
Levidad

แน่นอนว่า Levidad ฉันจะได้ดู!
สตีเวน

1
ฉันเพิ่งตรวจสอบคำตอบทั้งสองจาก 'เสียงที่ไร้เหตุผล' และ 'Erik Eidt' ทั้งสองอยู่ในแนวของสิ่งที่ฉันจะแสดงความคิดเห็นในคำถามที่คุณได้รับดังนั้นฉันจึงไม่สามารถเพิ่มคุณค่าที่นั่นจริงๆ และเพื่อตอบคำถามของคุณ: วิธีที่คุณสร้างWebAppAR ใน 'Cleaner Solution' ที่คุณแบ่งปันนั้นเป็นไปตามสิ่งที่ฉันคิดว่าเป็นแนวทางที่ดีสำหรับการรวม หวังว่านี่จะช่วยคุณในเรื่อง Levidad!
Steven
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.