Kode apa yang harus dimasukkan dalam kelas abstrak?

10

Saya bermasalah akhir-akhir ini tentang penggunaan kelas abstrak.

Terkadang kelas abstrak dibuat sebelumnya dan berfungsi sebagai templat bagaimana kelas turunan akan bekerja. Itu berarti, lebih atau kurang, bahwa mereka menyediakan beberapa fungsionalitas tingkat tinggi tetapi meninggalkan detail tertentu untuk diimplementasikan oleh kelas turunan. Kelas abstrak mendefinisikan perlunya rincian ini dengan menerapkan beberapa metode abstrak. Dalam kasus seperti itu, kelas abstrak berfungsi seperti cetak biru, deskripsi fungsi tingkat tinggi atau apa pun yang Anda ingin menyebutnya. Itu tidak dapat digunakan sendiri, tetapi harus khusus untuk mendefinisikan rincian yang telah ditinggalkan dari implementasi tingkat tinggi.

Kadang-kadang, kebetulan bahwa kelas abstrak dibuat setelah penciptaan beberapa kelas "turunan" (karena kelas induk / abstrak belum ada, mereka belum diturunkan, tetapi Anda tahu maksud saya). Dalam kasus ini, kelas abstrak biasanya digunakan sebagai tempat di mana Anda dapat meletakkan segala jenis kode umum yang berisi kelas turunan saat ini.

Setelah melakukan pengamatan di atas, saya bertanya-tanya yang mana dari dua kasus ini yang seharusnya menjadi aturan. Haruskah setiap jenis rincian digelembungkan ke kelas abstrak hanya karena mereka saat ini menjadi umum di semua kelas turunan? Haruskah kode umum yang bukan bagian dari fungsionalitas tingkat tinggi ada di sana?

Haruskah kode yang mungkin tidak memiliki makna untuk kelas abstrak itu sendiri ada di sana hanya karena kebetulan umum untuk kelas turunan?

Biarkan saya memberi contoh: Kelas abstrak A memiliki metode a () dan metode abstrak aq (). Metode aq (), di kedua kelas turunan AB dan AC, menggunakan metode b (). Haruskah b () dipindahkan ke A? Jika ya, maka jika seseorang hanya melihat A (mari kita berpura-pura AB dan AC tidak ada di sana), keberadaan b () tidak masuk akal! Apakah ini hal yang buruk? Haruskah seseorang dapat melihat kelas abstrak dan memahami apa yang terjadi tanpa mengunjungi kelas turunannya?

Sejujurnya, pada saat menanyakan ini, saya cenderung percaya bahwa menulis kelas abstrak yang masuk akal tanpa harus melihat di kelas turunannya adalah masalah kode bersih dan arsitektur bersih. Ι tidak terlalu menyukai ide kelas abstrak yang bertindak seperti dump untuk semua jenis kode yang biasa terjadi di semua kelas turunan.

Apa yang Anda pikirkan / praktikkan?

Alexandros Gougousis
sumber
7
Mengapa harus ada "aturan?"
Robert Harvey
1
Writing an abstract class that makes sense without having to look in the derived classes is a matter of clean code and clean architecture.- Kenapa? Apakah tidak mungkin bahwa, dalam proses mendesain dan mengembangkan aplikasi, saya menemukan bahwa beberapa kelas memiliki fungsi umum yang secara alami dapat di refactored menjadi kelas abstrak? Haruskah saya cukup waskita untuk selalu mengantisipasi ini sebelum menulis kode untuk kelas turunan? Jika saya gagal menjadi lihai ini, apakah saya dilarang melakukan refactoring seperti itu? Haruskah saya membuang kode dan memulai kembali?
Robert Harvey
Maaf, jika saya salah paham! Saya mencoba mengatakan apa yang saya rasakan (untuk saat ini) sebagai praktik yang lebih baik dan tidak menyiratkan bahwa harus ada aturan absolut. Selain itu, saya tidak menyiratkan bahwa kode apa pun yang termasuk dalam kelas abstrak harus ditulis terlebih dahulu. Saya menjelaskan bagaimana, dalam praktiknya, kelas abstrak berakhir dengan kode tingkat tinggi (yang bertindak sebagai templat untuk yang diturunkan) serta kode tingkat rendah (bahwa Anda tidak dapat memahami kegunaannya karena Anda tidak melihat ke dalam kelas turunan).
Alexandros Gougousis
@RobertHarvey untuk kelas basis publik, Anda akan dilarang melihat kelas turunan. Untuk kelas internal, tidak ada bedanya.
Frank Hileman

