使用继承将一个新的子类引入到已建立的系统中,这违反了Liskov替换原则



问题:在将具有现有基本功能子集的子类引入已建立的继承系统时,除了Liskov替换原则之外,还有其他设计原则需要考虑吗?

上下文:我们已经建立了一个系统,为具有共享基本功能的数十种不同类型的实体建模。因此,我们有基本数据库表、基本聚合、基本助手、基本验证器等,然后是从基类继承的实体类型特定的子类。

我们被要求引入一种新类型的实体,它只实现这个基本功能的子集。据我所知,保留基本功能并仅覆盖新实体的子类中受影响的属性和方法将违反Liskov替换原则。然而,将系统彻底检修为:似乎违反直觉

  • 将除新实体类型外的所有实体类型所需的数据库字段移动到相关表
  • 将基本聚合、助手和验证器实现更改为仅加载/获取/更新/验证基本数据的子集,并在所有实体类型特定的聚合中覆盖它(即使它们使用通用适配器或助手)
  • 等等

只针对一种新的实体类型,知道所有其他实体都需要以前共享的数据库字段和基本实现。

在这种情况下,违反LSP是可以接受的吗?是否有其他适用于这种情况的设计原则?

详细示例:考虑一个分配管理系统:

有许多类型的Assignment,但都分配了一个Person
  • 它们都具有AssignmentNotes,这允许将零到多个Notes添加到Assignment
  • AssignmentNotes还允许Assignment被标记为具有可选注释的组Assignment,并且Assignment中涉及的Person被识别
  • 每个Assignment类型都有Aggregate、Helper和Validator类,但由于共享功能,它们分别继承自BaseAssignmentAggregateBaseAssignmentHelperBaseAssignmentValidatorBaseAssignmentAggregate负责加载和更新共享数据,例如:

    public virtual async Task Load(int id)
    {
    Model = await Context.Assignment.SingleAsync(a => a.Id == id);
    
    await Context.AssignmentNotes.Where(a => a.AssignmentId == id)
    .Include(an => an.AssignmentNotesNote)
    .Include(an => an.AssignmentNotesGroupAssignmentIncludedPerson)
    .LoadAsync();    
    }
    public async Task SaveAssignmentNotes(IReadOnlyCollection<int> noteIds, bool isGroupAssignment, string groupAssignmentComments, IReadOnlyCollection<int> groupAssignmentIncludedPersonIds)
    {
    AssertIsLoaded();
    if (noteIds == null) throw new ArgumentNullException(nameof(noteIds));  
    ...
    // synchronise notes
    ...
    
    Model.AssignmentNotes.IsGroupAssignment = groupAssignmentComments;
    Model.AssignmentNotes.GroupAssignmentComments = groupAssignmentComments;
    
    // synchronise group assignment people
    ...
    // save
    await SaveChanges();
    }
    

    DbContext中的相关实体类型可以如下所示:

    public partial class Assignment
    {
    public int Id { get; set; }    
    public byte AssignmentTypeId { get; set; }    
    public int AssignedPersonId { get; set; }
    
    public virtual Person AssignedPerson { get; set; }    
    public virtual AssignmentNotes AssignmentNotes { get; set; }    
    public virtual AssignmentType AssignmentType { get; set; }        
    public virtual AssignmentTypeADetails AssignmentTypeADetails { get; set; }     
    public virtual AssignmentTypeBDetails AssignmentTypeBDetails { get; set; }     
    public virtual AssignmentTypeCDetails AssignmentTypeCDetails { get; set; }     
    public virtual AssignmentTypeDDetails AssignmentTypeDDetails { get; set; }     
    ...
    }
    public partial class Person
    {
    public Person()
    {            
    Assignment = new HashSet<Assignment>();            
    AssignmentNotesGroupAssignmentIncludedPerson = new HashSet<AssignmentNotesGroupAssignmentIncludedPerson>();            
    }
    public int PersonId { get; set; }        
    public string Name { get; set; }
    
    public virtual ICollection<Assignment> Assignment { get; set; }        
    public virtual ICollection<AssignmentNotesGroupAssignmentIncludedPerson> AssignmentNotesGroupAssignmentIncludedPerson { get; set; }        
    }
    public partial class AssignmentNotes
    {
    public AssignmentNotes()
    {            
    AssignmentNotesNote = new HashSet<AssignmentNotesNote>();
    AssignmentNotesGroupAssignmentIncludedPerson = new HashSet<AssignmentNotesGroupAssignmentIncludedPerson>();  
    }
    public int AssignmentId { get; set; }
    public bool IsGroupAssignment { get; set; }
    public string GroupAssignmentComments { get; set; }
    
    public virtual Assignment Assignment { get; set; }        
    public virtual ICollection<AssignmentNotesNote> AssignmentNotesNote { get; set; }
    public virtual ICollection<AssignmentNotesGroupAssignmentIncludedPerson> AssignmentNotesGroupAssignmentIncludedPerson { get; set; }
    }
    public partial class AssignmentNotesNote
    {
    public int Id { get; set; }
    public int AssignmentId { get; set; }
    public int NoteId { get; set; }
    public virtual Note Note { get; set; }
    public virtual AssignmentNotes Assignment { get; set; }    
    }
    public partial class AssignmentNotesGroupAssignmentIncludedPerson
    {
    public int Id { get; set; }
    public int AssignmentId { get; set; }
    public int PersonId { get; set; }
    public virtual Person Person { get; set; }
    public virtual AssignmentNotes Assignment { get; set; }
    }
    

    现在假设我们被要求引入一个新的AssignmentTypeZ,它永远不可能是组Assignment,即此AssignmentType仍然可以有Notes,但它永远不可以是IsGroupAssignment,它永远不能有GroupAssignmentComments,它永远不会有任何AssignmentNotesGroupAssignmentIncludedPerson

    这些属性现在存在于AssignmentNotes表和所有相关基类中是否无效?是否涉及其他设计原则,或者我现在是否有义务接受

    • 将这些属性转换为新的AssignmentNotesGroupAssignment数据库表
    • BaseAssignmentAggregate更改为不加载此表或在其上实现属性,并覆盖所有其他AssignmentTypeAggregate以执行此操作
    • BaseAssigmentAggregate更改为仅更新Notes,并覆盖所有其他AssignmentTypeAggregate以更新AssignmentNotesGroupAssignment详细信息
    • 等等

    只针对一种新的实体类型?

    这些属性现在存在于AssignmentNotes表和所有相关基类上是否无效?

    我认为在对象和数据之间画一条明显的线很重要。最常见的软件应用程序设计可能是";数据库驱动设计";其中首先设计数据库模式;对象";只是数据库表的表示但是这是一种糟糕的OOP方法,因为数据库表保存数据,而不是对象。表不持久化继承、多态性、封装或任何作为OOP中对象核心的业务逻辑。

    OO应用程序设计应该独立于持久性,因为同一个应用程序可以在NoSQL或RDBMS或平面文件或从网络中提取的JSON数据之上运行。没关系。

    这是一个长篇大论,说LSP不关心AssignmentNotes表。

    当涉及到AssignmentNotes类时,LSP对于总是为IsGroupAssignment返回false并且从不填充GroupAssignmentCommentsAssignmentNotesGroupAssignmentIncludedPerson的子类没有问题。毕竟,另一个子类在同一状态下是有效的,即使它也允许其他状态。

    LSP问题源于此方法签名:SaveAssignmentNotes(IReadOnlyCollection<int> noteIds, bool isGroupAssignment, string groupAssignmentComments, IReadOnlyCollection<int> groupAssignmentIncludedPersonIds)

    即使在添加所提出的子类之前,该方法似乎也有问题,因为isGroupAssignment参数决定了是否可以填充最后两个参数,这意味着该方法已经可以使用无效的参数组合进行调用。

    我会把这个方法一分为二。

    SaveAssignmentNotes(IReadOnlyCollection<int> noteIds)
    SaveAssignmentNotes(IReadOnlyCollection<int> noteIds, string groupAssignmentComments, IReadOnlyCollection<int> groupAssignmentIncludedPersonIds)
    

    理想情况下,这两个方法将在单独的基类中:一个支持组分配,另一个不支持组分配。如果您将它们放在同一个类中,请将第二个记录为可选的,用于支持组的AssignmentNotes

    无论哪种方式,Liskov的解决方案总是清楚地记录前置条件、后置条件和不变量。LSP是一个句法语义原则,这意味着API契约不仅是方法签名,而且是其文档。基类可以定义可选行为(在这种情况下是为了保存"不支持"字段),但它也必须为未实现可选行为的子类定义预期行为,因此客户端不会对该子类感到惊讶。

    相关内容

    • 没有找到相关文章