ทำไมสืบทอดคลาสและไม่เพิ่มคุณสมบัติ?


39

ฉันพบต้นไม้มรดกในฐานรหัส (ค่อนข้างใหญ่) ของเราที่ไปสิ่งนี้:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class OrderDateInfo : NamedEntity { }

จากสิ่งที่ฉันสามารถรวบรวมได้สิ่งนี้มักใช้เพื่อผูกสิ่งต่าง ๆ ไว้ที่ส่วนหน้า

NamedEntityสำหรับฉันนี้จะทำให้ความรู้สึกที่จะให้ชื่อเป็นรูปธรรมในชั้นเรียนแทนการอาศัยทั่วไป ในทางกลับกันมีจำนวนคลาสดังกล่าวที่ไม่มีคุณสมบัติเพิ่มเติม

มีข้อเสียสำหรับวิธีนี้หรือไม่?


25
อัพไซด์ไม่ได้กล่าวถึง: คุณสามารถแยกแยะวิธีที่เกี่ยวข้องกับOrderDateInfos จากที่เกี่ยวข้องกับNamedEntitys
Caleth

8
ด้วยเหตุผลเดียวกับที่ถ้าคุณเกิดขึ้นให้พูดIdentifierคลาสที่ไม่มีส่วนเกี่ยวข้องNamedEntityแต่ต้องมีทั้งคุณสมบัติIdและNameคุณสมบัติคุณจะไม่ใช้NamedEntityแทน บริบทและการใช้งานที่เหมาะสมของคลาสเป็นมากกว่าคุณสมบัติและวิธีการที่เก็บไว้
Neil

คำตอบ:


15

สำหรับฉันมันเหมาะสมแล้วที่จะให้ชื่อที่เป็นรูปธรรมกับชั้นเรียนแทนที่จะพึ่งพา NamedEntity ทั่วไป ในทางกลับกันมีจำนวนคลาสดังกล่าวที่ไม่มีคุณสมบัติเพิ่มเติม

มีข้อเสียสำหรับวิธีนี้หรือไม่?

วิธีการไม่ได้เลวร้าย แต่มีวิธีแก้ปัญหาที่ดีกว่า กล่าวโดยสรุปอินเตอร์เฟสจะเป็นทางออกที่ดีกว่าสำหรับสิ่งนี้ เหตุผลหลักที่อินเทอร์เฟซและการสืบทอดแตกต่างกันที่นี่เป็นเพราะคุณสามารถสืบทอดจากคลาสเดียวเท่านั้น แต่คุณสามารถใช้อินเทอร์เฟซมากมาย

ตัวอย่างเช่นพิจารณาว่าคุณได้ตั้งชื่อเอนทิตีและเอนทิตีที่ตรวจสอบแล้ว คุณมีหลายหน่วยงาน:

Oneไม่ใช่นิติบุคคลที่ผ่านการตรวจสอบหรือเป็นนิติบุคคลที่มีชื่อ ง่ายมาก:

public class One 
{ }

Twoเป็นนิติบุคคลที่มีชื่อ แต่ไม่ใช่นิติบุคคลที่ตรวจสอบแล้ว นั่นคือสิ่งที่คุณมีอยู่ตอนนี้:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Two : NamedEntity 
{ }

Threeเป็นทั้งรายการที่ระบุชื่อและตรวจสอบแล้ว นี่คือที่ที่คุณพบปัญหา คุณสามารถสร้างAuditedEntityชั้นฐาน แต่คุณไม่สามารถทำให้Threeเป็นมรดกทั้งสอง AuditedEntity และ NamedEntity :

public class AuditedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : NamedEntity, AuditedEntity  // <-- Compiler error!
{ }

อย่างไรก็ตามคุณอาจคิดถึงวิธีแก้ปัญหาด้วยการAuditedEntityรับช่วงNamedEntityต่อ นี่เป็นแฮ็คที่ฉลาดเพื่อให้แน่ใจว่าทุก ๆ คลาสจะต้องสืบทอด (โดยตรง) จากคลาสอื่น

public class AuditedEntity : NamedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : AuditedEntity
{ }