Jawaban:

4

Biarkan saya memberi contoh: Kelas abstrak A memiliki metode a () dan metode abstrak aq (). Metode aq (), di kedua kelas turunan AB dan AC, menggunakan metode b (). Haruskah b () dipindahkan ke A? Jika ya, maka jika seseorang hanya melihat A (mari kita berpura-pura AB dan AC tidak ada di sana), keberadaan b () tidak masuk akal! Apakah ini hal yang buruk? Haruskah seseorang dapat melihat kelas abstrak dan memahami apa yang terjadi tanpa mengunjungi kelas turunannya?

Apa yang Anda tanyakan adalah di mana harus menempatkan b(), dan, dalam beberapa hal lain pertanyaan adalah apakah Apilihan terbaik sebagai kelas super langsung untuk ABdan AC.

Tampaknya ada tiga pilihan:

  1. biarkan b()keduanya ABdanAC
  2. membuat kelas menengah ABAC-Parentyang mewarisi dari Adan yang memperkenalkan b(), dan kemudian digunakan sebagai kelas super langsung untuk ABdanAC
  3. menempatkan b()di A(tidak tahu apakah kelas masa lain ADakan ingin b()atau tidak)

  1. menderita karena tidak KERING .
  2. menderita YAGNI .
  3. jadi, yang tersisa ini.

Sampai kelas lain ADyang tidak menginginkan b()hadiah itu sendiri, (3) sepertinya pilihan yang tepat.

Pada saat ADhadiah, maka kita dapat menolak untuk pendekatan dalam (2) - setelah semua itu adalah perangkat lunak!

Erik Eidt
sumber
7
Ada opsi ke-4. Jangan masukkan b()kelas apa pun. Jadikan itu fungsi bebas yang mengambil semua datanya sebagai argumen dan memiliki keduanya ABdan ACmenyebutnya. Ini berarti Anda tidak perlu memindahkannya atau membuat kelas lagi ketika Anda menambahkan AD.
user1118321
1
@ user1118321, sangat baik, pemikiran yang bagus di luar kotak. Masuk akal khususnya jika b()tidak memerlukan keadaan instance-berumur panjang.
Erik Eidt
Apa yang mengganggu saya dalam (3) adalah bahwa kelas abstrak tidak menggambarkan lagi sepotong perilaku mandiri. Ini adalah potongan-potongan kode dan saya tidak dapat memahami kode kelas abstrak tanpa bolak-balik antara ini dan semua kelas turunan.
Alexandros Gougousis
Bisakah Anda membuat basis / default acyang memanggil balih-alih acabstrak?
Erik Eidt
3
Bagi saya, pertanyaan yang paling penting adalah, MENGAPA AB dan AC keduanya menggunakan metode yang sama b (). Jika itu hanya kebetulan, saya akan meninggalkan dua implementasi serupa di AB dan AC. Tapi mungkin itu karena ada beberapa abstraksi umum untuk AB dan AC. Bagi saya, KERING bukanlah nilai dalam dirinya sendiri, tetapi sebuah petunjuk bahwa Anda mungkin melewatkan beberapa abstraksi yang bermanfaat.
Ralf Kleberhoff
2

Kelas abstrak tidak dimaksudkan sebagai tempat pembuangan untuk berbagai fungsi atau data yang dilemparkan ke kelas abstrak karena nyaman.

Satu aturan praktis yang tampaknya memberikan pendekatan berorientasi objek yang paling dapat diandalkan dan diperluas adalah " Lebih suka komposisi daripada warisan ." Kelas abstrak mungkin dianggap sebagai spesifikasi antarmuka yang tidak mengandung kode apa pun.

