Abstrak Kelas Dasar dengan Antarmuka sebagai Perilaku?

14

Saya perlu merancang hierarki kelas untuk proyek C # saya. Pada dasarnya, fungsi kelas mirip dengan kelas WinForms jadi mari kita ambil toolkit WinForms sebagai contoh. (Namun, saya tidak bisa menggunakan WinForms atau WPF.)

Ada beberapa sifat dan fungsi inti yang perlu disediakan oleh setiap kelas. Dimensi, posisi, warna, visibilitas (benar / salah), metode Draw, dll.

Saya memerlukan saran desain. Saya telah menggunakan desain dengan kelas dasar abstrak dan antarmuka yang tidak benar-benar tipe tetapi lebih seperti perilaku. Apakah ini desain yang bagus? Jika tidak, apa yang akan menjadi desain yang lebih baik.

Kode ini terlihat seperti ini:

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();
}

Beberapa Kontrol dapat berisi Kontrol lainnya, beberapa hanya dapat berisi (sebagai anak-anak) jadi saya berpikir untuk membuat dua antarmuka untuk fungsi-fungsi ini:

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 mengontrol tampilan teks sehingga ini juga masuk ke antarmuka:

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

    void DrawText();
}

Beberapa Kontrol dapat dimasukkan ke dalam Kontrol induknya sehingga:

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

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

... dan sekarang mari kita buat beberapa kelas konkret:

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

"Masalah" pertama yang bisa saya pikirkan di sini adalah bahwa antarmuka pada dasarnya diatur setelah mereka dipublikasikan. Tetapi mari kita asumsikan saya akan dapat membuat antarmuka saya cukup baik untuk menghindari kebutuhan membuat perubahan pada mereka di masa depan.

Masalah lain yang saya lihat dalam desain ini adalah bahwa setiap kelas ini perlu mengimplementasikan antarmuka itu dan duplikasi kode akan segera terjadi. Misalnya dalam Label dan Tombol metode DrawText () berasal dari antarmuka ITextHolder atau di setiap kelas yang berasal dari manajemen anak IContainer.

Solusi saya untuk masalah ini adalah mengimplementasikan fungsionalitas "duplikat" ini dalam adaptor khusus dan meneruskan panggilan kepada mereka. Jadi baik Label dan Tombol akan memiliki anggota TextHolderAdapter yang akan disebut metode di dalam yang diwarisi dari antarmuka ITextHolder.

Saya pikir desain ini harus melindungi saya dari keharusan memiliki banyak fungsi umum di kelas dasar yang dapat dengan cepat membengkak dengan metode virtual dan "kode noise" yang tidak perlu. Perubahan perilaku akan dilakukan dengan memperluas adaptor dan bukan kelas yang diturunkan dari Kontrol.

Saya pikir ini disebut pola "Strategi" dan meskipun ada jutaan pertanyaan dan jawaban tentang topik itu, saya ingin meminta pendapat Anda tentang apa yang saya pertimbangkan untuk desain ini dan kekurangan apa yang dapat Anda pikirkan di pendekatan saya.

Saya harus menambahkan bahwa ada kemungkinan hampir 100% bahwa persyaratan di masa depan akan membutuhkan kelas baru dan fungsi baru.

grapkulec
sumber
Mengapa tidak mewarisi dari System.ComponentModel.Componentatau System.Windows.Forms.Controldari kelas dasar yang ada? Mengapa Anda perlu membuat hierarki kontrol Anda sendiri dan mendefinisikan semua fungsi ini lagi dari awal?
Cody Gray
3
IChildsepertinya nama yang mengerikan.
Raynos
@Cody: pertama, saya tidak bisa menggunakan WinForms atau rakitan WPF, kedua - ayolah, itu hanya contoh masalah desain yang saya tanyakan. Jika lebih mudah bagi Anda memikirkan bentuk atau binatang. kelas saya perlu berperilaku mirip dengan kontrol WinForms tetapi tidak dalam segala hal dan tidak persis seperti mereka
grapkulec
2
Tidak masuk akal untuk mengatakan bahwa Anda tidak dapat menggunakan perangkat WinForms atau WPF, namun Anda akan dapat menggunakan kerangka kontrol kustom apa pun yang Anda buat. Ini adalah kasus serius penemuan kembali roda, dan saya tidak bisa membayangkan untuk tujuan apa. Tetapi jika Anda benar-benar harus, mengapa tidak mengikuti contoh kontrol WinForms? Itu membuat pertanyaan ini cukup usang.
Cody Grey
1
@ graphkulec itu sebabnya ini adalah komentar :) Jika saya memiliki umpan balik yang berguna sebenarnya untuk memberi saya akan menjawab pertanyaan Anda. Itu masih nama yang mengerikan;)
Raynos

Jawaban:

5

[1] Tambahkan "getter" & "setter" virtual ke properti Anda, saya harus meretas pustaka kontrol lain, karena saya memerlukan fitur ini:

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

Saya tahu saran ini lebih "verbose" atau kompleks, tetapi, sangat berguna di dunia nyata.

[2] Tambahkan properti "IsEnabled", jangan bingung dengan "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

Berarti Anda harus menunjukkan kendali Anda, tetapi jangan perlihatkan informasi apa pun.

[3] Tambahkan properti "IsReadOnly", jangan bingung dengan "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

Itu berarti kontrol dapat menampilkan informasi, tetapi tidak dapat diubah oleh pengguna.

