Kapan harus menghentikan warisan?

9

Sekali waktu yang lalu saya mengajukan pertanyaan tentang Stack Overflow tentang warisan.

Saya telah mengatakan saya merancang mesin catur dalam mode OOP. Jadi saya mewarisi semua karya saya dari kelas abstrak Piece tetapi warisan tetap berjalan. Biarkan saya tunjukkan dengan kode

public abstract class Piece
{
    public void MakeMove();
    public void TakeBackMove();
}

public abstract class Pawn: Piece {}

public class WhitePawn :Pawn {}

public class BlackPawn:Pawn {}

Programmer telah menemukan desain saya sedikit di atas teknik dan menyarankan untuk menghapus kelas potongan berwarna dan tahan warna sepotong sebagai anggota properti seperti di bawah ini.

public abstract class Piece
{
    public Color Color { get; set; }
    public abstract void MakeMove();
    public abstract void TakeBackMove();
}

Dengan cara ini Piece bisa tahu warnanya. Setelah implementasi saya melihat implementasi saya berjalan seperti di bawah ini.

public abstract class Pawn: Piece
{
    public override void MakeMove()
    {
        if (this.Color == Color.White)
        {

        }
        else
        {

        }
    }

    public override void TakeBackMove()
    {
        if (this.Color == Color.White)
        {

        }
        else
        {

        }
    }
}

Sekarang saya melihat bahwa properti color keep menyebabkan pernyataan if dalam implementasi. Itu membuat saya merasa kami membutuhkan potongan warna khusus dalam hierarki warisan.

Dalam kasus seperti itu, apakah Anda memiliki kelas seperti WhitePawn, BlackPawn atau Anda akan mendesain dengan mempertahankan properti Color?

Tanpa melihat masalah seperti itu bagaimana Anda ingin memulai desain? Menyimpan properti Color atau memiliki solusi pewarisan?

Sunting: Saya ingin menentukan bahwa contoh saya mungkin tidak sepenuhnya cocok dengan contoh kehidupan nyata. Jadi sebelum mencoba menebak detail implementasi, konsentrasikan saja pertanyaannya.

Saya sebenarnya hanya bertanya apakah menggunakan properti Warna akan menyebabkan apakah pernyataan lebih baik untuk menggunakan warisan?

Darah segar
sumber
3
Saya tidak begitu mengerti mengapa Anda memodelkan potongan-potongan seperti ini sama sekali. Saat Anda memanggil MakeMove () langkah apa yang akan dilakukan? Saya akan mengatakan apa yang Anda benar-benar perlu mulai dengan memodelkan keadaan papan dan melihat kelas apa yang Anda butuhkan untuk itu
jk.
11
Saya pikir kesalahpahaman dasar Anda adalah tidak menyadari bahwa dalam "warna" catur adalah atribut pemain yang sebenarnya daripada potongan. Itu sebabnya kami mengatakan "kulit hitam bergerak" dll, pewarnaan ada untuk mengidentifikasi pemain mana yang memiliki bagian itu. Sebuah pion mengikuti aturan dan batasan gerakan yang sama tidak peduli apa warnanya, satu-satunya variasi adalah arah mana yang "maju".
James Anderson
5
ketika Anda tidak dapat menemukan pewarisan "kapan harus berhenti", lebih baik lepaskan saja. Anda dapat memperkenalkan kembali warisan pada tahap desain / implementasi / pemeliharaan nanti, saat hierarki menjadi jelas bagi Anda. Saya menggunakan pendekatan ini, bekerja seperti pesona
nyamuk
1
Masalah yang lebih menantang adalah apakah akan membuat sub-kelas untuk setiap jenis barang atau tidak. Tipe Sepotong menentukan aspek perilaku lebih dari warna sepotong.
NoChance
3
Jika Anda tergoda untuk memulai desain dengan ABC (Abstract Base Classes), tolong beri kami semua dan jangan ... Kasus-kasus di mana ABC sebenarnya berguna sangat jarang dan bahkan kemudian, biasanya lebih berguna untuk menggunakan antarmuka sebagai gantinya.
Evan Plaice

Jawaban:

12

Bayangkan Anda merancang aplikasi yang berisi kendaraan. Apakah Anda membuat sesuatu seperti ini?

class BlackVehicle : Vehicle { }
class DarkBlueVehicle : Vehicle { }
class DarkGreenVehicle : Vehicle { }
// hundreds of other classes...

Dalam kehidupan nyata, warna adalah properti dari objek (baik mobil atau bidak catur), dan harus diwakili oleh properti.

