คลาสฐานนามธรรมพร้อมอินเทอร์เฟซเป็นพฤติกรรม


14

ฉันต้องออกแบบลำดับชั้นของชั้นเรียนสำหรับโครงการ C # ของฉัน โดยพื้นฐานแล้วฟังก์ชันของคลาสนั้นคล้ายกับคลาสของ WinForms ดังนั้นให้ลองใช้ชุดเครื่องมือ WinForms เป็นตัวอย่าง (อย่างไรก็ตามฉันไม่สามารถใช้ WinForms หรือ WPF ได้)

มีคุณสมบัติและฟังก์ชั่นหลักบางอย่างที่ทุกชั้นต้องการ ขนาดตำแหน่งสีการมองเห็น (จริง / เท็จ) วิธีการวาด ฯลฯ

ฉันต้องการคำแนะนำด้านการออกแบบฉันใช้การออกแบบที่มีคลาสเบสและอินเทอร์เฟซที่เป็นนามธรรมซึ่งไม่ใช่ประเภทจริง ๆ แต่ชอบพฤติกรรมมากกว่า นี่เป็นการออกแบบที่ดีหรือไม่? ถ้าไม่สิ่งที่จะเป็นการออกแบบที่ดีกว่า

รหัสมีลักษณะดังนี้:

abstract class Control
{
    public int Width { get; set; }

    public int Height { get; set; }

    public int BackColor { get; set; }

    public int X { get; set; }

    public int Y { get; set; }

    public int BorderWidth { get; set; }

    public int BorderColor { get; set; }

    public bool Visible { get; set; }

    public Rectangle ClipRectange { get; protected set; }

    abstract public void Draw();
}

การควบคุมบางอย่างสามารถมีการควบคุมอื่น ๆ บางอย่างสามารถมี (เป็นเด็ก ๆ ) ดังนั้นฉันจึงคิดที่จะสร้างสองอินเตอร์เฟสสำหรับฟังก์ชันเหล่านี้:

interface IChild
{
    IContainer Parent { get; set; }
}

internal interface IContainer
{
    void AddChild<T>(T child) where T : IChild;
    void RemoveChild<T>(T child) where T : IChild;
    IChild GetChild(int index);
}

WinForms ควบคุมข้อความที่แสดงดังนั้นสิ่งนี้จะเข้าสู่อินเตอร์เฟส:

interface ITextHolder
{
    string Text { get; set; }
    int TextPositionX { get; set; }
    int TextPositionY { get; set; }
    int TextWidth { get; }
    int TextHeight { get; }

    void DrawText();
}

การควบคุมบางอย่างสามารถเชื่อมต่อภายในการควบคุมหลักของพวกเขาดังนั้น:

enum Docking
{ 
    None, Left, Right, Top, Bottom, Fill
}

interface IDockable
{
    Docking Dock { get; set; }
}

... และตอนนี้เรามาสร้างคลาสที่เป็นรูปธรรมกันบ้าง:

class Panel : Control, IDockable, IContainer, IChild {}
class Label : Control, IDockable, IChild, ITextHolder {}
class Button : Control, IChild, ITextHolder, IDockable {}
class Window : Control, IContainer, IDockable {}

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

อีกปัญหาที่ฉันเห็นในการออกแบบนี้คือทุกคลาสเหล่านี้จะต้องใช้อินเทอร์เฟซของมันและการทำซ้ำของรหัสจะเกิดขึ้นอย่างรวดเร็ว ตัวอย่างเช่นในฉลากและปุ่มวิธี DrawText () มาจากอินเทอร์เฟซ ITextHolder หรือในทุก ๆ คลาสที่ได้รับจากการจัดการ IContainer ของลูก

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

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

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

ฉันควรเพิ่มว่ามีโอกาสเกือบ 100% ที่ความต้องการในอนาคตจะเรียกชั้นเรียนใหม่และฟังก์ชั่นใหม่


ทำไมไม่เพียง แต่สืบทอดจากSystem.ComponentModel.ComponentหรือSystem.Windows.Forms.Controlหรือจากคลาสพื้นฐานอื่น ๆ ที่มีอยู่? ทำไมคุณต้องสร้างลำดับชั้นการควบคุมของคุณเองและกำหนดหน้าที่ทั้งหมดเหล่านี้อีกครั้งตั้งแต่เริ่มต้น?
Cody Gray

3
IChildดูเหมือนชื่อที่น่ากลัว
Raynos