Jika Anda memiliki beberapa metode yang merupakan jenis metode pustaka yang sejalan dengan kelas abstrak, metode yang merupakan cara yang paling mungkin untuk mengekspresikan beberapa fungsionalitas atau perilaku yang biasanya diperlukan oleh kelas yang berasal dari kelas abstrak, maka masuk akal untuk membuat kelas di antara kelas abstrak dan kelas-kelas lain di mana metode ini tersedia. Kelas baru ini menyediakan contoh khusus dari kelas abstrak yang mendefinisikan alternatif atau jalur implementasi tertentu dengan menyediakan metode ini.

Gagasan kelas abstrak adalah untuk memiliki model abstrak tentang apa yang seharusnya diberikan oleh kelas turunan yang menerapkan kelas abstrak sejauh layanan atau perilaku. Warisan mudah digunakan secara berlebihan dan berkali-kali kelas yang paling berguna terdiri dari berbagai kelas menggunakan pola mixin .

Namun selalu ada pertanyaan perubahan, apa yang akan berubah dan bagaimana itu akan berubah dan mengapa itu akan berubah.

Warisan dapat menyebabkan tubuh sumber rapuh dan rapuh (lihat juga Warisan: cukup hentikan menggunakannya! ).

Masalah kelas dasar rapuh adalah masalah arsitektur mendasar dari sistem pemrograman berorientasi objek di mana kelas dasar (superclasses) dianggap "rapuh" karena modifikasi yang tampaknya aman untuk kelas dasar, ketika diwarisi oleh kelas turunan, dapat menyebabkan kelas turunan tidak berfungsi . Programmer tidak dapat menentukan apakah perubahan kelas dasar aman hanya dengan memeriksa metode kelas dasar secara terpisah.

Richard Chambers
sumber
Saya yakin bahwa konsep OOP apa pun dapat menyebabkan masalah jika konsep itu disalahgunakan atau digunakan secara berlebihan atau disalahgunakan! Selain itu, membuat asumsi bahwa b () adalah metode perpustakaan (misalnya fungsi murni tanpa dependensi lain) mempersempit ruang lingkup pertanyaan saya. Mari kita hindari ini.
Alexandros Gougousis
@AlexandrosGougousis Saya tidak berasumsi itu b()adalah fungsi murni. Itu bisa berupa functoid atau templat atau yang lainnya. Saya menggunakan frase "metode perpustakaan" sebagai makna beberapa komponen apakah fungsi murni, objek COM, atau apa pun yang digunakan untuk fungsi yang disediakannya untuk solusi.
Richard Chambers
Maaf, jika saya tidak jelas! Saya menyebutkan fungsi murni sebagai satu dari banyak contoh (Anda telah memberikan lebih banyak).
Alexandros Gougousis
1

Pertanyaan Anda menyarankan pendekatan atau ke kelas abstrak. Tapi saya pikir Anda harus menganggapnya sebagai alat lain di kotak alat Anda. Dan kemudian pertanyaannya menjadi: Untuk pekerjaan / masalah mana kelas abstrak merupakan alat yang tepat?

Satu kasus penggunaan yang sangat baik adalah menerapkan pola metode templat . Anda meletakkan semua logika invarian di kelas abstrak dan logika varian di subkelasnya. Perhatikan, bahwa logika bersama itu sendiri tidak lengkap dan tidak berfungsi. Sebagian besar waktu tentang penerapan algoritma, di mana beberapa langkah selalu sama, tetapi setidaknya satu langkah bervariasi. Letakkan satu langkah ini sebagai metode abstrak yang dipanggil dari salah satu fungsi di dalam kelas abstrak.

Terkadang kelas abstrak dibuat sebelumnya dan berfungsi sebagai templat bagaimana kelas turunan akan bekerja. Itu berarti, lebih atau kurang, bahwa mereka menyediakan beberapa fungsionalitas tingkat tinggi tetapi meninggalkan detail tertentu untuk diimplementasikan oleh kelas turunan. Kelas abstrak mendefinisikan perlunya rincian ini dengan menerapkan beberapa metode abstrak. Dalam kasus seperti itu, kelas abstrak berfungsi seperti cetak biru, deskripsi fungsi tingkat tinggi atau apa pun yang Anda ingin menyebutnya. Itu tidak dapat digunakan sendiri, tetapi harus khusus untuk mendefinisikan rincian yang telah ditinggalkan dari implementasi tingkat tinggi.

