ฉันกำลังศึกษา 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 มาถึงจุดเดียวกันกับที่นี่ฉันมากับน้ำยานี้และทางออกที่ดีกว่า: