【问题标题】:SOA, TDD, DI & DDD - a shell game?SOA、TDD、DI 和 DDD - 一个空壳游戏?
【发布时间】:2011-03-31 12:45:06
【问题描述】:

好的,我将尝试简短而直截了当。我正在尝试开发一个可测试并支持依赖注入的松散耦合的多层服务应用程序。这是我所拥有的:

在服务层,我有一个 StartSession 方法,它接受启动会话所需的一些关键数据。我的服务类是一个外观,并委托给注入到服务类构造函数中的 ISessionManager 接口的一个实例。

我在数据访问层使用存储库模式。所以我有一个 ISessionRepository,我的域对象可以使用它,并且我使用当前的数据访问技术来实现。 ISessionRepository 具有 GetById、Add 和 Update 方法。

由于我的服务类只是一个门面,我认为可以肯定地说我的 ISessionManager 实现是我的体系结构中的实际服务类。此类与我的 Session 域/业务对象协调操作。这就是空壳游戏和问题所在。

在我的 SessionManager 类(具体的 ISessionManager)中,我是这样实现 StartSession 的:

public ISession StartSession(object sessionStartInfo)
{
    var session = Session.GetSession(sessionStartInfo);

    if (session == null)
        session = Session.NewSession(sessionStartInfo);

    return session;
}

这段代码有三个问题:

  • 首先,显然我可以将此逻辑移动到我的 Session 类中的 StartSession 方法中,但我认为这会破坏 SessionManager 类的目的,然后它只是成为第二个外观(或者它仍然被视为协调器?)。唉,贝壳游戏。
     
  • 其次,SessionManager 对 Session 类具有紧密耦合的依赖关系。我考虑创建一个可以注入到 SessionManager 中的 ISessionFactory/SessionFactory,但是我会在工厂内部进行相同的紧耦合。但是,也许没关系?
     
  • 最后,在我看来,真正的 DI 和工厂方法不能混合使用。毕竟,我们要避免“新建”一个对象的实例,而让容器将实例返回给我们。真正的 DI 说我们不应该直接引用容器。那么,如何将具体的 ISessionRepository 类注入到我的 Session 域对象中呢?我是否将其注入工厂类,然后在构造新实例时手动将其传递给 Session(使用“new”)?
     

请记住,这也只是一项操作,我还需要执行其他任务,例如保存会话、根据各种标准列出会话以及在我的解决方案中使用其他域对象。另外,Session 对象还封装了授权、验证等业务逻辑,所以(我认为)它需要存在。

我想要完成的关键不仅是功能性的,而且是可测试的。我正在使用 DI 来打破依赖关系,因此我们可以使用 mock 轻松实现单元测试,并让我们能够对具体实现进行更改,而无需在多个领域进行更改。

您能否帮助我了解此类设计的最佳实践以及如何才能最好地实现我的目标,以实现可靠的 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 类。

第二次更新

我在 Session 域对象中添加了 IValidator 依赖项,以说明还有其他组件在使用中。

【问题讨论】:

  • 您能发布更多代码吗?不清楚每个类的作用——例如,Session 类是被注入的服务类,还是存在三个不同的类(Session、SessionManager、SessionService)?
  • Session 类是静态的吗?同样,多一点代码会澄清很多。
  • Session 是域对象(根)。

标签: c# dependency-injection tdd domain-driven-design soa


【解决方案1】:

发布的代码澄清了很多。在我看来,会话类保持状态(具有行为),服务和管理器类严格执行操作/行为。

您可能会考虑从 Session 中删除 Repository 依赖项并将其添加到 SessionManager。因此,您的 Manager 类将有一个 Save(ISession session) 方法,而不是调用 Repository.Save(this) 的 Session,然后该方法将调用 Repository.Save(session)。这意味着会话本身不需要由容器管理,并且通过“new Session()”(或使用相同的工厂)创建它是完全合理的。我认为 Session 上的 Get- 和 New- 方法是静态的这一事实是它们可能不属于该类的线索/气味(此代码是否编译?似乎您在静态方法中使用实例属性)。

最后,在我看来,真正的 DI 和工厂方法不混合。后 总之,我们要避免“新建”一个 一个对象的实例,让 容器将实例返回给我们。 真正的 DI 说我们不应该 直接引用容器。所以, 那我怎么弄到混凝土 ISessionRepository 类注入 我的会话域对象?我有吗 然后注入工厂类 手动将其传递给 Session 时 构造一个新实例(使用 “新”)?

当涉及到通过 IOC 容器管理混合状态和服务的类时,这个问题会被问到很多。一旦你使用了一个使用“new”的抽象工厂,你就失去了从对象图中该类向下的 DI 框架的好处。您可以通过完全分离状态和服务并仅让您的类提供由容器管理的服务/行为来摆脱这种情况。这导致通过方法调用(也称为函数式编程)传递所有数据。一些容器(一个是温莎)也为这个问题提供了解决方案(在温莎它被称为工厂设施)。

编辑:想补充一点,函数式编程也会导致 Fowler 所说的“贫血域模型”。这在 DDD 中通常被认为是一件坏事,因此您可能需要权衡一下我在上面发布的建议。