Saya pikir contoh pertama Anda pada dasarnya adalah deskripsi pola metode templat (koreksi saya jika saya salah), jadi saya akan menganggap ini sebagai kasus penggunaan yang benar-benar valid dari kelas abstrak.

Kadang-kadang, kebetulan bahwa kelas abstrak dibuat setelah penciptaan beberapa kelas "turunan" (karena kelas induk / abstrak belum ada, mereka belum diturunkan, tetapi Anda tahu maksud saya). Dalam kasus ini, kelas abstrak biasanya digunakan sebagai tempat di mana Anda dapat meletakkan segala jenis kode umum yang berisi kelas turunan saat ini.

Untuk contoh kedua Anda, saya pikir menggunakan kelas abstrak bukanlah pilihan yang optimal, karena ada metode unggul untuk menangani logika bersama dan kode duplikat. Katakanlah Anda memiliki kelas abstrak A, kelas turunan Bdan Cdan kedua kelas turunan berbagi beberapa logika dalam bentuk metode s(). Untuk memutuskan pendekatan yang benar untuk menghilangkan duplikasi, penting untuk diketahui, apakah metode s()merupakan bagian dari antarmuka publik atau tidak.

Jika tidak (seperti dalam contoh nyata Anda sendiri dengan metode b()), kasusnya cukup sederhana. Cukup buat kelas terpisah darinya, juga mengekstraksi konteks yang diperlukan untuk melakukan operasi. Ini adalah contoh klasik komposisi atas warisan . Jika ada sedikit atau tidak ada konteks yang dibutuhkan, fungsi pembantu sederhana mungkin sudah cukup seperti yang disarankan dalam beberapa komentar.

Jika s()merupakan bagian dari antarmuka publik, itu menjadi sedikit lebih rumit. Di bawah asumsi yang s()tidak ada hubungannya dengan Bdan Cterkait dengan A, Anda tidak boleh dimasukkan ke s()dalam A. Jadi di mana meletakkannya? Saya berpendapat untuk mendeklarasikan antarmuka terpisah I, yang mendefinisikan s(). Kemudian lagi, Anda harus membuat kelas terpisah yang berisi logika untuk mengimplementasikan s()dan keduanya Bdan Cbergantung padanya.

Terakhir, berikut adalah tautan ke jawaban yang sangat bagus dari pertanyaan menarik tentang SO yang mungkin membantu Anda memutuskan kapan harus menggunakan antarmuka dan kapan untuk kelas abstrak:

Kelas abstrak yang baik akan mengurangi jumlah kode yang harus ditulis ulang karena fungsionalitas atau statusnya dapat dibagikan.

larsbe
sumber
Saya setuju bahwa s () menjadi bagian dari antarmuka publik membuat segalanya lebih rumit. Saya biasanya akan menghindari mendefinisikan metode publik memanggil metode publik lainnya di kelas yang sama.
Alexandros Gougousis
1

Sepertinya saya Anda kehilangan titik orientasi objek, baik secara logis dan teknis. Anda menggambarkan dua skenario: pengelompokan perilaku umum jenis dalam kelas dasar dan polimorfisme. Keduanya adalah aplikasi yang sah dari kelas abstrak. Tetapi apakah Anda harus membuat abstrak kelas atau tidak tergantung pada model analitis Anda, bukan kemungkinan teknis.

Anda harus mengenali jenis yang tidak memiliki inkarnasi di dunia nyata, namun meletakkan dasar untuk jenis tertentu yang memang ada. Contoh: seekor binatang. Tidak ada yang namanya binatang. Itu selalu anjing atau kucing atau apa pun tetapi tidak ada binatang yang nyata. Namun Animal membingkai semuanya.

