业务建模:BoundedContext(有界上下文)

有界上下文BoundedContext在DDD是一个拓展的概念,在Evans经典DDD的书里应该是没有这个概念的,Jdon论坛上面的很少涉及BoundedContext文章。
就BoundedContext的设计上,提出几个自己的在这方面看法,抛砖引玉。
1、BoundedContext不同于Role(DCI)
它们看起来有点相似,但BoundedContext的描述语意会更加广泛一点。Role(DCI)根据角色直接捕捉角色与系统交互行为,更接近于业务需求,BoundedContext更强调则业务分析避免歧义.
比如,Security模块的User,Hr模块的Employee,Project模块的Member等,当涉及“系统使用者”的时候,系统管理员会认为说的是系统用户,行政总监会认为说的是员工,项目经理会说的是项目成员,这时候Role与BoundedContext就很类似了,除了类似Role这样的概念,BoundedContext还可以其他业务概念的歧义问题,如“Account”他可能是财务的概念也可能是其他业务的概念。

2、BoundedContext与Repository
领域内的Entity都应该置于一定的上下文语境下面,而Entity的重建在Repository内完成的,所以Repository对BoundedContext存在一定的依赖关系

3、BoundedContext与Domain Service
业务之间或业务模块之间存在一定的交互关系,而这种交互关系最终体现为BoundedContext之间的交互关系,
业务的交互可以领域服务(Domain Service)来实现。

一个系统,当从数据的角度考察的时候,表现为一系列查询和命令,当从业务的角度考察的时候,表现为一系列业务行为,
这个视角的不同最终体现为不同的Context之间的差异,其交互通过QueryService、CommandService、UnitOfWorkService来实现。

4、BoundedContext可做为CommandHandler与EventHandler的实现。


架构CQRS有太多概念很容易模糊掉关注领域核心的视线,所以才会把很多架构里元素放到BoundedContext中。
[该贴被clonalman于2012-10-11 23:33修改过]