Apakah MakeMovedan TakeBackMoveberbeda untuk orang kulit hitam dan kulit putih? Tidak sama sekali: peraturannya sama untuk kedua pemain, yang berarti bahwa kedua metode itu akan sama persis untuk orang kulit hitam dan kulit putih , yang berarti Anda menambahkan kondisi di mana Anda tidak membutuhkannya.

Di sisi lain, jika Anda punya WhitePawndan BlackPawn, saya tidak akan terkejut jika cepat atau lambat Anda akan menulis:

var piece = (Pawn)this.CurrentPiece;
if (piece is WhitePawn)
{
}
else // The pice is of type BlackPawn
{
}

atau bahkan sesuatu seperti:

public abstract class Pawn
{
    public Color Player
    {
        get
        {
            return this is WhitePawn ? Color.White : Color.Black;
        }
    }
}

// Now imagine doing the same thing for every other piece.
Arseni Mourzenko
sumber
4
@Freshblood Saya pikir akan lebih baik untuk memiliki directionproperti dan menggunakannya untuk bergerak daripada menggunakan properti warna. Warna idealnya hanya digunakan untuk rendering, bukan untuk logika.
Gyan alias Gary Buyn
7
Selain itu, potongan-potongan itu bergerak ke arah yang sama, maju, mundur, kiri, kanan. Perbedaannya adalah dari sisi papan mana mereka mulai. Di jalan, mobil di jalur yang berlawanan keduanya bergerak maju.
slipset
1
@ Blrfl Anda mungkin benar bahwa directionproperti itu boolean. Saya tidak melihat apa yang salah dengan itu. Yang membingungkan saya tentang pertanyaan sampai komentar Freshblood di atas adalah mengapa ia ingin mengubah logika gerakan berdasarkan warna. Saya akan mengatakan itu lebih sulit untuk dipahami karena tidak masuk akal untuk warna mempengaruhi perilaku. Jika yang ingin Anda lakukan adalah membedakan pemain mana yang memiliki bagian yang Anda dapat menempatkannya dalam dua koleksi terpisah dan menghindari kebutuhan akan properti atau warisan. Potongan-potongan itu sendiri bisa sangat tidak menyadari pemain mana yang mereka miliki.
Gyan alias Gary Buyn
1
Saya pikir kita melihat hal yang sama dari dua perspektif berbeda. Jika kedua sisi berwarna merah dan biru, apakah perilaku mereka akan berbeda ketika mereka hitam dan putih? Saya akan mengatakan tidak, yang merupakan alasan saya mengatakan warna bukan penyebab perbedaan perilaku. Itu hanya perbedaan halus tapi itu sebabnya saya akan menggunakan directionatau mungkin playerproperti daripada yang colorada di logika permainan.
Gyan alias Gary Buyn
1
@Freshblood white castle's moves differ from black's- bagaimana? Apakah maksud Anda castling? Baik King-side castling dan Queen-side castling 100% simetris untuk Hitam Putih.
Konrad Morawski
4

Yang harus Anda tanyakan pada diri sendiri adalah mengapa Anda benar-benar membutuhkan pernyataan itu di kelas Gadai Anda:

    if (this.Color == Color.White)
    {

    }
    else
    {
    }

Saya cukup yakin itu akan cukup untuk memiliki seperangkat fungsi kecil seperti

public abstract class Piece
{
    bool DoesPieceHaveSameColor(Piece otherPiece) { /*...*/   }

    Color OtherColor{get{/*...*/}}
    // ...
}

di kelas Piece Anda dan terapkan semua fungsi di Pawn(dan derivasi lain dari Piece) menggunakan fungsi-fungsi tersebut, tanpa harus memiliki ifpernyataan di atas. Hanya pengecualian yang berfungsi untuk menentukan arah gerakan untuk Gadai berdasarkan warnanya, karena ini khusus untuk pion, tetapi dalam hampir semua kasus lain Anda tidak memerlukan kode khusus warna.

Pertanyaan menarik lainnya adalah apakah Anda benar-benar harus memiliki subclass yang berbeda untuk berbagai jenis potongan sama sekali. Itu tampak masuk akal dan alami bagi saya, karena aturan untuk gerakan untuk setiap bagian berbeda dan Anda pasti dapat menerapkan ini dengan menggunakan fungsi kelebihan beban yang berbeda untuk setiap jenis bagian.

