【问题标题】:C# Inheritance - Updating Base ClassesC# 继承 - 更新基类
【发布时间】:2018-09-24 12:54:39
【问题描述】:

我正在努力将我的一个系统更新为更面向 DDD 的方法,并着眼于从我的程序中消除贫血的域模型。

我正在尝试了解从孩子更新基类的最佳实践。

我有两节课。 (以下简化)

public class WorkItem
{
   public int Y {get; private set;}
   public int X {get; private set;}

   public void Update(int x, int y)
   {
      Validate(x);
      Validate(y);

      X = x;
      Y = y;
   }

   public void Validate(int x)
    {
      ///Validation rules here
    }

}


public class Ticket:WorkItem
{
   public int Id {get; set;}
   public int Z {get; set;}
}

这是一个 Web 应用程序,因此我可以更新票证。所以让我们假设我现在有一个看起来像这样的服务

public void UpdateTicket(int Id)
 {
    var ticket = _context.Tickets.Where(c=>c.Id == Id).SingleOrDefault();  
    //Update Tickets

   _context.SaveChanges();       
 }

我希望能够在运行 Validate 方法的同时同时更新 Workitem 类和工单类中的字段。

这是我想出的选项:

选项 A

public class Ticket:WorkItem
{
 public int Id {get; set;}
 public int Z {get; set;}

 public void Update(int x, int y, int z)
 {
  base.Validate(z);
  base.Update(x, y);
  Z=z;
 }
 }

在子级中添加一个更新方法,该方法调用父级中的更新方法。如果我添加另一个级别的继承,这开始感觉很混乱,因为我必须将父成员从底部传递到顶部。

选项 B:

 public class Ticket:WorkItem
 {
  public int Id {get; set;}
  public int Z {get; set;}

  public void Update(int x, int y, int z)
  {
   base.Validate(z)
   base.Validate(x)
   base.Validate(y)

   X = x;
   Y = y;
   Z = z;
  }
 }

在不调用父级中的 Update 方法的情况下显式更新子级 update 方法中的属性。这种方法感觉更好,但由于某种原因仍然感觉不对

我不完全确定这两种方法是否正确,请您推荐解决此类问题的最佳方法。

【问题讨论】:

  • 要考虑的一个选项 - 在基类 (WorkItem) 中创建一个 ValidateThenUpdate 方法。在基类中添加一个虚拟的Validate 方法(没有参数,它验证该级别的类的所有属性)。在基类中添加一个虚拟的Update 方法。 ValidateThenUpdate 调用Validate 然后Update。您的Ticket 类将继承基类并覆盖ValidateUpdate(包括对base.Validate 等的调用)。
  • 也许你应该考虑组合而不是继承?工单应该有一个工作项作为成员。每个对象都应该有自己的方法并实现自己的接口。看起来您也在幕后使用数据库,因此使用两个不同的表和连接它们的外键会更容易。另外,我相信有些工单没有与之关联的工作项..
  • @mjwills 如果方法是无参数的,那么类如何知道哪些字段需要更新和验证?
  • 为什么需要将 x、y 和 z 传递给 Update?为什么不让设置器在 X 和 Y 上公开,并在更新中验证属性?这样您就可以使其成为虚拟并覆盖(或者,只需在设置器本身中调用 validate,如果这对您的使用更有意义)
  • 我会设想一个模型,其中属性是读写的(而不是您当前的只读)。调用者设置属性,然后调用ValidateThenUpdate基本上是@KMoussa 所说的。

标签: c# .net inheritance


【解决方案1】:

答案很长,请耐心等待我完成每个重构步骤。

选项 A 肯定更好,因为您不会在派生类中重复行为,但是我想知道 Update() 的功能是什么。可以通过以下方式获得完全相同的运行时行为:

abstract class WorkItem {
    public int X {
        get => _x;
        set {
            Validate(value);
            _x = value;
        }
    }

    private int _x;
}

你需要一个单独的方法来做什么?虚构名称使您的逻辑更难推理,但如果Update() 实际上是一个高级操作,则可能有意义,在这种情况下,上述组合可能会起作用:

abstract class WorkItem {
    public int Quantity {
        get => _quantity;
        protected set {
            Validate(value);
            _quantity = value;
        }
    }

    // Other properties

    // Single method because you cannot change quantity without
    // affecting discount (and vice-versa).
    UpdateOrder(int newQuantity, float discount) {
        Quantity = newQuantity;
        Discount = discount; // Discount validation is maybe affected by Quantity
    }

    private int _quantity;
}

假设有一个派生类:

sealed class InternationalOrder : Workitem {
    public UpdateOrder(int quantity, float discount, string address) {
        Address = address;

        // Address may affect discount eligibility
        UpdateOrder(quantity, discount);
    }
}

当然这是虚构的,但您应该尽可能依赖现有行为。更好的是,不应该有一个 generic UpdateXyz() 方法(通常甚至不是公共 setter),而是 actionsChangeDeliveryDate()Cancel()Postpone()。优点是从业务规则中抽象出状态。

如果验证成本高昂或存在许多内部依赖项怎么办?在前面的示例中,您设置属性的顺序很重要,这是一个坏事。您可以一起验证一次:

abstract class WorkItem {
    public virtual void Validate() {
        if (Discount > MaximumDiscount)
            throw new InvalidOperationException("Discount is way to high!");

        if (Quantity < MinimumQuantityForDiscount)
            throw new InvalidOperationException("Order is not eligible for discount.");
    }
}

sealed class OnlineOrder : WorkItem {
    public override Validate() {
        base.Validate();

        // More validation rules, specific for OnlineOrder
    }
}

基本属性验证不是 DDD 逻辑的一部分,恕我直言,必须就地:

