SOA、TDD、DI 和DDD - 一个骗局游戏?
好吧,我将尝试简短、直入主题。我正在尝试开发一个松散耦合的多层服务应用程序,该应用程序可测试并支持依赖项注入。这就是我所拥有的:
在服务层,我有一个 StartSession 方法,它接受启动会话所需的一些关键数据。我的服务类是一个外观,并委托给注入到服务类构造函数中的 ISessionManager 接口的实例。
我在数据访问层中使用存储库模式。因此,我有一个 ISessionRepository,我的域对象将使用它,并且我使用当前的数据访问技术来实现它。 ISessionRepository 具有 GetById、Add 和 Update 方法。
由于我的服务类只是一个外观,因此我认为可以肯定地说我的 ISessionManager 实现是我的体系结构中的实际服务类。此类协调与我的会话域/业务对象的操作。这就是 shell 游戏和问题出现的地方。
在我的 SessionManager 类(具体的 ISessionManager)中,这是我如何实现 StartSession 的:
public ISession StartSession(object sessionStartInfo)
{
var session = Session.GetSession(sessionStartInfo);
if (session == null)
session = Session.NewSession(sessionStartInfo);
return session;
}
这段代码存在三个问题:
- 首先,显然我可以将此逻辑移至我的 StartSession 方法中。 Session 类,但我认为这会破坏 SessionManager 类的目的,而 SessionManager 类只是成为第二个外观(或者它仍然被视为协调器?)。唉,这就是骗局。
- 其次,SessionManager 对 Session 类具有紧密耦合的依赖性。我考虑创建一个可以注入到 SessionManager 中的 ISessionFactory/SessionFactory,但随后我会在工厂内部具有相同的紧密耦合。但是,也许这样可以吗?
- 最后,在我看来,真正的 DI 和工厂方法不能混合。毕竟,我们希望避免“new”对象的实例并让容器将实例返回给我们。而真正的 DI 表示我们不应该直接引用容器。那么,如何将具体的 ISessionRepository 类注入到我的 Session 域对象中呢?我是否将其注入到工厂类中,然后在构造新实例(使用“new”)时手动将其传递到 Session 中?
请记住,这也只是一项操作,我还需要执行其他任务,例如保存会话、根据各种条件列出会话以及使用解决方案中的其他域对象。另外,Session 对象还封装了授权、验证等业务逻辑。所以(我认为)它需要存在。
我希望实现的目标的关键不仅是功能性的,而且是可测试的。我使用 DI 来打破依赖关系,这样我们就可以使用模拟轻松实现单元测试,并且使我们能够更改具体实现,而无需在多个区域进行更改。
您能否帮助我了解此类设计的最佳实践以及如何最好地实现可靠的 SOA、DDD 和 TDD 解决方案的目标?
更新
我被要求提供一些额外的代码,尽可能简洁:
[ServiceContract()]
public class SessionService : ISessionService
{
public SessionService(ISessionManager manager) { Manager = manager; }
public ISessionManager Manager { get; private set; }
[OperationContract()]
public SessionContract StartSession(SessionCriteriaContract criteria)
{
var session = Manager.StartSession(Mapper.Map<SessionCriteria>(criteria));
return Mapper.Map<SessionContract>(session);
}
}
public class SessionManager : ISessionManager
{
public SessionManager() { }
public ISession StartSession(SessionCriteria criteria)
{
var session = Session.GetSession(criteria);
if (session == null)
session = Session.NewSession(criteria);
return session;
}
}
public class Session : ISession
{
public Session(ISessionRepository repository, IValidator<ISession> validator)
{
Repository = repository;
Validator = validator;
}
// ISession Properties
public static ISession GetSession(SessionCriteria criteria)
{
return Repository.FindOne(criteria);
}
public static ISession NewSession(SessionCriteria criteria)
{
var session = ????;
// Set properties based on criteria object
return session;
}
public Boolean Save()
{
if (!Validator.IsValid(this))
return false;
return Repository.Save(this);
}
}
而且,显然,有一个 ISessionRepository 接口和具体的 XyzSessionRepository 类,我认为不需要显示。
第二次更新
我将 IValidator 依赖项添加到会话域对象中,以说明还有其他组件正在使用。
Okay, I'm going to try and go short and straight to the point. I am trying to develop a loosely-coupled, multi-tier service application that is testable and supports dependency injection. Here's what I have:
At the service layer, I have a StartSession method that accepts some key data required to, well, start the session. My service class is a facade and delegates to an instance of the ISessionManager interface that is injected into the service class constructor.
I am using the Repository pattern in the data access layer. So I have an ISessionRepository that my domain objects will work with and that I implement using the data access technology du jour. ISessionRepository has methods for GetById, Add and Update.
Since my service class is just a facade, I think it is safe to say that my ISessionManager implementation is the actual service class in my architecture. This class coordinates the operations with my Session domain/business object. And here's where the shell game and problem comes in.
In my SessionManager class (the concrete ISessionManager), here's how I have StartSession implemented:
public ISession StartSession(object sessionStartInfo)
{
var session = Session.GetSession(sessionStartInfo);
if (session == null)
session = Session.NewSession(sessionStartInfo);
return session;
}
I have three problems with this code:
- First, obviously I could move this logic into a StartSession method in my Session class but I think that would defeat the purpose of the SessionManager class which then simply becomes a second facade (or is it still considered a coordinator?). Alas, the shell game.
- Second, SessionManager has a tightly-coupled dependance upon the Session class. I considered creating an ISessionFactory/SessionFactory that could be injected into SessionManager but then I'd have the same tight-coupling inside the factory. But, maybe that's okay?
- Finally, it seems to me that true DI and factory methods don't mix. After all, we want to avoid "new"ing an instance of an object and let the container return the instance to us. And true DI says that we should not reference the container directly. So, how then do I get the concrete ISessionRepository class injected into my Session domain object? Do I have it injected into the factory class then manually pass it into Session when constructing a new instance (using "new")?
Keep in mind that this is also only one operation and I also need to perform other tasks such as saving a session, listing sessions based on various criteria plus work with other domain objects in my solution. Plus, the Session object also encapsulates business logic for authorization, validation, etc. so (I think) it needs to be there.
The key to what I am looking to accomplish is not only functional but testable. I am using DI to break dependencies so we can easily implement unit tests using mocks as well as give us the ability to make changes to the concrete implementations without requiring changes in multiple areas.
Can you help me wrap my head around the best practices for such a design and how I can best achieve my goals for a solid SOA, DDD and TDD solution?
UPDATE
I was asked to provide some additional code, so as succinctly as possible:
[ServiceContract()]
public class SessionService : ISessionService
{
public SessionService(ISessionManager manager) { Manager = manager; }
public ISessionManager Manager { get; private set; }
[OperationContract()]
public SessionContract StartSession(SessionCriteriaContract criteria)
{
var session = Manager.StartSession(Mapper.Map<SessionCriteria>(criteria));
return Mapper.Map<SessionContract>(session);
}
}
public class SessionManager : ISessionManager
{
public SessionManager() { }
public ISession StartSession(SessionCriteria criteria)
{
var session = Session.GetSession(criteria);
if (session == null)
session = Session.NewSession(criteria);
return session;
}
}
public class Session : ISession
{
public Session(ISessionRepository repository, IValidator<ISession> validator)
{
Repository = repository;
Validator = validator;
}
// ISession Properties
public static ISession GetSession(SessionCriteria criteria)
{
return Repository.FindOne(criteria);
}
public static ISession NewSession(SessionCriteria criteria)
{
var session = ????;
// Set properties based on criteria object
return session;
}
public Boolean Save()
{
if (!Validator.IsValid(this))
return false;
return Repository.Save(this);
}
}
And, obviously, there is an ISessionRepository interface and concrete XyzSessionRepository class that I don't think needs to be shown.
2nd UPDATE
I added the IValidator dependency to the Session domain object to illustrate that there are other components in use.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
发布的代码澄清了很多。在我看来,会话类保存状态(具有行为),而服务和管理器类严格执行操作/行为。
您可能会考虑从会话中删除存储库依赖项并将其添加到会话管理器中。因此,您的 Manager 类将具有一个 Save(ISession session) 方法,然后调用 Repository.Save(session),而不是 Session 调用 Repository.Save(this)。这意味着会话本身不需要由容器管理,并且通过“new Session()”(或使用执行相同操作的工厂)创建它是完全合理的。我认为 Session 上的 Get- 和 New- 方法是静态的这一事实是它们可能不属于该类的线索/气味(此代码是否可以编译?似乎您正在静态方法中使用实例属性)。
当涉及到通过 IOC 容器管理混合状态和服务的类时,这个问题经常被问到。一旦您使用了使用“new”的抽象工厂,您就会失去对象图中从该类向下的 DI 框架的好处。您可以通过完全分离状态和服务并仅让提供由容器管理的服务/行为的类来摆脱这种情况。这导致通过方法调用(也称为函数式编程)传递所有数据。一些容器(温莎就是其中之一)也为这个问题提供了解决方案(在温莎它被称为工厂设施)。
编辑:想补充一点,函数式编程也会导致福勒所说的“贫血领域模型”。这在 DDD 中通常被认为是一件坏事,因此您可能必须根据我上面发布的建议来权衡这一点。
The posted code clarifies a lot. It looks to me like the session class holds state (with behavior), and the service and manager classes strictly perform actions/behavior.
You might look at removing the Repository dependency from the Session and adding it to the SessionManager. So instead of the Session calling Repository.Save(this), your Manager class would have a Save(ISession session) method that would then call Repository.Save(session). This would mean that the session itself would not need to be managed by the container, and it would be perfectly reasonable to create it via "new Session()" (or using a factory that does the same). I think the fact that the Get- and New- methods on the Session are static is a clue/smell that they may not belong on that class (does this code compile? Seems like you are using an instance property within a static method).
This question gets asked a LOT when it comes to managing classes that mix state and service via an IOC container. As soon as you use an abstract factory that uses "new", you lose the benefits of a DI framework from that class downward in the object graph. You can get away from this by completely separating state and service, and having only your classes that provide service/behavior managed by the container. This leads to passing all data through method calls (aka functional programming). Some containers (Windsor for one) also provide a solution to this very problem (in Windsor it's called the Factory Facility).
Edit: wanted to add that functional programming also leads to what Fowler would call "anemic domain models". This is generally considered a bad thing in DDD, so you might have to weigh that against the advice I posted above.
只是一些评论...
这并不是 100% 正确的。您希望避免仅跨所谓的“接缝”进行“new”操作,这些接缝基本上是层之间的线。如果您尝试使用存储库抽象持久性 - 这就是一个接缝,如果您尝试将域模型与 UI(经典模型 - system.web 参考)分离,那么就有一个接缝。如果您位于同一层,那么将一个实现与另一个实现解耦有时没有什么意义,只会增加额外的复杂性(无用的抽象、ioc 容器配置等)。您想要抽象某些东西的另一个(明显的)原因是您现在已经需要多态性。
这是真实的。但您可能缺少的另一个概念是所谓的组合根(事物有一个名字是件好事:)。这个概念解决了与“何时使用服务定位器”的混淆。想法很简单 - 你应该尽快构建你的依赖图。应该只有 1 个地方是您实际引用 ioc 容器的地方。
例如,在asp.net mvc应用程序中,组合的共同点是ControllerFactory。
正如我所见,工厂通常有两件事:
1.创建复杂对象(构建器模式有很大帮助)
2.解决违反开放封闭和单一职责原则
变为:
这样,如果满足以下条件,则无需修改包含
PurchaseProduct
的类 :新的折扣政策即将出现,PurchaseProduct
将只负责购买产品,而不知道要应用什么折扣。PS 如果您对 DI 感兴趣,您应该阅读 马克·西曼。
Just some comments...
this ain't true for 100%. You want to avoid "new"ing only across so called seams which basically are lines between layers. if You try to abstract persistence with repositories - that's a seam, if You try to decouple domain model from UI (classic one - system.web reference), there's a seam. if You are in same layer, then decoupling one implementation from another sometimes makes little sense and just adds additional complexity (useless abstraction, ioc container configuration etc.). another (obvious) reason You want to abstract something is when You already right now need polymorphism.
this is true. but another concept You might be missing is so called composition root (it's good for things to have a name :). this concept resolves confusion with "when to use service locator". idea is simple - You should compose Your dependency graph as fast as possible. there should be 1 place only where You actually reference ioc container.
E.g. in asp.net mvc application, common point for composition is
ControllerFactory
.As I see so far, factories are generally good for 2 things:
1.To create complex objects (Builder pattern helps significantly)
2.Resolving violations of open closed and single responsibility principles
becomes as:
In that way Your class that holds
PurchaseProduct
won't be needed to be modified if new discount policy appears in sight andPurchaseProduct
would become responsible for purchasing product only instead of knowing what discount to apply.P.s. if You are interested in DI, You should read "Dependency injection in .NET" by Mark Seemann.
我想我应该发布我最终遵循的方法,同时给予上面应有的认可。
在阅读了一些有关 DDD 的其他文章后,我终于发现我们的域对象不应该对其创建或持久性负责,并且可以从域内“新建”域对象的实例。层(正如阿尼斯所回避的那样)。
因此,我保留了 SessionManager 类,但将其重命名为 SessionService,这样可以更清楚地看出它是一个域服务(不要与门面层中的 SessionService 混淆)。它现在的实现如下:
Session 类现在更像是一个真正的域对象,只关心使用 Session 时所需的状态和逻辑,例如上面显示的 CanResume 属性和验证逻辑。
SessionFactory 类负责创建新实例,并允许我仍然注入容器提供的 ISessionValidator 实例,而无需直接引用容器本身:
除非有人指出我的方法中的缺陷,否则我很满意这与DDD 并为我提供了对单元测试等的全面支持 - 我所追求的一切。
I thought I'd post the approach I ended up following while giving due credit above.
After reading some additional articles on DDD, I finally came across the observation that our domain objects should not be responsible for their creation or persistence as well as the notion that it is okay to "new" an instance of a domain object from within the Domain Layer (as Arnis eluded).
So, I retained my SessionManager class but renamed it SessionService so it would be clearer that it is a Domain Service (not to be confused with the SessionService in the facade layer). It is now implemented like:
The Session class is now more of a true domain object only concerned with the state and logic required when working with the Session, such as the CanResume property shown above and validation logic.
The SessionFactory class is responsible for creating new instances and allows me to still inject the ISessionValidator instance provided by the container without directly referencing the container itself:
Unless someone can point out a flaw in my approach, I'm pretty comfortable that this is consistent with DDD and gives me full support for unit testing, etc. - everything I was after.