“不要在设计中使用抽象基类;但在建模/分析中”

本文关键字:建模 基类 抽象 | 更新日期: 2023-09-27 18:25:54

我是SOA的新手,尽管我在OOAD方面有一些经验。

SOA 设计的准则之一是"仅使用抽象类进行建模。从设计中省略它们"。使用抽象有助于建模(分析阶段(。

在分析阶段,我提出了一个 BankAccount 基类。从中衍生出的专门类别是"固定账户"和"储蓄账户"。我需要创建一个服务,该服务将返回用户的所有帐户(帐户列表(。满足要求的服务结构应该是什么?

注意:如果可以使用 WCF 提供代码演示,那就太好了。

“不要在设计中使用抽象基类;但在建模/分析中”

听起来您正在尝试使用 SOA 远程访问您的对象模型。 最好查看希望服务公开的交互和功能,并避免公开服务实现的继承详细信息。

因此,在这种情况下,您需要一个用户帐户列表,您的界面将如下所示

[ServiceContract]
interface ISomeService
{
    [OperationContract]
    Collection<AccountSummary> ListAccountsForUser(
        User user /*This information could be out of band in a claim*/);
}
[DataContract]
class AccountSummary
{
     [DataMember]
     public string AccountNumber {get;set;}
     [DataMember]
     public string AccountType {get;set;}
     //Other account summary information
}

如果您决定沿继承路线走下去,则可以使用 KnownType 属性,但请注意,这会将一些类型信息添加到通过线路发送的消息中,这在某些情况下可能会限制互操作性。

更新

当我回答时,我的时间有点有限,所以我会尝试详细说明为什么我更喜欢这种风格。

我不建议在单独的层中通过 DTO 公开您的 OOAD,这通常会导致一个臃肿的界面,您可以在其中传递大量未使用的数据,并虔诚地将其映射到本质上是您的域模型的副本,删除了所有逻辑,我只是看不到价值。我通常围绕它公开的操作设计我的服务层,并使用 DTO 来定义服务交互。

使用基于公开操作而不是域模型的 DTO 有助于保持服务封装并减少与域模型的耦合。 通过不公开我的域模型,我不必为了序列化而对字段可见性或继承做出任何妥协。

例如,如果我将转移方法从一个帐户公开到另一个帐户,则服务接口将如下所示:

[ServiceContract]
interface ISomeService
{
    [OperationContract]
    TransferResult Transfer(TransferRequest request);
}
[DataContract]
class TransferRequest
{
     [DataMember]
     public string FromAccountNumber {get;set;}
     [DataMember]
     public string ToAccountNumber {get;set;}
     [DataMember]
     public Money Amount {get;set;}
}
class SomeService : ISomeService
{
    TransferResult Transfer(TransferRequest request)
    {
        //Check parameters...omitted for clarity
        var from = repository.Load<Account>(request.FromAccountNumber);
        //Assert that the caller is authorised to request transfer on this account
        var to = repository.Load<Account>(request.ToAccountNumber);
        from.Transfer(to, request.Amount);
        //Build an appropriate response (or fault)
    }
}

现在,从这个界面,Conusmer非常清楚调用此操作所需的数据是什么。 如果我将其实现为

[ServiceContract]
interface ISomeService
{
    [OperationContract]
    TransferResult Transfer(AccountDto from, AccountDto to, MoneyDto dto);
}

和 AccountDto 是账户中字段的副本,作为使用者,我应该填充哪些字段?全部?如果添加新属性以支持新操作,则所有操作的所有用户现在都可以看到此属性。 WCF 允许我将此属性标记为非必需属性,这样我就不会中断所有其他客户端,但如果它是新操作的必需属性,则客户端只会在调用该操作时发现。

更糟糕的是,作为服务实施者,如果他们为我提供了当前余额会发生什么? 我应该相信它吗?

这里的一般规则是询问谁拥有数据,客户端或服务? 如果客户端拥有它,那么它可以将其传递给服务,并且在执行一些基本检查后,服务可以使用它。 如果服务拥有它,则客户端应仅传递足够的信息,以便服务检索所需的内容。 这允许服务维护其拥有的数据的一致性。

在此示例中,服务拥有帐户信息,用于查找该信息的密钥是帐号。 虽然服务可以验证金额(为正数、支持的货币等(,但这归客户所有,因此我们希望填充 DTO 上的所有字段。

总之,我已经看到它完成了所有 3 种方式,但围绕特定操作设计 DTO 是迄今为止从服务和消费者实现来看最成功的。 它允许操作独立发展,并且非常明确地说明服务的期望以及将返回给客户端的内容。

我几乎同意其他人在这里所说的,但可能需要添加这些:

  • 大多数 SOA 系统使用 Web 服务进行通信。Web 服务通过 WSDL 公开其接口。WSDL 对继承没有任何了解。
  • DTO 中的所有行为在越过电线时都将丢失
  • 所有私有/受保护的字段在越过电线时都将丢失

想象一下这个场景(案例很愚蠢,但说明性(:

public abstract class BankAccount
{
    private DateTime _creationDate = DateTime.Now;
    public DateTime CreationDate
    {
        get { return _creationDate; }
        set { _creationDate = value; }
    }
    public virtual string CreationDateUniversal
    {
        get { return _creationDate.ToUniversalTime().ToString(); }
    }
}
public class SavingAccount : BankAccount
{
    public override string CreationDateUniversal
    {
        get
        {
            return base.CreationDateUniversal + " UTC";
        }
    }
}

现在,您已在客户端上使用"添加服务引用">"添加 Web 引用">(而不是重复使用程序集(来访问储蓄帐户。

SavingAccount account = serviceProxy.GetSavingAccountById(id);
account.CreationDate = DateTime.Now;
var creationDateUniversal = account.CreationDateUniversal; // out of sync!!

将要发生的是对CreationDate的更改不会对CreationDateUniversal进行响应,因为没有跨线的实现,只有服务器上序列化时的CreationDateUniversal值。