public int Quantity {
    get => _quantity;
    set {
        if (value < 0)
            throw new ArgumentOutOfRangeException("...");

        _quantity = value;
    }
}

什么时候必须调用Validate()

  • 每次更改属性时(内部实现类似于INotifyPropertyChanged 的机制)。这是最简单的方法,但如果验证非常复杂或缓慢(例如,有数千个业务规则,可能来自外部数据源的数据),它可能会影响性能。请注意,业务规则验证不应该是任何重要场景的类责任.
  • 在调用SaveChanges() 之前手动在UpdateTicket() 方法中。明显的缺点是调用者可能忘记调用它。
  • 以上的组合。

我显然更喜欢第三种选择:使其自动并在执行批量更新时临时禁用验证。概念证明:

abstract class WorkItem {
    public int Quantity {
        get => _quantity;
        protected set {
            if (value < 0)
                throw new ArgumentOutOfRangeException("...");

            if (_quantity != value) {
                _quantity = value;
                Validate();
            }
        }
    }

    public virtual void Validate() {
        if (_isValidationDisabled)
            return;

        if (Discount > MaximumDiscount)
            throw new InvalidOperationException("Discount is way to high!");

        if (Quantity < MinimumQuantityForDiscount)
            throw new InvalidOperationException("Order is not eligible for discount.");
    }

    public void BeginUpdate() {
        _isValidationDisabled = true;
    }

    public void EndUpdate() {
        _isValidationDisabled = false;
        Validate();
    }

    private bool _isValidationDisabled;
    private int _quantity;
}

当然要提取一个可重用的SetBackingStore()方法来设置字段的值并调用Validate()

会这样使用:

public void UpdateTicket(int Id) {
    var ticket = _context.Tickets.Where(c => c.Id == Id).SingleOrDefault();  
    if (ticket == null) { /* no ticket? */ }

    ticket.BeginUpdate();
    tickets.Status = TicketStatus.Resolved;
    ticked.ResolvedAt = DateTime.UtcNow;
    ticket.EndUpdate();

    _context.SaveChanges();       
}

最后的想法:BeginUpdate()/EndUpdate() 元组真的很困扰我(如果出现错误怎么办?如果调用者忘记调用 EndUpdate() 怎么办?)创建一个简单的 IDisposable 类来为您完成工作(让它成为一个可以访问私有状态的嵌套类)能够编写:

public void UpdateTicket(int Id) {
    var ticket = _context.Tickets.Where(c => c.Id == Id).SingleOrDefault();  
    if (ticket == null) { /* no ticket? */ }

    using (ticket.BeginUpdate()) {
        tickets.Status = TicketStatus.Resolved;
        ticked.ResolvedAt = DateTime.UtcNow;
    }

    _context.SaveChanges();       
}

我们这里仍然有一股难闻的味道,那些 setter(或 Java 风格的等效方法,如 UpdateStatus(TicketStatus.Resolved, DateTime.Now))错失了引入抽象的机会:​​

public void UpdateTicket(int Id) {
    var ticket = _context.Tickets.Where(c => c.Id == Id).SingleOrDefault();  
    if (ticket == null) { /* no ticket? */ }

    ticket.Close();

    _context.SaveChanges();       
}

Close 方法的实现可能与前面的示例完全相同(不要忘记尽可能重用现有的行为):

void Close() {
    tickets.Status = TicketStatus.Resolved;
    ticked.ResolvedAt = DateTime.UtcNow;
    Validate();
}

并不总是可以拥有这样的高级方法(例如在编辑对象属性时...)然后在这些情况下将 BeginUpdate() 保留在原位。

【讨论】:

  • 你说得对,我的 validate 方法不只是做基本的验证,而且更像 if(Completed && ReviewDate != null) //remove reviewdate。它更像是完整性检查而不是验证检查。我喜欢你的回答,但是选项 3 不是 DDD 对吗?在 DDD 中,您将有一个诸如 UpdateTicketStatus(Status) 之类的方法,而不是在服务中设置它?
  • UpdateTicketStatus()ticket.Status = ... 几乎完全相同。 DDD 是通用的,并非所有语言都有属性(让我们尝试用 Java 重写相同的示例). The biggest design smell here is probably that we should have _high-level_ actions instead of dumb setters (but it's harder to figure out a good example with fictional objects...). I mean: instead of calling UpdateTicketStatus(TicketStatus.Closed)` 你应该有一个方法 Close()。我添加了一个例子。
  • 感谢您最后的编辑。这更像是我脑海中的感觉。如果我们要引入继承角度,并说 Ticket 有一个 Status 枚举属性,而 WorkItem 包含一个简单的 bool 来表示已完成。 Ticket.Close() 会做类似 Ticket.Status = Resolved; 的事情吗? WorkItem.Completed = true;证实(); base.Validate();使用单独的验证确保票证的规则以及 WorkItem() 的规则都得到验证; (感谢您也可以在 Ticket.Validate 中调用 base.Validate。
  • 是的,如果 IsCompleted 不能从现有属性的状态中推断出来。同样在这种情况下,我会想象一个Resolve() 方法(在基类中)和一个Close() 在派生类中的方法(其中Close() 将酌情调用Resolve())。我只会在Validate() 内部调用base.Validate(),为什么每次需要时都必须记住这样做?如果您删除基类以支持接口怎么办?
  • 我们将 WorkItem 用于仪表板(例如打开项目的总数),因此更容易在其中使用该布尔值(我们有多个派生类)。忘记调用基本验证会导致问题,因为这意味着 Ticket 和 WorkItem 状态不再同步。无论如何,我已经接受了您的回答,感谢您的彻底回复并回答我的后续问题。
猜你喜欢
  • 1970-01-01
  • 2011-08-27
  • 1970-01-01
  • 1970-01-01
  • 2012-06-16
  • 2017-09-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多