使用违反 Liskov 替换原则的继承将新子类引入已建立的系统
Introducing a new subclass to an established system using inheritance which violates the Liskov Substitution Principle
问题:在将具有现有基本功能的子集的子类引入已建立的继承系统时,是否需要考虑除 Liskov 替换原则之外的任何设计原则?
Context:我们有一个已建立的系统,可以对具有共享基础功能的数十种不同类型的实体进行建模。因此,我们有基础数据库 tables、基础聚合、基础助手、基础验证器等,然后是实体类型特定的 sub类,它继承自基础 类.
我们被要求引入一种新型实体,该实体仅实现此基本功能的一个子集。据我了解,保留基本功能并仅针对新实体覆盖 sub类 中受影响的属性和方法将违反 Liskov 替换原则。然而,彻底检修系统似乎违反直觉:
- 将除新实体类型之外的所有实体类型所需的数据库字段移动到相关的table
- 更改基础聚合、帮助器和验证器实现以仅加载/获取/更新/验证基础数据的子集并在所有实体类型特定聚合中覆盖它(即使它们使用公共适配器或帮助器)
- 等...
仅针对一种新的实体类型,知道所有其他实体类型都需要以前共享的数据库字段和基础实现。
在这种情况下是否可以接受table违反LSP?还有其他设计原则适用于这种情况吗?
详细示例:考虑一个作业管理系统:
Assignment
有多种类型,但都分配了 Person
。
- 它们都有
AssignmentNotes
,这允许将零到多个 Notes
添加到 Assignment
。
AssignmentNotes
还允许将 Assignment
标记为带有可选注释的组 Assignment
,以及 Assignment
中涉及的 Person
=]待识别。
每种分配类型都有聚合、帮助程序和验证程序 类,但由于共享功能,它们分别继承自 BaseAssignmentAggregate
、BaseAssignmentHelper
和 BaseAssignmentValidator
. 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 和所有相关基础 类 上存在这些属性现在是否无效?是否涉及其他设计原则或我现在有义务
- 移动这些属性说一个新的
AssignmentNotesGroupAssignment
数据库 table?
- 将
BaseAssignmentAggregate
更改为不加载此 table 或在其上实现属性,并覆盖所有其他 AssignmentTypeAggregates 来这样做?
- 将
BaseAssigmentAggregate
更改为仅更新注释,并覆盖所有其他 AssignmentTypeAggregates 以更新 AssignmentNotesGroupAssignment
详细信息?
- 等...
只针对一种新的实体类型?
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 并且从不填充 GroupAssignmentComments
或 AssignmentNotesGroupAssignmentIncludedPerson
。毕竟,另一个 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.
问题:在将具有现有基本功能的子集的子类引入已建立的继承系统时,是否需要考虑除 Liskov 替换原则之外的任何设计原则?
Context:我们有一个已建立的系统,可以对具有共享基础功能的数十种不同类型的实体进行建模。因此,我们有基础数据库 tables、基础聚合、基础助手、基础验证器等,然后是实体类型特定的 sub类,它继承自基础 类.
我们被要求引入一种新型实体,该实体仅实现此基本功能的一个子集。据我了解,保留基本功能并仅针对新实体覆盖 sub类 中受影响的属性和方法将违反 Liskov 替换原则。然而,彻底检修系统似乎违反直觉:
- 将除新实体类型之外的所有实体类型所需的数据库字段移动到相关的table
- 更改基础聚合、帮助器和验证器实现以仅加载/获取/更新/验证基础数据的子集并在所有实体类型特定聚合中覆盖它(即使它们使用公共适配器或帮助器)
- 等...
仅针对一种新的实体类型,知道所有其他实体类型都需要以前共享的数据库字段和基础实现。
在这种情况下是否可以接受table违反LSP?还有其他设计原则适用于这种情况吗?
详细示例:考虑一个作业管理系统:
Assignment
有多种类型,但都分配了Person
。- 它们都有
AssignmentNotes
,这允许将零到多个Notes
添加到Assignment
。 AssignmentNotes
还允许将Assignment
标记为带有可选注释的组Assignment
,以及Assignment
中涉及的Person
=]待识别。
每种分配类型都有聚合、帮助程序和验证程序 类,但由于共享功能,它们分别继承自 BaseAssignmentAggregate
、BaseAssignmentHelper
和 BaseAssignmentValidator
. 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 和所有相关基础 类 上存在这些属性现在是否无效?是否涉及其他设计原则或我现在有义务
- 移动这些属性说一个新的
AssignmentNotesGroupAssignment
数据库 table? - 将
BaseAssignmentAggregate
更改为不加载此 table 或在其上实现属性,并覆盖所有其他 AssignmentTypeAggregates 来这样做? - 将
BaseAssigmentAggregate
更改为仅更新注释,并覆盖所有其他 AssignmentTypeAggregates 以更新AssignmentNotesGroupAssignment
详细信息? - 等...
只针对一种新的实体类型?
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 并且从不填充 GroupAssignmentComments
或 AssignmentNotesGroupAssignmentIncludedPerson
。毕竟,另一个 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.