问题:在将具有现有基本功能子集的子类引入已建立的继承系统时,除了Liskov替换原则之外,还有其他设计原则需要考虑吗?
上下文:我们已经建立了一个系统,为具有共享基本功能的数十种不同类型的实体建模。因此,我们有基本数据库表、基本聚合、基本助手、基本验证器等,然后是从基类继承的实体类型特定的子类。
我们被要求引入一种新类型的实体,它只实现这个基本功能的子集。据我所知,保留基本功能并仅覆盖新实体的子类中受影响的属性和方法将违反Liskov替换原则。然而,将系统彻底检修为:似乎违反直觉
- 将除新实体类型外的所有实体类型所需的数据库字段移动到相关表
- 将基本聚合、助手和验证器实现更改为仅加载/获取/更新/验证基本数据的子集,并在所有实体类型特定的聚合中覆盖它(即使它们使用通用适配器或助手)
- 等等
只针对一种新的实体类型,知道所有其他实体都需要以前共享的数据库字段和基本实现。
在这种情况下,违反LSP是可以接受的吗?是否有其他适用于这种情况的设计原则?
详细示例:考虑一个分配管理系统:
有许多类型的Assignment
,但都分配了一个Person
AssignmentNotes
,这允许将零到多个Notes
添加到Assignment
AssignmentNotes
还允许Assignment
被标记为具有可选注释的组Assignment
,并且Assignment
中涉及的Person
被识别每个Assignment类型都有Aggregate、Helper和Validator类,但由于共享功能,它们分别继承自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
,它永远不可能是组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并且从不填充GroupAssignmentComments
或AssignmentNotesGroupAssignmentIncludedPerson
的子类没有问题。毕竟,另一个子类在同一状态下是有效的,即使它也允许其他状态。
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契约不仅是方法签名,而且是其文档。基类可以定义可选行为(在这种情况下是为了保存"不支持"字段),但它也必须为未实现可选行为的子类定义预期行为,因此客户端不会对该子类感到惊讶。