umlcat
sumber
walaupun saya tidak suka semua metode virtual ini di kelas dasar saya akan memberi mereka kesempatan kedua dalam pemikiran saya tentang desain. dan Anda mengingatkan saya bahwa saya benar-benar membutuhkan properti IsReadOnly :)
grapkulec
@ grapkulec Karena ini adalah kelas dasar, Anda harus menentukan bahwa kelas turunan akan memiliki properti-properti itu, tetapi, bahwa metode yang mengontrol perilaku dapat diubah pada setiap kelas purpouse ;-)
umlcat
mmkey, saya mengerti maksud Anda dan terima kasih telah meluangkan waktu untuk menjawab
grapkulec
1
Saya menerima ini sebagai jawaban karena begitu saya mulai mengimplementasikan desain "keren" saya dengan antarmuka, saya dengan cepat menemukan diri saya membutuhkan setter virtual dan terkadang getter juga. Itu tidak berarti bahwa saya tidak menggunakan antarmuka sama sekali saya hanya membatasi penggunaannya ke tempat-tempat di mana mereka benar-benar diperlukan.
grapkulec
3

Pertanyaan bagus! Hal pertama yang saya perhatikan adalah bahwa metode menggambar tidak memiliki parameter, di mana itu menarik? Saya pikir itu harus menerima beberapa objek Surface atau Graphics sebagai parameter (sebagai antarmuka tentu saja).

Satu hal lagi yang saya temukan aneh adalah antarmuka IContainer. Ini memiliki GetChildmetode yang mengembalikan satu anak dan tidak memiliki parameter - mungkin ia harus mengembalikan koleksi atau jika Anda memindahkan metode Draw ke antarmuka, maka ia dapat mengimplementasikan antarmuka ini dan memiliki metode menggambar yang dapat menarik koleksi anak internal tanpa memaparkannya. .

Mungkin untuk ide Anda bisa melihat WPF juga - itu adalah kerangka kerja yang dirancang dengan sangat baik menurut saya. Anda juga dapat melihat pola desain Komposisi dan Dekorator. Yang pertama mungkin berguna untuk elemen kontainer dan yang kedua untuk menambahkan fungsionalitas ke elemen. Saya tidak bisa mengatakan seberapa baik itu akan bekerja pada tampilan pertama, tapi inilah yang saya pikirkan:

Untuk menghindari duplikasi, Anda dapat memiliki sesuatu seperti elemen primitif - seperti elemen teks. Kemudian Anda dapat membangun elemen yang lebih rumit menggunakan primitif ini. Sebagai contoh, a Borderadalah elemen yang memiliki anak tunggal. Ketika Anda memanggil Drawmetodenya, ia menggambar perbatasan dan latar belakang lalu menarik anaknya di dalam perbatasan. SEBUAHTextElement hanya menggambar beberapa teks. Sekarang jika Anda menginginkan sebuah tombol, Anda dapat membangun satu dengan menyusun dua primitif itu, yaitu meletakkan teks di dalam Border. Di sini Border adalah sesuatu seperti Dekorator.

Postingnya agak panjang jadi beri tahu saya jika Anda menemukan ide itu menarik dan saya bisa memberikan beberapa contoh atau lebih banyak penjelasan.

Georgi Stoyanov
sumber
1
param yang hilang bukan masalah, itu hanya sketsa metode nyata setelah semua. Mengenai WPF saya telah merancang sistem tata letak berdasarkan ide mereka jadi saya pikir saya sudah memiliki bagian komposisi, karena untuk dekorator saya harus memikirkannya. ide Anda tentang elemen primitif terdengar masuk akal dan saya pasti mencobanya. Terima kasih!
grapkulec
@ grapkulec Sama-sama! Tentang params - ya, tidak apa-apa, saya hanya ingin memastikan saya memahami intensitas Anda dengan benar. Saya senang Anda menyukai ide itu. Semoga berhasil!
2

Memotong kode dan menuju ke inti pertanyaan Anda "Apakah desain saya dengan kelas dasar abstrak dan antarmuka bukan sebagai tipe tetapi lebih karena perilaku adalah desain yang baik atau tidak.", Saya akan mengatakan tidak ada yang salah dengan pendekatan itu.

Sebenarnya saya telah menggunakan pendekatan seperti itu (di mesin rendering saya) di mana setiap antarmuka mendefinisikan perilaku subsistem yang dikonsumsi. Misalnya, IUpdateable, ICollidable, IRenderable, ILoadable, ILoader dan sebagainya dan sebagainya. Untuk lebih jelasnya, saya juga memisahkan masing-masing "perilaku" ini ke dalam kelas parsial yang terpisah seperti "Entity.IUpdateable.cs", "Entity.IRenderable.cs" dan mencoba untuk menjaga bidang dan metode sel independen mungkin.

Pendekatan menggunakan antarmuka untuk mendefinisikan pola perilaku juga setuju dengan saya karena parameter tipe generik, kendala dan co (ntra) bekerja sangat baik bersama.

Salah satu hal yang saya amati tentang metode desain ini adalah: Jika Anda mendapati diri Anda mendefinisikan antarmuka dengan lebih dari beberapa metode, Anda mungkin tidak menerapkan perilaku.

Ani
sumber
apakah Anda mengalami masalah atau pada suatu saat Anda menyesali desain seperti itu dan harus berkelahi dengan kode Anda sendiri untuk benar-benar menyelesaikan sesuatu yang dimaksudkan? seperti misalnya tiba-tiba ternyata Anda perlu membuat begitu banyak implementasi perilaku yang tidak masuk akal dan Anda berharap Anda tetap berpegang pada tema lama "membuat kelas dasar dengan jutaan virtual dan hanya mewarisi dan menimpanya keluar dari itu"?
grapkulec