Tentu saja, orang bisa mencoba model ini dengan cara yang lebih generik, dengan memisahkan properti dari potongan-potongan dari aturan bergerak mereka, tetapi ini mungkin berakhir di kelas abstrak MovingRuledan hirarki subclass MovingRulePawn, MovingRuleQueen... saya tidak akan memulainya bahwa cara, karena bisa dengan mudah mengarah ke bentuk lain dari rekayasa berlebihan.

Doc Brown
sumber
+1 untuk menunjukkan bahwa kode khusus warna kemungkinan tidak-tidak. Pion putih dan hitam pada dasarnya berperilaku sama, sama seperti semua pion lainnya.
Konrad Morawski
2

Warisan tidak berguna ketika ekstensi tidak memengaruhi kemampuan eksternal objek .

Properti Color tidak memengaruhi cara objek Piece digunakan . Sebagai contoh, semua bagian dapat menggunakan metode moveForward atau moveBackwards (bahkan jika langkah tersebut tidak sah), tetapi logika enkapsulasi Piece menggunakan properti Color untuk menentukan nilai absolut yang mewakili gerakan maju atau mundur (-1 atau + 1 pada "sumbu y"), sejauh menyangkut API Anda, superclass dan subkelas Anda adalah objek yang identik (karena semuanya memperlihatkan metode yang sama).

Secara pribadi, saya akan mendefinisikan beberapa konstanta yang mewakili setiap jenis bagian, kemudian menggunakan pernyataan beralih untuk bercabang ke metode pribadi yang mengembalikan kemungkinan gerakan untuk contoh "ini".

Leo
sumber
Gerakan mundur hanya ilegal jika digadaikan. Dan mereka tidak benar-benar harus tahu apakah mereka hitam atau putih - itu cukup (dan logis) untuk membuat mereka membedakan ke depan vs ke belakang. The Boardkelas (misalnya) bisa bertanggung jawab untuk mengubah konsep ini menjadi -1 atau +1 tergantung pada warna dari potongan.
Konrad Morawski
0

Secara pribadi saya tidak akan mendapatkan apa pun dari Piecesemuanya, karena saya pikir Anda tidak mendapatkan apa-apa dari melakukan itu. Agaknya sepotong tidak benar-benar peduli bagaimana bergerak, hanya kotak yang merupakan tujuan yang valid untuk langkah selanjutnya.

Mungkin Anda bisa membuat Kalkulator Bergerak Valid yang mengetahui pola pergerakan yang tersedia untuk jenis keping, dan dapat mengambil daftar kotak tujuan yang valid, misalnya

interface IMoveCalculator
{
    List<Square> GetValidDestinations();
}

class KnightMoveCalculator : IMoveCalculator;
class QueenMoveCalculator : IMoveCalculator;
class PawnMoveCalculator : IMoveCalculator; 
// etc.

class Piece
{
    IMoveCalculator move_calculator;
public:
    Piece(IMoveCalculator mc) : move_calculator(mc) {}
}
Ben Cottrell
sumber
Tapi siapa yang menentukan bagian mana yang mendapatkan kalkulator bergerak? Jika Anda memiliki kelas dasar piece, dan memiliki PawnPiece, Anda dapat membuat asumsi itu. Tetapi kemudian pada tingkat itu, karya itu sendiri hanya bisa menerapkan bagaimana bergerak, seperti yang dirancang dalam pertanyaan asli
Steven Striga
@WeekendWarrior - "Secara pribadi saya tidak akan mewarisi apa pun dari Piecesama sekali". Tampaknya Piece bertanggung jawab atas lebih dari sekadar menghitung gerakan, yang merupakan alasan untuk memisahkan gerakan sebagai masalah terpisah. Menentukan bagian mana yang mendapatkan kalkulator mana yang merupakan masalah ketergantungan injeksi, misalnyavar my_knight = new Piece(new KnightMoveCalculator())
Ben Cottrell
Ini akan menjadi kandidat yang bagus untuk pola bobot terbang. Ada 16 pion di papan catur dan semuanya memiliki perilaku yang sama . Perilaku ini dapat dengan mudah diekstraksi menjadi satu kelas terbang. Hal yang sama berlaku untuk semua bagian lainnya.
MattDavey
-2

Dalam jawaban ini, saya berasumsi bahwa dengan "mesin catur", penulis pertanyaan berarti "catur AI", bukan hanya mesin validasi bergerak.