Kemudian Anda berbicara tentang level. Level bukan bagian dari kesepakatan ketika datang ke warisan. Juga tidak mengenali data umum atau perilaku umum, itu adalah pendekatan teknis yang kemungkinan tidak akan membantu. Anda harus mengenali stereotip dan kemudian memasukkan kelas dasar. Jika tidak ada stereotip seperti itu, Anda mungkin lebih baik menggunakan beberapa antarmuka yang diimplementasikan oleh beberapa kelas.

Nama kelas abstrak Anda bersama dengan anggotanya harus masuk akal. Itu harus independen dari setiap kelas turunan, baik secara teknis maupun logis. Seperti Hewan bisa memiliki metode abstrak Makan (yang akan menjadi polimorfik) dan sifat abstrak boolean IsPet dan IsLifestock, yang masuk akal tanpa mengetahui tentang kucing atau anjing atau babi. Setiap ketergantungan (teknis atau logis) harus berjalan satu arah saja: dari keturunan ke basis. Kelas-kelas dasar itu sendiri juga tidak harus memiliki pengetahuan tentang kelas-kelas yang menurun.

Martin Maat
sumber
Stereotip yang Anda sebutkan kurang lebih sama dengan yang saya maksud ketika saya berbicara tentang fungsionalitas tingkat yang lebih tinggi atau ketika orang lain berbicara tentang konsep umum yang dijelaskan oleh kelas yang lebih tinggi dalam hierarki (vs konsep yang lebih khusus dijelaskan oleh kelas yang lebih rendah di hirarki).
Alexandros Gougousis
"Kelas dasar sendiri seharusnya tidak memiliki pengetahuan tentang kelas turun juga." Saya sangat setuju dengan ini. Kelas induk (abstrak atau tidak) harus mengandung logika bisnis mandiri yang tidak perlu memeriksa implementasi kelas anak untuk memahami hal itu.
Alexandros Gougousis
Ada hal lain tentang kelas dasar mengetahui subtipe-nya. Setiap kali subtipe baru dikembangkan, kelas dasar perlu dimodifikasi yang terbalik dan melanggar prinsip buka-tutup. Baru-baru ini saya menemukan ini dalam kode yang ada. Kelas dasar memiliki metode statis yang membuat instance subtipe berdasarkan konfigurasi aplikasi. Maksudnya adalah pola pabrik. Melakukannya dengan cara ini juga melanggar SRP. Dengan beberapa poin SOLID saya dulu berpikir "ya, itu sudah jelas" atau "bahasa akan secara otomatis mengurus itu" tetapi saya menemukan orang-orang lebih kreatif daripada yang bisa saya bayangkan.
Martin Maat
0

Kasus pertama , dari pertanyaan Anda:

Dalam kasus seperti itu, kelas abstrak berfungsi seperti cetak biru, deskripsi fungsi tingkat tinggi atau apa pun yang Anda ingin menyebutnya.

Kasus kedua:

Beberapa waktu lain ... kelas abstrak biasanya digunakan sebagai tempat di mana Anda dapat meletakkan segala jenis kode umum yang berisi kelas turunan saat ini.

Lalu, pertanyaan Anda :

Saya ingin tahu yang mana dari dua kasus ini yang seharusnya menjadi peraturan. ... Haruskah kode yang mungkin tidak memiliki makna untuk kelas abstrak itu sendiri ada di sana hanya karena kebetulan umum untuk kelas turunan?

IMO Anda memiliki dua skenario yang berbeda, karena itu Anda tidak dapat membuat aturan tunggal untuk memutuskan desain apa yang harus diterapkan.

Untuk kasus pertama, kami memiliki hal-hal seperti pola Metode Templat yang telah disebutkan dalam jawaban lain.

Untuk kasus kedua, Anda memberi contoh metode abstrak penerima A kelas ab (); IMO metode b () hanya boleh dipindahkan jika masuk akal untuk SEMUA kelas turunan lainnya. Dengan kata lain, jika Anda memindahkannya karena digunakan di dua tempat, mungkin ini bukan pilihan yang baik, karena besok mungkin ada kelas turunan beton baru di mana b () tidak masuk akal sama sekali. Juga, seperti yang Anda katakan, jika Anda melihat kelas A secara terpisah dan b () juga tidak masuk akal dalam konteks itu, maka mungkin itu petunjuk bahwa ini bukan pilihan desain yang baik juga.

