بله. dump level از stack overflow جلوگیری میکند. اما جای دیگری است که stack overflow واقعی رخ میدهد:
«بررسی خطای Circular References در ASP.NET MVC Json Serialization»
دانش و مهارت زیادی لازم نیست تا یک برنامه نوشت. بچههای دبیرستانی نیز این کار را در دبیرستان انجام میدهند. مردان و زنان جوان در دانشگاه با سر هم کردن چند خط کد PHP یا Ruby کسب و کارهای میلیارد دلاری را شروع کرده اند. برنامه نویسان تازه کار زیادی در دفاتر کاری مکعبی شکلشان در سرتاسر دنیا در بین اسناد حجیم نیازمندیهای موجود در سیستمهای issue tracking خود در حال تقلا هستند تا سیستم هایشان را با صرف فعل خواستن توانستن است به کار بیندازند. کدهایی که تولید میکنند ممکن است زیبا نباشند ولی کار میکنند. کار میکند به این دلیل که چیزی بتواند یک بار کار کند، خیلی سخت هم نیست. این که چیزی واقعا به درستی کار کند موضوعی کاملا متفاوت است. پیاده سازی صحیح نرم افزار سخت است چرا که دانش و مهارت هایی نیاز دارد که هنوز بیشتر برنامه نویسان جوان به آن دست نیافته اند.
سیر تکاملی یک توسعه دهنده به یک طراح
شرایطی را در نظر بگیرید که نیاز است از تغییرات یک Entity در سیستم آگاه شویم. برای مثلا در زمان ثبت سفارش جدید در فروشگاه، ایمیلی به مدیر فروشگاه ارسال شود، یک Business Rule نیز چک شود و همچنین بنابر نیاز مشتری، تعداد آنها روز به روز ممکن است افزایش یابد و چه بسا در اعمال این Ruleها، موجودیتهای مختلفی درگیر باشند. در این صورت است که خواسته یا ناخواسته اتصال بین کلاسها خیلی افزایش خواهد یافت. یکی از راه حلهای رهایی از این پیچیدگی و اتصال بالا، استفاده از Event میباشد.
هدف طراحی و پیاده سازی زیرساختی برای استفاده از DomainEventها میباشد. کدهای کامل این مطلب را میتوانید از اینجا دریافت کنید.
Domain Event چیست؟
چیزی که در یک Domain خاصی رخ داده است و هدف از آن آگاه کردن سایر بخشهای آن Domain میباشد تا بتوانند واکنش مناسبی را نشان دهند. با بهره گیری از این نوع رویدادها، میتوان Separation Of Concerns خوبی را بین کلاسهای موجود در آن Domain اعمال کرد و به طراحی ای با Coupling پایین رسید. این رویدادها عموما داخل پروسه Raise میشوند.
namespace DomainEventsSample.Framework.Eventing.DomainEvents { public interface IDomainEvent : ITransientDependency { } }
namespace DomainEventsSample.Framework.Eventing.DomainEvents { public interface IDomainEventHandler<in T> : ITransientDependency where T : IDomainEvent { bool IsAdvisable { get; } void Handle(T domainEvent); } }
- متد Raise مربوط به Engine برای رویداد خاصی فراخوانی میشود.
- با استفاده از یک IOC Container، تمام هندلرهای مربوط به رویداد جمع آوری میشود.
- متد Handle مربوط به تک تک هندلرها، فراخوانی خواهد شد.
namespace DomainEventsSample.Framework.Eventing.DomainEvents { public interface IDomainEventEngine : ISingletonDependency { void Raise<T>(T domainEvent) where T : IDomainEvent; } } namespace DomainEventsSample.Framework.Eventing.DomainEvents { public class DomainEventEngine : IDomainEventEngine { private readonly IContainer _container; public DomainEventEngine(IContainer container) { _container = container; } public void Raise<T>(T domainEvent) where T : IDomainEvent { foreach (var handler in _container.GetAllInstances<IDomainEventHandler<T>>()) try { handler.Handle(domainEvent); } catch (Exception) { if (domainEvent.IsAdvisable && handler.IsAdvisable) throw; } } } }
namespace DomainEventsSample.Framework.Domain.Events { public abstract class EntityDomainEvent<TEntity> : IDomainEvent where TEntity : Entity { protected EntityDomainEvent(TEntity entity) { Entity = entity; } public TEntity Entity { get; } } }
کلاس بالا به عنوان کلاس پایه یکسری رویداد مشترک مابین Entityهای سیستم در نظر گرفته شده است.
namespace DomainEventsSample.Framework.Domain.Events { public class EntityCreatingEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityCreatingEvent(TEntity entity) : base(entity) { } } } namespace DomainEventsSample.Framework.Domain.Events { public class EntityCreatedEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityCreatedEvent(TEntity entity) : base(entity) { } } }
این رویدادها مربوط به زمان قبل و بعد از ایجاد یک Entity میباشند.
namespace DomainEventsSample.Framework.Domain.Events { public class EntityEditingEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityEditingEvent(TEntity entity) : base(entity) { } } } namespace DomainEventsSample.Framework.Domain.Events { public class EntityEditedEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityEditedEvent(TEntity entity) : base(entity) { } } }
این رویدادها مربوط به زمان قبل و بعد از ویرایش یک Entity میباشند.
namespace DomainEventsSample.Framework.Domain.Events { public class EntityDeletingEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityDeletingEvent(TEntity entity) : base(entity) { } } } namespace DomainEventsSample.Framework.Domain.Events { public class EntityDeletedEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityDeletedEvent(TEntity entity) : base(entity) { } } }
این رویدادها مربوط به زمان قبل و بعد از حذف یک Entity میباشند.
namespace DomainEventsSample.Framework.Domain.Events { public class EntitySavingEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntitySavingEvent(TEntity entity) : base(entity) { } } } namespace DomainEventsSample.Framework.Domain.Events { public class EntitySavedEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntitySavedEvent(TEntity entity) : base(entity) { } } }
این رویدادها مربوط به زمان قبل و بعد از ذخیره (ایجاد و ویرایش) یک Entity میباشند.
نکته: برای اسکن کردن تمام هندلرها لازم است کد زیر را به تنظیمات StructureMap اضافه کنید:
Scan(scan => { scan.ConnectImplementationsToTypesClosing(typeof(IDomainEventHandler<>)); });
public class ProductCreatedEventHandler : IDomainEventHandler<EntityCreatedEvent<Product>> { public bool IsAdvisable => false; public void Handle(EntityCreatedEvent<Product> domainEvent) { //todo: notify users } }
در متد Create مربوط به ProductApplicationService و بعد از عملیات ذخیره سازی به شکل زیر میبایست عمل کرد:
public class ProductApplicationService : IProductApplicationService { private readonly IDomainEventEngine _eventEngine; private readonly IUnitOfWork _unitOfWork; private readonly IMapper _mapper; public ProductApplicationService(IDomainEventEngine eventEngine,IMapper mapper,IUnitOfWork unitOfWork) { _eventEngine = eventEngine; _mapper=mapper; _unitOfWork=unitOfWork; } [Transactional] public void Create(ProductCreateViewModel model) { var entity=_mapper.Map<Product>(model); _unitOfWork.Set<Product>().Add(entity); _unitOfWork.SaveChanges(); _eventEngine.Raise(new EntityCreatedEvent<Product>(entity)); } }
البته بهتر است برای Raise کردن این نوع رویدادها از مکانیزم Hook استفاده کرد و در زمان ذخیره سازی و فراخوانی متد SaveChange، این عملیات به صورت خودکار صورت گیرند.
در مقاله بعدی با استفاده از Hookها این عملیات را انجام خواهیم داد.
کدهای این قسمت را میتوانید از اینجا دریافت کنید.
using System; using SimpleCqrs.Domain; namespace CqrsPattern.Cqrs.Command { public class Account : AggregateRoot { public Account(Guid id) { Apply(new AccountCreatedEvent { AggregateRootId = id }); } public void SetName(string firstName, string lastName) { Apply(new AccountNameSetEvent { FirstName = firstName, LastName = lastName }); } public void OnAccountCreated(AccountCreatedEvent evt) { Id = evt.AggregateRootId; } } }
using SimpleCqrs.Commanding; namespace CqrsPattern.Cqrs.Command { public class CreateAccountCommand : ICommand { public string FirstName { get; set; } public string LastName { get; set; } } }
using System; using SimpleCqrs.Commanding; using SimpleCqrs.Domain; namespace CqrsPattern.Cqrs.Command { public class CreateAccountCommandHandler : CommandHandler<CreateAccountCommand> { private readonly IDomainRepository repository; public CreateAccountCommandHandler(IDomainRepository repository) { this.repository = repository; } public override void Handle(CreateAccountCommand command) { var account = new Account(Guid.NewGuid()); account.SetName(command.FirstName, command.LastName); repository.Save(account); } } }
using SimpleCqrs.Eventing; namespace CqrsPattern.Cqrs.Command { public class AccountCreatedEvent : DomainEvent { } }
using SimpleCqrs.Eventing; namespace CqrsPattern.Cqrs.Command { public class AccountNameSetEvent : DomainEvent { public string FirstName { get; set; } public string LastName { get; set; } } }
using System.Linq; using SimpleCqrs.Eventing; using CqrsPattern.Cqrs.Db; namespace CqrsPattern.Cqrs.Command { public class AccountEventHandler : IHandleDomainEvents<AccountCreatedEvent>, IHandleDomainEvents<AccountNameSetEvent> { private readonly FakeAccountTable accountTable; public AccountEventHandler(FakeAccountTable accountTable) { this.accountTable = accountTable; } public void Handle(AccountCreatedEvent domainEvent) { accountTable.Add(new FakeAccountTableRow { Id = domainEvent.AggregateRootId }); } public void Handle(AccountNameSetEvent domainEvent) { var account = accountTable.Single(x => x.Id == domainEvent.AggregateRootId); account.Name = domainEvent.FirstName + " " + domainEvent.LastName; } } }
using System.Collections.Generic; namespace CqrsPattern.Cqrs.Db { public class FakeAccountTable : List<FakeAccountTableRow> { } }
using System; namespace CqrsPattern.Cqrs.Db { public class FakeAccountTableRow { public Guid Id { get; set; } public string Name { get; set; } } }
using SimpleCqrs; using SimpleCqrs.Unity; namespace CqrsPattern { public class SampleRunTime : SimpleCqrsRuntime<UnityServiceLocator> { } }
using System; namespace CqrsPattern.Cqrs.Query { public class AccountReadModel { public string Name { get; set; } public Guid Id { get; set; } } }
using CqrsPattern.Cqrs.Db; using System.Collections.Generic; using System.Linq; namespace CqrsPattern.Cqrs.Query { public class AccountReportReadService { private FakeAccountTable fakeAccountDb; public AccountReportReadService(FakeAccountTable fakeAccountDb) { this.fakeAccountDb = fakeAccountDb; } public IEnumerable<AccountReadModel> GetAccounts() { return from a in fakeAccountDb select new AccountReadModel { Id = a.Id, Name = a.Name }; } } }
using System; using SimpleCqrs.Commanding; using CqrsPattern.Cqrs.Query; using CqrsPattern.Cqrs.Command; namespace CqrsPattern { class Program { static void Main(string[] args) { var runtime = new SampleRunTime(); runtime.Start(); var fakeAccountTable = new FakeAccountTable(); runtime.ServiceLocator.Register(fakeAccountTable); runtime.ServiceLocator.Register(new AccountReportReadService(fakeAccountTable)); var commandBus = runtime.ServiceLocator.Resolve<ICommandBus>(); var cmd = new CreateAccountCommand { FirstName = "Ali", LastName = "Kh" }; commandBus.Send(cmd); var accountReportReadModel = runtime.ServiceLocator.Resolve<AccountReportReadService>(); Console.WriteLine("Accounts in database"); Console.WriteLine("####################"); foreach (var account in accountReportReadModel.GetAccounts()) { Console.WriteLine(" Id: {0} Name: {1}", account.Id, account.Name); } runtime.Shutdown(); Console.ReadLine(); } } }
public override void Handle(CreateAccountCommand command) { var account = new Account(Guid.NewGuid()); //line 1 account.SetName(command.FirstName, command.LastName); //line 2 repository.Save(account); //line 3 }
public Account(Guid id) { Apply(new AccountCreatedEvent { AggregateRootId = id }); }
public void SetName(string firstName, string lastName) { Apply(new AccountNameSetEvent { FirstName = firstName, LastName = lastName }); }
public void Handle(AccountCreatedEvent domainEvent) { accountTable.Add(new FakeAccountTableRow { Id = domainEvent.AggregateRootId }); } public void Handle(AccountNameSetEvent domainEvent) { var account = accountTable.Single(x => x.Id == domainEvent.AggregateRootId); account.Name = domainEvent.FirstName + " " + domainEvent.LastName; }
var accountReportReadModel = runtime.ServiceLocator.Resolve<AccountReportReadService>(); var accounts = accountReportReadModel.GetAccounts();