Saya pikir menggunakan OOP untuk bagian dan gerakan valid mereka adalah ide yang buruk karena dua alasan:

  1. Kekuatan mesin catur sangat bergantung pada seberapa cepat ia dapat memanipulasi papan, lihat misalnya representasi papan program Catur . Mengingat tingkat optimasi yang dijelaskan artikel ini, saya tidak berpikir panggilan fungsi biasa terjangkau. Panggilan ke metode virtual akan menambah tingkat tipuan dan mendatangkan hukuman kinerja yang lebih tinggi. Biaya OOP tidak terjangkau di sana.
  2. Manfaat OOP seperti ekstensibilitas tidak berguna untuk kepingan, karena catur tidak akan mendapatkan kepingan baru dalam waktu dekat. Alternatif yang layak untuk hierarki tipe berbasis-warisan termasuk menggunakan enum dan sakelar. Teknologi sangat rendah dan seperti C, tetapi sulit dikalahkan dalam hal kinerja.

Beranjak dari pertimbangan kinerja, termasuk warna bagian dalam hirarki jenis menurut saya adalah ide yang buruk. Anda akan menemukan diri Anda dalam situasi di mana Anda perlu memeriksa apakah dua potong memiliki warna yang berbeda, misalnya untuk menangkap. Memeriksa kondisi ini mudah dilakukan menggunakan properti. Melakukannya menggunakan is WhitePiece-test akan lebih buruk dalam perbandingan.

Ada juga alasan praktis lain untuk menghindari perpindahan generasi yang bergerak ke metode khusus, yaitu bagian yang berbeda memiliki gerakan yang sama, misalnya benteng dan ratu. Untuk menghindari kode duplikat, Anda harus memfaktisasi kode umum menjadi metode dasar, dan melakukan panggilan mahal lainnya.

Yang sedang berkata, jika itu mesin catur pertama yang Anda tulis, saya pasti akan menyarankan untuk pergi dengan prinsip pemrograman bersih dan menghindari trik optimasi yang dijelaskan oleh penulis Crafty. Berurusan dengan aturan catur yang spesifik (castling, promotion, en-passant) dan teknik AI yang kompleks (hashing board, alpha-beta cut) cukup menantang.

Joh
sumber
1
Mungkin tidak bertujuan untuk menulis mesin catur terlalu cepat.
Freshblood
5
-1. Penilaian seperti "secara hipotesis itu bisa menjadi masalah kinerja, jadi itu buruk", apakah IMHO hampir selalu murni takhayul. Dan warisan bukan hanya tentang "ekstensibilitas" dalam arti yang Anda sebutkan. Aturan catur yang berbeda berlaku untuk bagian yang berbeda, dan setiap jenis permainan memiliki implementasi yang berbeda dari metode seperti WhichMovesArePossibleadalah pendekatan yang masuk akal.
Doc Brown
2
Jika Anda tidak memerlukan ekstensibilitas, pendekatan yang didasarkan pada sakelar / enum lebih disukai karena lebih cepat daripada metode pengiriman virtual. Mesin catur adalah CPU-berat, dan operasi yang paling sering dilakukan (dan karenanya efisiensi kritis) melakukan gerakan dan membatalkannya. Hanya satu tingkat di atas yang menampilkan daftar semua kemungkinan gerakan. Saya berharap ini juga akan menjadi waktu kritis. Faktanya adalah, saya telah memprogram mesin catur di C. Bahkan tanpa OOP, optimasi tingkat rendah pada representasi papan adalah signifikan. Pengalaman seperti apa yang Anda miliki untuk memberhentikan pengalaman saya?
Joh
2
@ Jo: Saya tidak akan tidak menghargai pengalaman Anda, tapi saya cukup yakin bukan itu yang diinginkan OP. Dan meskipun optimasi tingkat rendah mungkin penting untuk beberapa masalah, saya menganggapnya sebagai kesalahan untuk menjadikannya sebagai dasar untuk keputusan desain tingkat tinggi - terutama ketika seseorang tidak menghadapi masalah kinerja sejauh ini. Pengalaman saya adalah bahwa Knuth sangat benar ketika mengatakan "Optimalisasi prematur adalah akar dari semua kejahatan."
Doc Brown
1
-1 Saya harus setuju dengan Doc. Faktanya adalah, "mesin" yang dibicarakan OP ini bisa menjadi mesin validasi untuk permainan catur 2 pemain. Dalam hal itu, berpikir tentang kinerja OOP vs gaya lain benar-benar menggelikan, validasi sederhana dari momen akan mengambil milidetik bahkan pada perangkat ARM low-end. Jangan membaca lebih lanjut persyaratan yang sebenarnya.
Timothy Baldridge