Emerson Cardoso
sumber
-1

Saya punya pertanyaan yang persis sama beberapa waktu lalu!

Apa yang saya temukan adalah setiap kali saya menemukan masalah ini saya menemukan bahwa ada beberapa konsep tersembunyi yang belum saya temukan. Dan konsep ini lebih mungkin daripada seharusnya diungkapkan dengan beberapa nilai-objek . Anda memiliki contoh yang sangat abstrak, jadi saya khawatir saya tidak dapat menunjukkan apa yang saya maksud dengan menggunakan kode Anda. Tetapi ini adalah kasus dari praktik saya sendiri. Saya memiliki dua kelas yang mewakili cara untuk mengirim permintaan ke sumber daya eksternal dan mengurai respons. Ada kesamaan dalam bagaimana permintaan dibentuk dan bagaimana tanggapan diuraikan. Jadi seperti itulah hierarki saya:

abstract class AbstractProtocol
{
    /**
     * @return array Registration params to send
     */
    abstract protected function assembleRegistrationPart();

    /**
     * @return array Payment params to send
     */
    abstract protected function assemblePaymentPart();

    protected function doSend(array $data)
    {
        return
            (new HttpClient(
                [
                    'timeout' => 60,
                    'encoding' => 'utf-8',
                    'language' => 'en',
                ]
            ))
                ->send($data);
    }

    protected function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}

class ClassicProtocol extends AbstractProtocol
{
    public function send()
    {
        $registration = $this->assembleRegistrationPart();
        $payment = $this->assemblePaymentPart();
        $specificParams = $this->assembleClassicSpecificPart();

        $dataToSend =
            array_merge(
                $registration, $payment, $specificParams
            );

        $this->log($dataToSend);

        $this->doSend($dataToSend);
    }

    protected function assembleRegistrationPart()
    {
        return ['hello' => 'there'];
    }

    protected function assemblePaymentPart()
    {
        return ['pay' => 'yes'];
    }
}

Kode semacam itu menunjukkan bahwa saya hanya menyalahgunakan warisan. Begitulah cara itu bisa di refactored:

class ClassicProtocol
{
    private $request;
    private $logger;

    public function __construct(Request $request, Logger $logger, Client $client)
    {
        $this->request = $request;
        $this->client = $client;
        $this->logger = $logger;
    }

    public function send()
    {
        $this->logger->log($this->request->getData());
        $this->client->send($this->request->getData());
    }
}

$protocol =
    new ClassicProtocol(
        new PaymentRequest(
            new RegistrationData(),
            new PaymentData(),
            new ClassicSpecificData()
        ),
        new ClassicLogger(),
        new ClassicClient()
    );

class RegistrationData
{
    public function getData()
    {
        return ['hello' => 'there'];
    }
}

class PaymentData
{
    public function getData()
    {
        return ['pay' => 'yes'];
    }
}

class ClassicLogger
{
    public function log(array $data)
    {
        $header = 'Here is a request to external system!';
        $body = implode(', ', $this->maskData($data));
        Logger::log($header . '. \n ' . $body);
    }
}
class ClassicClient
{
    private $properties;

    public function __construct()
    {
        $this->properties =
            [
                'timeout' => 60,
                'encoding' => 'utf-8',
                'language' => 'en',
            ];
    }
}

Sejak itu saya memperlakukan warisan dengan sangat hati-hati karena saya sering kali terluka.

Sejak itu saya sampai pada kesimpulan lain tentang warisan. Saya sangat menentang warisan berdasarkan struktur internal . Ini memecah enkapsulasi, rapuh, ini prosedural. Dan ketika saya mendekomposisi domain saya dengan cara yang benar , pewarisan tidak sering terjadi.

Vadim Samokhin
sumber
@Downvoter, tolong bantu saya dan komentar apa yang salah dengan jawaban ini.
Vadim Samokhin