@Cody: ก่อนอื่นฉันไม่สามารถใช้แอสเซมบลี WinForms หรือ WPF สอง - มาเลยมันเป็นเพียงตัวอย่างของปัญหาการออกแบบที่ฉันถาม ถ้ามันง่ายกว่าที่คุณจะคิดรูปร่างหรือสัตว์ ชั้นเรียนของฉันจำเป็นต้องประพฤติตนคล้ายกับการควบคุม WinForms แต่ไม่ได้อยู่ในทุกด้านและไม่เหมือนกับพวกเขาอย่างแน่นอน
grapkulec

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

1
@graphkulec นั่นเป็นเหตุผลว่าทำไมมันถึงเป็นความคิดเห็น :) ถ้าฉันมีข้อเสนอแนะที่เป็นประโยชน์จริง ๆ เพื่อให้ฉันจะได้ตอบคำถามของคุณ ยังคงเป็นชื่อที่น่ากลัว;)
Raynos

คำตอบ:


5

[1] เพิ่ม"getters" & "setters" เสมือนในคุณสมบัติของคุณฉันต้องแฮกห้องสมุดควบคุมอื่นเพราะฉันต้องการคุณสมบัตินี้:

abstract class Control
{
    // internal fields for properties
    protected int _Width;
    protected int _Height;
    protected int _BackColor;
    protected int _X;
    protected int _Y;
    protected bool Visible;

    protected int BorderWidth;
    protected int BorderColor;


    // getters & setters virtual !!!

    public virtual int getWidth(...) { ... }
    public virtual void setWidth(...) { ... }

    public virtual int getHeight(...) { ... }
    public virtual void setHeight(...) { ... }

    public virtual int getBackColor(...) { ... }
    public virtual void setBackColor(...) { ... }

    public virtual int getX(...) { ... }
    public virtual void setX(...) { ... }

    public virtual int getY(...) { ... }
    public virtual void setY(...) { ... }

    public virtual int getBorderWidth(...) { ... }
    public virtual void setBorderWidth(...) { ... }

    public virtual int getBorderColor(...) { ... }
    public virtual void setBorderColor(...) { ... }

    public virtual bool getVisible(...) { ... }
    public virtual void setVisible(...) { ... }

    // properties WITH virtual getters & setters

    public int Width { get getWidth(); set setWidth(value); }

    public int Height { get getHeight(); set setHeight(value); }

    public int BackColor { get getBackColor(); set setBackColor(value); }

    public int X { get getX(); set setX(value); }

    public int Y { get getY(); set setY(value); }

    public int BorderWidth { get getBorderWidth(); set setBorderWidth(value); }

    public int BorderColor { get getBorderColor(); set setBorderColor(value); }

    public bool Visible { get getVisible(); set setVisible(value); }

    // other methods

    public Rectangle ClipRectange { get; protected set; }   
    abstract public void Draw();
} // class Control

/* concrete */ class MyControl: Control
{
    public override bool getVisible(...) { ... }
    public override void setVisible(...) { ... }
} // class MyControl: Control

ฉันรู้ว่าคำแนะนำนี้เป็น "verbose" หรือซับซ้อนมากขึ้น แต่มันมีประโยชน์มากในโลกแห่งความเป็นจริง

[2] เพิ่มคุณสมบัติ "IsEnabled" อย่าสับสนกับ "IsReadOnly":

abstract class Control
{
    // internal fields for properties
    protected bool _IsEnabled;

    public virtual bool getIsEnabled(...) { ... }
    public virtual void setIsEnabled(...) { ... }

    public bool IsEnabled{ get getIsEnabled(); set setIsEnabled(value); }
} // class Control

หมายถึงคุณอาจต้องแสดงการควบคุมของคุณ แต่ไม่ต้องแสดงข้อมูลใด ๆ

[3] เพิ่มคุณสมบัติ "IsReadOnly" อย่าสับสนกับ "IsEnabled":

abstract class Control
{
    // internal fields for properties
    protected bool _IsReadOnly;

    public virtual bool getIsReadOnly(...) { ... }
    public virtual void setIsReadOnly(...) { ... }

    public bool IsReadOnly{ get getIsReadOnly(); set setIsReadOnly(value); }
} // class Control

นั่นหมายถึงการควบคุมสามารถแสดงข้อมูล แต่ผู้ใช้ไม่สามารถเปลี่ยนแปลงได้