【讨论】:

  • 那么,如果我将 ISessionRepository 注入到 SessionManager 类中并从 Session 中消除工厂方法,那么检索和创建新 Session 实例的逻辑会去哪里?在 SessionManager 中,存储库还是让 SessionFactory 处理该责任会更好?
  • 为了更进一步,我在 Session 域对象中添加了对验证器的依赖。因此,即使我将对存储库的依赖移出,我仍然必须处理这个(以及任何其他潜在的合作者)。
  • 这些额外的合作者也不能由经理协调吗?
  • 这是我的困惑的一部分 - 谁应该对应用程序流程中的内容负责。是的,我可以将验证放在管理器类中,但是假设我有更多的域对象,我是否最终会为每个或 1 个管理器创建一个管理器类,该管理器接受一堆注入的协作者?我对 DDD 的理解是我应该为每个域根或聚合根有 1 个存储库,所以现在我将为每个(管理器、域对象、存储库)有 3 个类。我不反对,只要它真的有意义。这是否也一直延伸到服务外观?
  • 如果为每个类设置三个类是有意义的,并且如果每个类都有一组不同的职责,那么您应该将其视为一个好的想法,因为这种分离会导致可靠(和 SOLID :) 软件设计。
【解决方案2】:

只是一些 cmets...

毕竟,我们希望避免“新建”一个对象的实例,让容器将实例返回给我们。

这不是 100% 的。您希望避免“新建”只跨越所谓的 seam,它们基​​本上是层之间的线。如果您尝试使用存储库抽象持久性 - 这是一个 seam,如果您尝试将域模型与 UI 分离(经典的一个 - system.web 参考),那么会有一个 seam。如果您在同一层,那么将一个实现与另一个实现解耦有时没有什么意义,只会增加额外的复杂性(无用的抽象、ioc 容器配置等)。您想要抽象某些东西的另一个(明显)原因是您现在已经需要多态性。

真正的 DI 说我们不应该直接引用容器。

这是真的。但是您可能缺少的另一个概念是所谓的 composition root(事物有名字是件好事:)。这个概念解决了与“何时使用服务定位器”的混淆。想法很简单——你应该尽可能快地构建你的依赖图。应该只有 1 个地方您实际引用 ioc 容器。

例如在asp.net mvc应用中,composition的共同点是ControllerFactory

我是否将其注入工厂类,然后在构造新实例时手动将其传递给 Session

就我目前所见,工厂通常对两件事有利:

1.创建复杂对象(Builder pattern 有很大帮助)
2.解决违反open closedsingle responsibility原则的行为

public void PurchaseProduct(Product product){
  if(product.HasSomething) order.Apply(new FirstDiscountPolicy());
  if(product.HasSomethingElse) order.Apply(new SecondDiscountPolicy());
}

变成:

public void PurchaseProduct(Product product){
  order.Apply(DiscountPolicyFactory.Create(product));
}

这样,如果出现新的折扣政策,您持有PurchaseProduct 的班级将不需要修改,PurchaseProduct 将只负责购买产品,而不知道要申请什么折扣。

附:如果您对 DI 感兴趣,请阅读 "Dependency injection in .NET" by Mark Seemann

【讨论】:

  • 我没有引用 IoC 容器的问题是,当我们需要创建一个注入了依赖项的类型的新实例时。在典型的解决方案中,将具体实现映射到我们编写代码所针对的接口是在客户端应用程序(例如 WCF 的服务主机)中完成的。除非我让容器自动创建实例并注入它,否则在某些时候我必须引用容器。例如,如果 FirstDiscountPolicy 和 SecondDiscountPolicy 通过构造函数注入需要某些东西,DiscountPolicyFactory 将如何创建它们?
【解决方案3】:

我想我会发布我最终遵循的方法,同时在上面给予应有的赞誉。

在阅读了一些关于 DDD 的其他文章后,我终于发现我们的域对象不应该对它们的创建或持久性负责,以及可以从在域层内(正如 Arnis 所避开的那样)。

所以,我保留了我的 SessionManager 类,但将其重命名为 SessionService,以便更清楚地表明它是一个域服务(不要与外观层中的 SessionService 混淆)。现在实现如下:

public class SessionService : ISessionService
{
    public SessionService(ISessionFactory factory, ISessionRepository repository)
    {
        Factory = factory;
        Repository = repository;
    }

    public ISessionFactory Factory { get; private set; }
    public ISessionRepository Repository { get; private set; }

    public ISession StartSession(SessionCriteria criteria)
    {
        var session = Repository.GetSession(criteria);

        if (session == null)
            session = Factory.CreateSession(criteria);
        else if (!session.CanResume)
            thrown new InvalidOperationException("Cannot resume the session.");

        return session;
    }
}

Session 类现在更像是一个真正的领域对象,只关心使用 Session 时所需的状态和逻辑,例如上面显示的 CanResume 属性和验证逻辑。

SessionFactory 类负责创建新实例,并允许我仍然注入容器提供的 ISessionValidator 实例,而无需直接引用容器本身:

public class SessionFactory : ISessionFactory
{
    public SessionFactory(ISessionValidator validator)
    {
        Validator = validator;
    }

    public ISessionValidator Validator { get; private set; }

    public Session CreateSession(SessionCriteria criteria)
    {
        var session = new Session(Validator);

        // Map properties

        return session;
    }
}

除非有人能指出我的方法中的缺陷,否则我很满意这与 DDD 一致,并为我提供了对单元测试等的全面支持——我所追求的一切。

【讨论】:

    猜你喜欢
    • 2016-04-18
    • 2010-11-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多