簡體   English   中英

使用 inheritance 向已建立的系統引入新的子類,這違反了 Liskov 替換原則

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

問題:在將具有現有基本功能的子集的子類引入已建立的 inheritance 系統時,是否還有其他設計原則需要考慮?

上下文:我們有一個已建立的系統,可以對具有共享基礎功能的數十種不同類型的實體進行建模。 因此,我們有基礎數據庫表、基礎聚合、基礎助手、基礎驗證器等,然后是從基類繼承的實體類型特定子類。

我們被要求引入一種僅實現此基本功能的子集的新型實體。 據我了解,保留基本功能並僅覆蓋新實體的子類中受影響的屬性和方法將違反 Liskov 替換原則。 然而,徹底檢修系統似乎違反直覺:

  • 將除新實體類型之外的所有實體類型所需的數據庫字段移動到相關表中
  • 將基礎聚合、助手和驗證器實現更改為僅加載/獲取/更新/驗證基礎數據的子集,並在所有實體類型特定的聚合中覆蓋它(即使它們使用公共適配器或助手)
  • ETC...

僅針對一種新的實體類型,知道所有其他實體類型都需要以前共享的數據庫字段和基本實現。

在這種情況下,違反 LSP 是否可以接受? 還有其他設計原則適用於這種情況嗎?

詳細示例:考慮一個分配管理系統:

  • 有許多類型的Assignment ,但都分配了一個Person
  • 它們都有AssignmentNotes ,允許將零到多個Notes添加到Assignment中。
  • AssignmentNotes還允許將Assignment標記為帶有可選注釋的組Assignment ,並識別參與AssignmentPerson

每種分配類型都有 Aggregate、Helper 和 Validator 類,但由於共享功能,它們分別繼承自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表和所有相關的基類上是不是無效了? 是否涉及其他設計原則,或者我現在有義務

  • 轉移這些屬性說一個新的AssignmentNotesGroupAssignment數據庫表?
  • BaseAssignmentAggregate更改為不加載此表或在其上實現屬性,並覆蓋所有其他 AssignmentTypeAggregates 來這樣做?
  • BaseAssigmentAggregate更改為僅更新 Notes,並覆蓋所有其他 AssignmentTypeAggregates 以更新AssignmentNotesGroupAssignment詳細信息?
  • ETC...

只是為了一種新的實體類型?

這些屬性現在存在於AssignmentNotes表和所有相關的基類上是不是無效了?

我認為在對象和數據之間划清界限很重要。 最常見的軟件應用程序設計可能是“數據庫驅動設計”,其中首先設計數據庫模式,每個“對象”只是一個數據庫表的表示。 然而,這是執行 OOP 的糟糕方法,因為數據庫表保留數據,而不是對象。 表不會持久化 inheritance 或多態性或封裝或作為 OOP 中的 object 核心的任何業務邏輯。

OO 應用程序設計應該獨立於持久性,因為同一個應用程序可以在 NoSQL 或 RDBMS 或平面文件或從 a.network 提取的 JSON 數據之上運行。 沒關系。

這是一個冗長的咆哮,說 LSP 不關心 AssignmentNotes 表。

當涉及到AssignmentNotes class 時,LSP 對於IsGroupAssignment總是返回 false 並且從不填充GroupAssignmentCommentsAssignmentNotesGroupAssignmentIncludedPerson的子類沒有問題。 畢竟,另一個子類在同一個 state 中也是有效的,即使它也允許其他狀態。

LSP 問題源於此方法簽名: SaveAssignmentNotes(IReadOnlyCollection<int> noteIds, bool isGroupAssignment, string groupAssignmentComments, IReadOnlyCollection<int> groupAssignmentIncludedPersonIds)

即使在添加建議的子類之前,該方法似乎也有問題,因為isGroupAssignment參數指示是否可以填充最后兩個參數,這意味着該方法已經可以使用 arguments 的無效組合調用。

我會把這個方法一分為二。

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

理想情況下,這兩種方法應位於不同的基類中:一種支持組分配,另一種不支持。 如果您將它們保留在同一個 class 中,請將第二個記錄為可選,用於支持組的AssignmentNotes

無論哪種方式,Liskov 的解決方案始終是清楚地記錄前置條件、后置條件和不變量。 LSP 是一種句法語義原則,這意味着 API 合同不僅是方法簽名,而且是其文檔。 基礎 class 可以定義可選行為(在本例中用於保存“不受支持”的字段),但它還必須為未實現可選行為的子類定義預期行為,因此客戶不會對該子類感到驚訝。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM