使用违反 Liskov 替换原则的继承将新子类引入已建立的系统

Introducing a new subclass to an established system using inheritance which violates the Liskov Substitution Principle

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

Context:我们有一个已建立的系统,可以对具有共享基础功能的数十种不同类型的实体进行建模。因此,我们有基础数据库 tables、基础聚合、基础助手、基础验证器等,然后是实体类型特定的 sub类,它继承自基础 类.

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

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

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

详细示例:考虑一个作业管理系统:

每种分配类型都有聚合、帮助程序和验证程序 类,但由于共享功能,它们分别继承自 BaseAssignmentAggregateBaseAssignmentHelperBaseAssignmentValidator . BaseAssignmentAggregate 负责加载和更新共享数据,例如:

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,它永远不能是一个组作业,即这个 AssignmentType 仍然可以有注释,但它永远不能是 IsGroupAssignment,它永远不能有 GroupAssignmentComments 并且它永远不会有任何 AssignmentNotesGroupAssignmentIncludedPerson.

AssignmentNotes table 和所有相关基础 类 上存在这些属性现在是否无效?是否涉及其他设计原则或我现在有义务

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

Is it now invalid for these properties to exist on the AssignmentNotes table and all related base classes?

我认为在对象和数据之间划清界限很重要。最常见的软件应用程序设计可能是“database-driven-design”,其中首先设计数据库模式,每个“对象”只是一个数据库的表示 table。 但是 这是执行 OOP 的糟糕方法,因为数据库 table 保存的是数据,而不是对象。表不保留继承或多态性或封装或作为 OOP 中对象核心的任何业务逻辑。

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

这是一个 long-winded 咆哮,说 LSP 不关心 AssignmentNotes table。

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

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

甚至在添加提议的 subclass 之前,该方法似乎有问题,因为 isGroupAssignment 参数指示是否可以填充最后两个参数,这意味着该方法已经可以调用无效的参数组合。

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

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

理想情况下,这两种方法应该在不同的基础 classes 中:一种支持组分配,另一种不支持。如果您将它们放在相同的 class 中,请将第二个记录为可选,因为 AssignmentNotes 支持组。

无论哪种方式,Liskov 的解决方案始终是清楚地记录前提条件、后置条件和不变量。 LSP 是句法 语义原则,这意味着 API 契约不仅是方法签名,而且是其文档。基础 class 可以定义可选行为(在本例中用于保存“不受支持”的字段),但它还必须为未实现可选行为的子 class 定义预期行为,因此客户端不会对那个子感到惊讶class.