ยังคงใช้งานได้ แต่สิ่งที่คุณได้ทำที่นี่คือที่ระบุไว้ว่าทุกกิจการที่ตรวจสอบแล้วเป็นอย่างโดยเนื้อแท้ยังเป็นชื่อนิติบุคคล ซึ่งนำฉันไปเป็นตัวอย่างสุดท้ายของฉัน Fourเป็นนิติบุคคลที่ตรวจสอบแล้ว แต่ไม่ได้เป็นนิติบุคคลที่มีชื่อ แต่คุณไม่สามารถปล่อยให้FourสืบทอดจากAuditedEntityที่คุณจะได้รับNamedEntityเนื่องจากมรดกระหว่าง AuditedEntity andNamedEntity`

การใช้การสืบทอดไม่มีวิธีในการทำทั้งสองอย่างThreeและFourทำงานนอกเสียจากคุณจะเริ่มคลาสที่ซ้ำกัน (ซึ่งเปิดชุดปัญหาใหม่ทั้งหมด)

การใช้อินเทอร์เฟซสามารถทำได้อย่างง่ายดาย:

public interface INamedEntity
{
    int Id { get; set; }
    string Name { get; set; }
}

public interface IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class One 
{ }

public class Two : INamedEntity 
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Three : INamedEntity, IAuditedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class Four : IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

ข้อเสียเปรียบเล็กน้อยเพียงอย่างเดียวที่นี่คือคุณยังต้องใช้อินเทอร์เฟซ แต่คุณจะได้รับประโยชน์ทั้งหมดจากการมีชนิดที่นำกลับมาใช้ซ้ำได้โดยไม่ต้องมีข้อเสียใด ๆ ที่เกิดขึ้นเมื่อคุณต้องการรูปแบบที่หลากหลายในประเภททั่วไปสำหรับเอนทิตีที่กำหนด

แต่ความแตกต่างของคุณยังคงเหมือนเดิม:

var one = new One();
var two = new Two();
var three = new Three();
var four = new Four();

public void HandleNamedEntity(INamedEntity namedEntity) {}
public void HandleAuditedEntity(IAuditedEntity auditedEntity) {}

HandleNamedEntity(one);    //Error - not a named entity
HandleNamedEntity(two);
HandleNamedEntity(three);  
HandleNamedEntity(four);   //Error - not a named entity

HandleAuditedEntity(one);    //Error - not an audited entity
HandleAuditedEntity(two);    //Error - not an audited entity
HandleAuditedEntity(three);  
HandleAuditedEntity(four);

ในทางกลับกันมีจำนวนคลาสดังกล่าวที่ไม่มีคุณสมบัติเพิ่มเติม

นี่คือรูปแบบที่เปลี่ยนแปลงในรูปแบบอินเทอร์เฟซของตัวทำเครื่องหมายซึ่งคุณใช้อินเทอร์เฟซที่ว่างเปล่าเพื่อให้สามารถใช้ชนิดของอินเทอร์เฟซเพื่อตรวจสอบว่าคลาสที่กำหนดเป็น "ทำเครื่องหมาย" ด้วยอินเทอร์เฟซนี้
คุณกำลังใช้คลาสที่สืบทอดมาแทนที่จะใช้อินเตอร์เฟส แต่เป้าหมายนั้นเหมือนกันดังนั้นฉันจะเรียกมันว่าเป็น "คลาสที่ทำเครื่องหมาย"

ตามมูลค่าแล้วไม่มีอะไรผิดปกติกับอินเทอร์เฟซ / คลาสของตัวทำเครื่องหมาย พวกเขาเป็นไวยากรณ์และเทคนิคที่ถูกต้องและไม่มีข้อบกพร่องโดยธรรมชาติเพื่อใช้พวกเขาให้เครื่องหมายเป็นความจริงในระดับสากล (ที่รวบรวมเวลา) และไม่ได้มีเงื่อนไข

นี่คือวิธีที่คุณควรแยกความแตกต่างระหว่างข้อยกเว้นต่าง ๆ แม้ว่าข้อยกเว้นเหล่านั้นจะไม่มีคุณสมบัติ / วิธีการเพิ่มเติมใด ๆ เมื่อเปรียบเทียบกับวิธีการพื้นฐาน

ดังนั้นจึงไม่มีอะไรผิดปกติกับการทำเช่นนั้น แต่ฉันจะแนะนำให้ใช้อย่างระมัดระวังเพื่อให้แน่ใจว่าคุณไม่เพียงแค่พยายามปกปิดข้อผิดพลาดทางสถาปัตยกรรมที่มีอยู่ด้วยความหลากหลายที่ออกแบบมาไม่ดี


4
"คุณสามารถสืบทอดได้จากคลาสเดียวเท่านั้น แต่คุณสามารถใช้หลายอินเตอร์เฟสได้" นั่นไม่จริงสำหรับทุกภาษา บางภาษาอนุญาตการสืบทอดหลายคลาส
Kenneth K.

ดังที่ Kenneth กล่าวว่าภาษายอดนิยมสองภาษามีความสามารถในการสืบทอดหลายภาษา C ++ และ Python ชั้นเรียนthreeในตัวอย่างข้อผิดพลาดที่ไม่ได้ใช้อินเทอร์เฟซของคุณนั้นใช้งานได้จริงใน C ++ ตัวอย่างเช่นในความเป็นจริงสิ่งนี้ทำงานได้อย่างถูกต้องสำหรับตัวอย่างทั้งสี่ ในความเป็นจริงทั้งหมดนี้ดูเหมือนว่าจะมีข้อ จำกัด ของ C # เป็นภาษามากกว่านิทานเตือนไม่ให้ใช้มรดกเมื่อคุณควรจะใช้อินเตอร์เฟซ
opa

63

นี่คือสิ่งที่ฉันใช้เพื่อป้องกันความแตกต่างจากการใช้งาน

สมมติว่าคุณมี 15 ชั้นเรียนที่แตกต่างกันที่มีเป็นบางชั้นฐานในห่วงโซ่มรดกของพวกเขาและคุณจะเขียนวิธีการใหม่ที่เป็นเพียงที่ใช้บังคับกับ NamedEntityOrderDateInfo

คุณ "ทำได้" เพียงแค่เขียนลายเซ็นเป็น

void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)

และความหวังและการภาวนาก็ไม่มีใครใช้ระบบพิมพ์แบบFooBazNamedEntityผิด ๆ

หรือคุณ "สามารถ" void MyMethod(OrderDateInfo foo)เพียงเขียน ตอนนี้คอมไพเลอร์บังคับใช้แล้ว เรียบง่ายสง่างามและไม่พึ่งพาคนที่ไม่ทำผิดพลาด

นอกจากนี้ตามที่ @candied_orange ชี้ให้เห็นว่าข้อยกเว้นเป็นกรณีที่ยอดเยี่ยมสำหรับเรื่องนี้ ไม่ค่อยมาก (และฉันหมายถึงมากมาก ๆ น้อยมาก) คุณเคยต้องการจับทุกสิ่งด้วยcatch (Exception e)หรือไม่ มีแนวโน้มที่คุณต้องการที่จะจับSqlExceptionหรือFileNotFoundExceptionหรือข้อยกเว้นที่กำหนดเองสำหรับแอพลิเคชันของคุณ คลาสเหล่านั้นบ่อยครั้งไม่ได้ให้ข้อมูลหรือฟังก์ชันใด ๆ มากกว่าExceptionคลาสพื้นฐานแต่อนุญาตให้คุณแยกแยะสิ่งที่พวกเขาแทนโดยไม่ต้องตรวจสอบและตรวจสอบฟิลด์ประเภทหรือค้นหาข้อความที่ต้องการ

โดยรวมแล้วมันเป็นเคล็ดลับที่จะทำให้ระบบการพิมพ์อนุญาตให้คุณใช้ชุดประเภทที่แคบกว่าที่เป็นไปได้ถ้าคุณใช้คลาสพื้นฐาน ฉันหมายความว่าคุณสามารถกำหนดตัวแปรและข้อโต้แย้งทั้งหมดของคุณว่ามีประเภทObjectแต่นั่นจะทำให้งานของคุณยากขึ้นใช่มั้ย


4
ให้อภัยความแปลกใหม่ของฉัน แต่ฉันอยากรู้ว่าทำไมมันจะไม่เป็นวิธีที่ดีกว่าที่จะทำให้ OrderDateInfo มีวัตถุ NamedEntity เป็นคุณสมบัติหรือไม่
อดัม B

9
@ AdamB นั่นเป็นตัวเลือกการออกแบบที่ถูกต้อง ไม่ว่าจะดีกว่าหรือไม่ก็ขึ้นอยู่กับว่ามันจะใช้เพื่ออะไร ในกรณียกเว้นฉันไม่สามารถสร้างคลาสใหม่ที่มีคุณสมบัติข้อยกเว้นเพราะภาษา (C # สำหรับฉัน) จะไม่ให้ฉันโยนทิ้ง อาจมีข้อควรพิจารณาในการออกแบบอื่น ๆ ที่จะขัดขวางการใช้องค์ประกอบแทนที่จะเป็นมรดก องค์ประกอบหลายครั้งดีกว่า มันขึ้นอยู่กับระบบของคุณ
Becuzz

17
@ AdamB ด้วยเหตุผลเดียวกับที่เมื่อคุณสืบทอดจากคลาสฐานและทำการเพิ่มเมธอดหรือคุณสมบัติที่ฐานขาดคุณจะไม่สร้างคลาสใหม่และวางเบสแรกเป็นคุณสมบัติ มีความแตกต่างระหว่างมนุษย์เป็นสัตว์เลี้ยงลูกด้วยนมและมีสัตว์เลี้ยงลูกด้วยนม
Logarr

8
@Logarr จริงมีความแตกต่างระหว่างฉันเป็นสัตว์เลี้ยงลูกด้วยนมและมีสัตว์เลี้ยงลูกด้วยนม แต่ถ้าฉันอยู่กับสัตว์เลี้ยงลูกด้วยนมภายในของฉันคุณจะไม่มีทางรู้ถึงความแตกต่าง คุณอาจสงสัยว่าทำไมฉันสามารถให้นมหลายชนิดในราชประสงค์
candied_orange

5
@ AdamB คุณสามารถทำสิ่งนี้ได้และเป็นที่รู้จักกันในชื่อ แต่คุณจะเปลี่ยนชื่อNamedEntityสำหรับสิ่งนี้เช่นเพื่อEntityNameให้องค์ประกอบที่เหมาะสมเมื่ออธิบาย
Sebastian Redl

28

นี่คือการใช้มรดกที่ชื่นชอบ ฉันใช้เป็นส่วนใหญ่สำหรับข้อยกเว้นที่อาจใช้ชื่อได้ดีกว่าเฉพาะเจาะจงมากขึ้น

ปัญหาปกติของการสืบทอดที่นำไปสู่โซ่ยาวและทำให้เกิดปัญหาโยโย่ไม่ได้ใช้ที่นี่เนื่องจากไม่มีสิ่งใดที่จะกระตุ้นให้คุณโซ่


1
อืมยกเว้นจุดดี ฉันจะบอกว่ามันเป็นสิ่งที่น่ากลัวที่จะทำ แต่คุณได้ชักชวนฉันเป็นอย่างอื่นด้วยกรณีการใช้งานที่ยอดเยี่ยม
David Arno

Yo-Yo Ho และเหล้ารัมหนึ่งขวด นี่คือ orlop ที่หายาก มันรั่วไหลบ่อยครั้งเพราะต้องการไม้โอ๊คทวีที่เหมาะสม ไปที่ตู้เก็บของ Davey Jones พร้อมที่ดินที่พวกเขาสร้างขึ้นมา การแปล: มันเป็นเรื่องยากที่ห่วงโซ่การสืบทอดนั้นดีมากจนคลาสฐานสามารถถูกละเว้นได้อย่างมีประสิทธิภาพ / เป็นนามธรรม
Radarbob

ฉันคิดว่ามันคุ้มค่าที่จะชี้ให้เห็นว่าคลาสใหม่ที่สืบทอดจากคลาสอื่นจะเพิ่มคุณสมบัติใหม่ - ชื่อของคลาสใหม่ นี่คือสิ่งที่คุณใช้เมื่อสร้างข้อยกเว้นที่มีชื่อที่ดีกว่า
รถกระบะ Logan

1

IMHO การออกแบบชั้นเรียนไม่ถูกต้อง มันควรจะเป็น.

public class EntityName
{
    public int Id { get; set; }
    public string Text { get; set; }
}

public class OrderDateInfo
{
    public EntityName Name { get; set; }
}

OrderDateInfoHAS A Nameเป็นความสัมพันธ์ที่เป็นธรรมชาติมากขึ้นและสร้างคลาสที่เข้าใจง่ายสองคลาสซึ่งจะไม่ทำให้เกิดคำถามเดิมขึ้นมา

วิธีการใด ๆ ที่ยอมรับNamedEntityว่าเป็นพารามิเตอร์ควรสนใจIdและNameคุณสมบัติเท่านั้นดังนั้นจึงควรเปลี่ยนวิธีการดังกล่าวเพื่อยอมรับEntityNameแทน

เหตุผลทางเทคนิคเพียงอย่างเดียวที่ฉันยอมรับสำหรับการออกแบบดั้งเดิมมีไว้สำหรับการเชื่อมโยงคุณสมบัติซึ่ง OP ระบุไว้ object.Name.Idกรอบอึจะไม่สามารถที่จะรับมือกับคุณสมบัติพิเศษและผูกไว้กับ แต่ถ้ากรอบการผูกมัดของคุณไม่สามารถรับมือกับสิ่งนั้นได้คุณก็จะมีหนี้เทคเพิ่มขึ้นในรายการ

ฉันจะไปพร้อมกับคำตอบของ @ Flater แต่ด้วยอินเทอร์เฟซจำนวนมากที่มีคุณสมบัติคุณจบลงด้วยการเขียนโค้ดสำเร็จรูปจำนวนมากแม้ว่าจะมีคุณสมบัติอัตโนมัติที่น่ารักของ C # ลองนึกภาพทำใน Java!


1

คลาสเปิดเผยพฤติกรรมและโครงสร้างข้อมูลเปิดเผยข้อมูล

ฉันเห็นคำหลักของชั้นเรียน แต่ฉันไม่เห็นพฤติกรรมใด ๆ หมายความว่าฉันจะเริ่มดูคลาสนี้เป็นโครงสร้างข้อมูล ในหลอดเลือดดำนี้ฉันจะใช้ถ้อยคำของคุณใหม่เป็น

ทำไมมีโครงสร้างข้อมูลระดับบนสุดทั่วไป?

ดังนั้นคุณสามารถใช้ชนิดข้อมูลระดับสูงสุด สิ่งนี้อนุญาตให้ใช้ประโยชน์จากระบบประเภทเพื่อให้แน่ใจว่านโยบายข้ามชุดข้อมูลโครงสร้างขนาดใหญ่ที่แตกต่างกันโดยมั่นใจว่าคุณสมบัติทั้งหมดอยู่ที่นั่น

ทำไมมีโครงสร้างข้อมูลที่รวมถึงระดับบนสุด แต่เพิ่มอะไรเลย

ดังนั้นคุณสามารถใช้ประเภทข้อมูลระดับล่าง สิ่งนี้อนุญาตให้ใส่คำแนะนำในระบบการพิมพ์เพื่อแสดงวัตถุประสงค์ของตัวแปร

Top level data structure - Named
   property: name;

Bottom level data structure - Person

ในลำดับชั้นด้านบนเราพบว่าสะดวกในการระบุว่าชื่อบุคคลดังนั้นผู้คนสามารถรับและแก้ไขชื่อได้ แม้ว่ามันอาจจะสมเหตุสมผลในการเพิ่มคุณสมบัติพิเศษให้กับPersonโครงสร้างข้อมูลปัญหาที่เราแก้ไขไม่จำเป็นต้องมีการสร้างแบบจำลองที่สมบูรณ์แบบของบุคคลดังนั้นเราจึงละเลยที่จะเพิ่มคุณสมบัติทั่วไปเช่นageฯลฯ

ดังนั้นจึงเป็นการใช้ประโยชน์จากระบบการพิมพ์เพื่อแสดงเจตจำนงของรายการที่มีชื่อในลักษณะที่ไม่ทำลายด้วยการอัปเดต (เช่นเอกสารประกอบ) และสามารถขยายได้ในภายหลังด้วยความง่ายดาย (ถ้าคุณพบว่าคุณต้องการageฟิลด์ในภายหลัง )

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.