แม้ว่าฉันจะไม่ชอบวิธีเสมือนเหล่านี้ทั้งหมดในคลาสพื้นฐานฉันจะให้โอกาสครั้งที่สองแก่พวกเขาในการคิดเกี่ยวกับการออกแบบ และคุณเตือนฉันว่าฉันต้องการอสังหาริมทรัพย์ IsReadOnly :)
grapkulec

@grapkulec เนื่องจากเป็นคลาสพื้นฐานคุณต้องระบุว่าคลาสที่ได้รับจะมีคุณสมบัติเหล่านั้น แต่วิธีการที่พฤติกรรมการควบคุมสามารถเปลี่ยนแปลงได้ในแต่ละคลาส purpouse ;-)
umlcat

mmkey ฉันเห็นประเด็นของคุณและขอบคุณที่สละเวลาตอบ
grapkulec

1
ฉันยอมรับคำตอบนี้เป็นคำตอบเพราะเมื่อฉันเริ่มใช้การออกแบบ "เท่" กับอินเทอร์เฟซฉันพบว่าตัวเองต้องการ virtual setters และบางครั้ง getters เช่นกัน ไม่ได้หมายความว่าฉันไม่ได้ใช้อินเทอร์เฟซเลยฉัน จำกัด การใช้งานเฉพาะสถานที่ที่พวกเขาต้องการจริงๆ
grapkulec

3

เป็นคำถามที่ดี! สิ่งแรกที่ฉันสังเกตได้คือวิธีการวาดไม่มีพารามิเตอร์ใด ๆ มันจะวาดที่ไหน? ฉันคิดว่ามันควรจะได้รับวัตถุ Surface หรือ Graphics เป็นพารามิเตอร์ (เป็นส่วนต่อประสานของหลักสูตร)

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

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

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

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


1
params ที่ขาดหายไปไม่ใช่ปัญหามันเป็นเพียงแค่ภาพร่างของวิธีการจริงหลังจากทั้งหมด เกี่ยวกับ WPF ฉันได้ออกแบบระบบเลย์เอาต์ตามความคิดของพวกเขาดังนั้นฉันคิดว่าฉันมีส่วนองค์ประกอบอยู่แล้วในฐานะผู้ตกแต่งฉันจะต้องคิดเกี่ยวกับมัน ความคิดของคุณเกี่ยวกับองค์ประกอบดั้งเดิมนั้นฟังดูสมเหตุสมผลและฉันก็ลองทำดู ขอบคุณ!
grapkulec

@grapkulec ยินดีต้อนรับ! เกี่ยวกับ params - ใช่มันโอเคฉันแค่อยากให้แน่ใจว่าฉันเข้าใจความตั้งใจของคุณอย่างถูกต้อง ฉันดีใจที่คุณชอบความคิด โชคดี!

2

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

ในความเป็นจริงฉันได้ใช้วิธีการดังกล่าว (ในเครื่องมือการแสดงผลของฉัน) ที่แต่ละอินเตอร์เฟสกำหนดพฤติกรรมที่ระบบย่อยการบริโภคคาดว่าจะได้ ตัวอย่างเช่น IUpdateable, ICollidable, IRenderable, ILoadable, ILoader และอื่น ๆ เป็นต้น เพื่อความชัดเจนฉันยังแยก "พฤติกรรม" เหล่านี้ออกเป็นคลาสย่อยบางส่วนเช่น "Entity.IUpdateable.cs", "Entity.IRenderable.cs" และพยายามรักษาฟิลด์และวิธีการต่างๆให้เป็นอิสระมากที่สุด

วิธีการใช้อินเทอร์เฟซเพื่อกำหนดรูปแบบพฤติกรรมยังเห็นด้วยกับฉันเพราะ generics, ข้อ จำกัด และพารามิเตอร์ประเภทตัวแปร co (ntra) ทำงานร่วมกันได้ดีมาก

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


คุณพบปัญหาหรือไม่ในขณะที่คุณรู้สึกเสียใจกับการออกแบบดังกล่าวและต้องต่อสู้กับรหัสของคุณเองเพื่อให้ได้สิ่งที่ตั้งใจทำจริง ๆ ? ตัวอย่างเช่นทันใดนั้นปรากฎว่าคุณต้องสร้างพฤติกรรมการใช้งานหลายอย่างที่มันไม่สมเหตุสมผลและคุณต้องการให้คุณยึดติดกับธีมเก่า "สร้างคลาสพื้นฐานที่มีเสมือนล้านเป็นล้าน
grapkulec
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.