BoundedContext的交互通过Domain Service来实现,BoundedContext只需要承载一个相关的Ioc容器,
Ioc Container 实现(C#):


public interface IDependencyInjectionContainer: IServiceProvider
{
void Register<TService>(string key, Func<TService> factory);
void Register<TService, TResult>(string key, Func<TService, TResult> factory);
void Register<TService>(Func<TService> factory);
void Register<TService>(Func<IDependencyInjectionContainer, TService> factory);
void Unregister(object instance);
TService Resolve<TService>();
TService Resolve<TService>(IDictionary arguments);
TService Resolve<TService>(string key);
TService Resolve<TService>(string key, IDictionary arguments);
TService[] ResolveAll<TService>();
}



internal class DependencyInjectionContainer : IDependencyInjectionContainer, IServiceProvider
{
private readonly IDictionary<string, object> factories = new Dictionary<string, object>();

public void Register<TService>(Func<IDependencyInjectionContainer, TService> factory)
{
Register(() => factory(this));
}

public void Register<TService>(Func<TService> factory)
{
Register(typeof(TService).AssemblyQualifiedName, factory);
}

public void Register<TService, TResult>(string key, Func<TService, TResult> factory)
{
Register(key, () => factory(Resolve<TService>()));
}

public void Register<TService>(string key, Func<TService> factory)
{
factories[key] = factory;
}

public void Unregister(object instance)
{
if (factories.ContainsKey(instance.GetType().AssemblyQualifiedName))
{
factories.Remove(instance.GetType().AssemblyQualifiedName);
}
else
{
IDependencyInjectionContainer container = Resolve<IDependencyInjectionContainer>();
if (container != null)
{
container.Unregister(instance);
}
}
}

public TService Resolve<TService>()
{
object obj;
if (factories.TryGetValue(typeof(TService).AssemblyQualifiedName, out obj))
{
return ((Func<TService>)obj)();
}
else
{
IDependencyInjectionContainer container = Resolve<IDependencyInjectionContainer>();
if (container != null)
{
return container.Resolve<TService>();
}
}
return default(TService);
}

public TService Resolve<TService>(IDictionary arguments)
{
object obj;
if (factories.TryGetValue(typeof(TService).AssemblyQualifiedName, out obj))
{
return ((Func<TService>)obj)();
}
else
{
IDependencyInjectionContainer container = Resolve<IDependencyInjectionContainer>();
if (container != null)
{
return container.Resolve<TService>(arguments);
}
}
return default(TService);
}

public TService Resolve<TService>(string key)
{
object obj;
if (factories.TryGetValue(key, out obj))
{
return ((Func<TService>)obj)();
}
else
{
IDependencyInjectionContainer container = Resolve<IDependencyInjectionContainer>();
if (container != null)
{
return container.Resolve<TService>(key);
}
}
return default(TService);
}

public TService Resolve<TService>(string key, IDictionary arguments)
{
object obj;
if (factories.TryGetValue(key, out obj))
{
return ((Func<TService>)obj)();
}
else
{
IDependencyInjectionContainer container = Resolve<IDependencyInjectionContainer>();
if (container != null)
{
return container.Resolve<TService>(key, arguments);
}
}
return default(TService);
}

public TService[] ResolveAll<TService>()
{
List<TService> services = new List<TService>();
foreach (Func<TService> obj in factories.Values.OfType<Func<TService>>())
{
services.Add(obj());
}
IDependencyInjectionContainer container = Resolve<IDependencyInjectionContainer>();
if (container != null)
{
services.AddRange(container.ResolveAll<TService>());
}
return services.ToArray();
}

public object GetService(Type serviceType)
{
object obj;
if (factories.TryGetValue(serviceType.AssemblyQualifiedName, out obj))
{
Delegate deleg = (Delegate)obj;
if (deleg != null)
{
return deleg.DynamicInvoke();
}
}
else
{
IDependencyInjectionContainer container = Resolve<IDependencyInjectionContainer>();
if (container != null)
{
return container.GetService(serviceType);
}
}
return null;
}

}



public static class DI
{
static DI()
{
Current = new DependencyInjectionContainer();
RegisterCoreDependencies(Current);
}

public static IDependencyInjectionContainer Current
{
[MethodImpl(MethodImplOptions.Synchronized)]
get;
private set;
}

private static void RegisterCoreDependencies(IDependencyInjectionContainer di)
{

}
}

除了具备简单容器功能,主要加载第三方容器如Spring或Windsor等

不同上下文(BoundedContext)
1、通用上下文BoundedContext,适用于任何情况,具体业务场景使用的基类
C#


public class BoundedContext : MarshalByRefObject, IDisposable
{
protected BoundedContext(IDependencyInjectionContainer container)
{
Guard.NotNullArgument(container, "container");
Container = container;
}

public BoundedContext() :
this(DI.Current)
{

}

public IDependencyInjectionContainer Container
{
get;
private set;
}
.....
}

2、系统只是一个CRUD系统或CQS系统的数据上下文,从通用上下文继承。
C代码


public sealed class NHibernateContext : BoundedContext, IDisposable
{
IWindsorContainer m_Container;

public NHibernateContext(IWindsorContainer container)
: base(container.Resolve<IDependencyInjectionContainer>())
{
m_Container = container;
}
....
}


public sealed class NHibernateQueryService : IQueryService
{
public NHibernateQueryService(NHibernateContext context)
{
this.Context = context;
}

public NHibernateContext Context
{
get;
private set;
}
....
}


public sealed class NHibernateCommandService: ICommandService
{
public NHibernateCommandService(NHibernateContext context)
{
this.Context = context;
}

public NHibernateContext Context
{
get;
private set;
}

void ICommandService.Execute(ICommand command)
{
command.Execute(Context);
}
}

NHibernateContext, NHibernateCommandService, NHibernateQueryService 均在容器中

3、业务场景上下文:如InventoryContext


public class InventoryContext: BoundedContext
{
public InventoryContext(){

}
......
}

Container属性可以轻松获取ICommandService与IQueryService,实现与NHibernateContext的交互;InventoryContext与NHibernateContext的交互主要实现数据的查询与持久化等功能

另外InventoryContext还可以于其他的业务系统的Context进行交互,由业务上的服务来实现。。



[该贴被clonalman于2012-10-12 08:59修改过]


我这些实现可能跟现有的很多设计思路很不一样,主要是试图对上面这个图
进行解构,让非领域的技术架构概念隐藏在DDD领域概念下,或者说是让架构的概念实现或演变为领域概念,以实现架构与领域的分离。

BoundedContext、IQueryService、ICommandService放在基础设施层,应用层有复杂的查询,可以直接越过领域层,直接访问基础设施层的IQueryService,领域层数据的访问不直接调用IQueryService、ICommandService,而是通过Repository进行封装,具体体现为领域对象的重建。


public class RepositoryBase : IDisposable
{

public RepositoryBase() :
this(new BoundedContext())
{

}

protected RepositoryBase(BoundedContext context)
{
Guard.NotNullArgument(context, "context");
Context = context;
}

protected BoundedContext Context
{
get;
private set;
}

protected virtual void Dispose(bool disposing)
{

}

void IDisposable.Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

~RepositoryBase()
{
Dispose(false);
}
}

[该贴被clonalman于2012-10-12 09:04修改过]
[该贴被admin于2012-10-12 14:45修改过]
[该贴被admin于2012-10-12 14:46修改过]

系统各Context之间的相互交互关系,构成了系统Context Map,Command可以沿这Context Map在不同Context之间传递
比如:添加一个产品Product,提交的AddCommand从ProductContext接受处理转移到NHibernateContext来持久化处理。


/// <summary>
/// The Command Pattern
/// </summary>
public abstract class Command : ICommand, IDisposable
{
public Command()
{

}

public void Execute(BoundedContext context)
{
try
{
OnExecute(context);
}
catch (Exception ex)
{
Exception = ex;
throw ex;
}

}

public Exception Exception
{
get;
set;
}

protected abstract void OnExecute(BoundedContext context);

protected virtual void Dispose(bool disposing)
{

}

public void Dispose()
{
Dispose(true);
}

~Command()
{
Dispose(false);
}
}

[该贴被clonalman于2012-10-12 10:55修改过]
[该贴被clonalman于2012-10-12 11:22修改过]

领域内的事件 Domain Event ,首先被传递其所属BoundedContext上,
再由所属BoundedContext与其他模块的BoundedContext进行交互。
而是事件的触发是在Entity上的,
简单伪代码如下:


public EntityAContext: BoundedContext
{
public void Handle(DomainEvent event)
{
//进行事件处理或通过Service与其他Context交互
}
}

public class EntityA
{
public event Handler<DomainEvent> OnEvent;

public void DoSomething()
{
if(OnEvent != null)
{
OnEvent(new DomainEvent())
}
}
}

public class EntityARepository
{
public EntityARepository(EntityAContextcontext)
: base(context)
{

}

public EntityA Find(int id)
{
IQueryService serive = Context.Container.Resolve<IQueryService>();
if(serive != null){
EntityA a = serive.Find<EntityA>(id);
EntityAContext context = Context as EntityAContext;
a.OnEvent += context.Handle;
return a;
}
throw Error.ServiceNoFound;
}
}

class EntityDoAService
{
void DoSomething()
{
using(EntityAContext context = new EntityAContext())
{
using(EntityARepository repository = new EntityARepository(context))
{
EntityA a = repository.Find(1);
a.DoSomething();
}
}
}
}

业务建模:CQRS应用场景的讨论中就一直思考将它做为架构与领域的结合点。
BoundedContext在网上的资料基本都是停留在概念层面上的东西,很少实现层面的东西,这只是初步的想法,先写到这里,欢迎道友们积极讨论,共同来完善。。。
[该贴被clonalman于2012-10-12 11:37修改过]
[该贴被clonalman于2012-10-12 16:24修改过]

2012-10-12 11:10 "@clonalman"的内容
BoundedContext在网上的资料基本都是停留在概念层面上的东西,很少实现层面的东西 ...

界限上下文在DDD中也只是一种隐含的背景,是领域模型运行活动的环境,缺省我们是认为这个上下文是在服务中,因为服务是相对客户端而言,这里也隐式指定了一种客户应用场景,如下图:所以,上下文本身可能带有具体客户端应用痕迹。

Client -- > Service --- > Bounded Context --- > Domain model。

这里带来一个问题,强调隐式的有界上下文可能会强调模型的个性,而忽视其通用性,在DDD中有界上下文我认为是用来解决模型的个性问题,但它是一把双刃剑,用不好就个性超过通用性了。

先抛出以上观点,如果同意,下面可讨论如何避免模型的过分个性化,实际就是去服务化,离开具体客户端越远,受其定制性就差,这要从架构和业务两个方面入手,业务上入手就是将有界上下文和业务用例场景绑定,两者如果等同起来就更好。

2012-10-12 15:30 "@banq"的内容
先抛出以上观点,如果同意,下面可讨论如何避免模型的过分个性化,实际就是去服务化,离开具体客户端越远,受其定制性就差,这要从架构和业务两个方面入手,业务上入手就是将有界上下文和业务用例场景绑定,两者如果等同起来就更好。 ...

并不是并有界上下文上来取代服务,他们之间是一个交替的关系。
个性化模型的表达更多是通过BoundedContext带的个性化属性来体现的,所有我觉得需要显式的去表达
外界对领域的操作命令和领域内对外界发出的事件都经过这个上下问来进行处理,才有领域边界的概念。
这样有界上下文内的就是领域元素,有界上下文外的就是架构元素。

原模型的基础上加一个Bounded Context ---> Service的关系:

.................|----------------
................ V...................|
Client -- > Service --- > Bounded Context --- > Domain model。

有界上下文和业务用例场景是否等同,取决于业务;
如果需求分析的业务用例场景,在进行设计时不需要进一步抽象,二者可能是一样的;


class EntityADoService
{
void DoSomething()
{
using(EntityAContext context = new EntityAContext())
{
using(EntityARepository repository = new EntityARepository(context))
{
EntityA a = repository.Find(1);
a.DoSomething();
}
}
}
}

如果没有特定场景,则可使用隐含的通用上下文


class EntityARepository: RepositoryBase
{
public EntityARepository()
: base(new BoundedContext())
{

}
....
}

class EntityADoService
{
void DoSomething()
{
using(EntityARepository repository = new EntityARepository())
{
EntityA a = repository.Find(1);
a.DoSomething();
}
}
}

[该贴被clonalman于2012-10-12 16:49修改过]
[该贴被clonalman于2012-10-12 16:49修改过]

上下文是否可理解为来龙去脉?我们讲故事需要有个来龙去脉,建模需要上下文。

bdd行为驱动开发讲究将需求故事带入开发中,bdd的给定某个场景,发生什么事件,导致什么后果,这个场景是需求故事的来龙去脉,是否也就是模型的有界上下文呢?

2012-10-13 13:35 "@banq"的内容
上下文是否可理解为来龙去脉?我们讲故事需要有个来龙去脉,建模需要上下文。 ...

就象我们去电影院买票,业务比较简单,无座位上的差别,电影票的确认只决定于放映的影片,座位的选择范围是全部,如果业务复杂一点,按座位差异化营销,电影票确认除了影片之外,还需要对座位确认(比如:普通座、VIP座),选择的范围是部分。

领域中,电影票还是那些电影票,座位还是那些座位,不同业务形态下,买票、退票等都在不同BoundedContext进行的。

[该贴被clonalman于2012-10-13 21:22修改过]

BoundedContext放在Repository,在对象重建时关联实体的事件,避免了Role直接放在实体中,事件一样在实体内部触发,避免了领域事件在代码中高来高去,不好理解。

这想法太妙了,很好的解决了领域事件满天飞的情况。一个Event发到EventBus之后,我们都无法跟踪Event的流转情况。

使用有界上下文之后就能明确的知道Event所属的场景,或者服务。

我为了解决Event界限的问题,使用了和楼主不同的思路,在领域服务中定义私有命令通道。除了全局的EventBus之外,每个领域服务中都有若干条命令通道,一个Event可以发给全局的Bus,也可以发给领域服务中私有的Bus。

全局Bus,C代码


//将自定义命令发送到全局Event Bus
EventManager.Bus.Send(new CustomCommand(
"PojoEntity_Delete", 2));

//指定任意方法为"PojoEntity_Delete"命令的处理程序
[Handler(
"Order_Delete")]
public void PojoEntity_Delete(CustomCommand cmd)
{
Debug.WriteLine(string.Format(
"PojoEntity {0} Deleted.", cmd.ID));
}

//定义一个同步命令
public class CustomCommand : SyncCommand
{
public CustomCommand(string commandName, int? ID)
: base(commandName)
{
this.ID = ID;
}
public int? ID { get; set; }
}

私有Bus,C代码


public class TopicService : IService
{
public static EventManager CRUDBus = EventManager.Define();
public static EventManager CacheBus = EventManager.Define();
//分配命令处理程序
CRUDBus.On(
"CommandName", commandHandler);
//发送命令
CacheBus.Send(new SyncCommand(
"CommandName"));
}

详情可以看最后一条:http://kylinorm.codeplex.com/wikipage?title=Advanced%20Features&referringTitle=Documentation

2012-10-14 09:18 "@gameboyLV"的内容
public class TopicService : IService
{
public static EventManager CRUDBus = EventManager.Define();
public static Eve ...

怎么交互,如通过EventBus,最好不要显式地存在领域模型之中,你的架构要调整了,将来重构的工作量是很大的,引入有界上下文的目的,除了表示业务场景之外,就是隐藏架构实现的细节,对于领域模型来说,只要描述当前的上下文与架构的交互,业务上下文之间的交互既可,怎么交互不是领域应该关心的,所以应该尽量用DDD的元素来表的,而不使用有技术特征的元素(EventManager)
[该贴被clonalman于2012-10-16 08:10修改过]

2012-10-16 08:08 "@clonalman"的内容
所以应该尽量用DDD的元素来表的,而不使用有技术特征的元素(EventManager) ...

是不是可以认为EventManager类似DB一样,是一种Bounded(DB/EventBus) Context呢?

这里有一个视频Putting Your Entities Into Context with Entity Framework,需要爬梯子,其大概截图如下。

在一个大系统中,存在Sales Billing 人事等等不同的有界上下文(Bounded Context),如图: