Ganti Kondisional dengan Polimorfisme dengan cara yang benar?

10

Pertimbangkan dua kelas Dogdan Catkeduanya sesuai dengan Animalprotokol (dalam hal bahasa pemrograman Swift. Itu akan menjadi antarmuka dalam Java / C #).

Kami memiliki layar yang menampilkan daftar campuran anjing dan kucing. Ada Interactorkelas yang menangani logika di balik layar.

Sekarang kami ingin menyajikan peringatan konfirmasi kepada pengguna ketika ia ingin menghapus kucing. Namun, anjing harus segera dihapus tanpa pemberitahuan. Metode dengan kondisional akan terlihat seperti ini:

func tryToDeleteModel(model: Animal) {
    if let model = model as? Cat {
        tellSceneToShowConfirmationAlert()
    } else if let model = model as? Dog {
        deleteModel(model: model)
    }
}

Bagaimana kode ini dapat di refactored? Jelas baunya

Andrey Gordeev
sumber

Jawaban:

9

Anda membiarkan tipe protokol itu sendiri menentukan perilaku. Anda ingin memperlakukan semua protokol sama di seluruh program Anda kecuali di kelas pelaksana itu sendiri. Melakukannya dengan cara ini adalah menghormati Prinsip Substitusi Liskov, yang mengatakan Anda harus dapat melewati salah satu Catatau Dog(atau protokol lain yang pada akhirnya Animalberpotensi Anda miliki ), dan membuatnya bekerja dengan acuh tak acuh.

Jadi mungkin Anda akan menambahkan isCriticalfungsi untuk Animaldiimplementasikan oleh keduanya Dogdan Cat. Implementasi apa pun Dogakan kembali salah dan implementasi apa pun Catakan kembali benar.

Pada titik itu, Anda hanya perlu melakukan (Maafkan saya jika sintaksisnya salah. Bukan pengguna Swift):

func tryToDeleteModel(model: Animal) {
    if model.isCritical() {
        tellSceneToShowConfirmationAlert()
    } else {
        deleteModel(model: model)
    }
}

Hanya ada masalah kecil dengan itu, dan itu adalah bahwa Dogdan Catprotokol, yang berarti mereka dalam diri mereka sendiri tidak menentukan apa isCriticalhasil, meninggalkan ini untuk setiap kelas yang mengimplementasikan untuk memutuskan untuk diri mereka sendiri. Jika Anda memiliki banyak implementasi, mungkin ada baiknya Anda membuat kelas yang dapat diperpanjang Catatau Dogyang sudah mengimplementasikan dengan benar isCriticaldan secara efektif menghapus semua kelas implementasi dari kebutuhan untuk mengganti isCritical.

Jika ini tidak menjawab pertanyaan Anda, silakan tulis di komentar dan saya akan memperluas jawaban saya sesuai!

Neil
sumber
Ini sedikit tidak jelas dalam pernyataan pertanyaan, tetapi Dogdan Catdigambarkan sebagai kelas, sedangkan Animalprotokol yang diterapkan oleh masing-masing kelas tersebut. Jadi ada sedikit ketidakcocokan antara pertanyaan dan jawaban Anda.
Caleb
Jadi, Anda menyarankan untuk membiarkan model memutuskan apakah akan menampilkan munculan konfirmasi atau tidak? Tetapi bagaimana jika ada logika yang terlibat, seperti hanya menampilkan popup jika ada 10 kucing yang ditampilkan? Logikanya tergantung pada Interactorkondisi sekarang
Andrey Gordeev
Ya, maaf tentang pertanyaan yang tidak jelas, saya telah membuat beberapa suntingan. Seharusnya lebih jelas sekarang
Andrey Gordeev
1
Perilaku seperti ini seharusnya tidak dikaitkan dengan model. Itu tergantung pada konteks dan bukan entitas itu sendiri. Saya pikir Kucing dan Anjing lebih cenderung menjadi POJO. Perilaku harus ditangani di tempat lain dan dapat berubah sesuai konteks. Mendelegasikan perilaku atau metode yang menjadi sandaran perilaku pada Kucing atau Anjing akan menyebabkan terlalu banyak tanggung jawab di kelas tersebut.
Grégory Elhaimer
@ GrégoryElhaimer Harap dicatat bahwa ini tidak menentukan perilaku. Itu hanya menyatakan apakah itu kelas kritis atau tidak. Perilaku di seluruh program yang perlu diketahui jika ini adalah kelas kritis kemudian dapat mengevaluasi dan bertindak sesuai. Jika ini memang sebuah properti yang membedakan bagaimana instance dalam keduanya Catdan Dogditangani, itu bisa dan harus menjadi properti umum di Animal. Melakukan hal lain adalah meminta perawatan sakit kepala nanti.
Neil
4

Tell vs. Ask

Pendekatan bersyarat yang Anda tunjukkan akan kami sebut " bertanya ". Di sinilah klien yang mengkonsumsi bertanya "kamu seperti apa?" dan menyesuaikan perilaku dan interaksi mereka dengan objek sesuai.

Ini kontras dengan alternatif yang kita sebut " kirim ". Dengan menggunakan tell , Anda mendorong lebih banyak pekerjaan ke dalam implementasi polimorfik, sehingga kode klien yang digunakan lebih sederhana, tanpa persyaratan, dan umum terlepas dari kemungkinan implementasi.

Karena Anda ingin menggunakan lansiran konfirmasi, Anda bisa menjadikannya kemampuan antarmuka yang eksplisit. Jadi, Anda mungkin memiliki metode boolean yang secara opsional memeriksa dengan pengguna dan mengembalikan boolean konfirmasi. Di kelas yang tidak ingin mengkonfirmasi, mereka hanya menimpanya return true;. Implementasi lain mungkin secara dinamis menentukan apakah mereka ingin menggunakan konfirmasi.

Klien yang mengkonsumsi akan selalu menggunakan metode konfirmasi terlepas dari subkelas tertentu yang bekerja sama dengannya, yang membuat interaksi mengatakan daripada meminta .

(Pendekatan lain adalah dengan mendorong konfirmasi ke penghapusan, tetapi itu akan mengejutkan klien yang mengharapkan operasi penghapusan untuk berhasil.)

Erik Eidt
sumber
Jadi, Anda menyarankan untuk membiarkan model memutuskan apakah akan menampilkan munculan konfirmasi atau tidak? Tetapi bagaimana jika ada logika yang terlibat, seperti hanya menampilkan popup jika ada 10 kucing yang ditampilkan? Logikanya tergantung pada Interactorkeadaan sekarang
Andrey Gordeev
2
Ok, ya, itu pertanyaan yang berbeda, membutuhkan jawaban yang berbeda.
Erik Eidt
2

Menentukan apakah konfirmasi diperlukan adalah tanggung jawab Catkelas, jadi aktifkan untuk melakukan tindakan itu. Saya tidak tahu Kotlin, jadi saya akan mengungkapkan hal-hal dalam C #. Semoga ide-ide tersebut kemudian dapat ditransfer ke Kotlin juga.

interface Animal
{
    bool IsOkToDelete();
}

class Cat : Animal
{
    private readonly Func<bool> _confirmation;

    public Cat (Func<bool> confirmation) => _confirmation = confirmation;

    public bool IsOkToDelete() => _confirmation();
}

class Dog : Animal
{
    public bool IsOkToDelete() => true;
}

Kemudian, saat membuat sebuah Catinstance, Anda menyediakannya TellSceneToShowConfirmationAlert, yang harus dikembalikan truejika OK untuk menghapus:

var model = new Cat(TellSceneToShowConfirmationAlert);

Dan kemudian fungsi Anda menjadi:

void TryToDeleteModel(Animal model) 
{
    if (model.IsOKToDelete())
    {
        DeleteModel(model)
    }
}
David Arno
sumber
1
Bukankah ini memindahkan logika delete ke model? Bukankah lebih baik menggunakan objek lain untuk menangani ini? Mungkin struktur data seperti Kamus <Cat> di dalam ApplicationService; periksa untuk melihat apakah Kucing itu ada dan apakah ia mematikan peringatan konfirmasi?
keelerjr12
@ keelerjr12, ia memindahkan tanggung jawab untuk menentukan apakah diperlukan konfirmasi untuk menghapus ke dalam Catkelas. Saya berpendapat bahwa itu adalah tempatnya. Itu tidak bisa memutuskan bagaimana konfirmasi itu dicapai (yang disuntikkan) dan itu tidak menghapus sendiri. Jadi tidak, itu tidak memindahkan logika delete ke dalam model.
David Arno
2
Saya merasa pendekatan ini akan menghasilkan berton-ton kode terkait UI yang melekat pada kelas itu sendiri. Jika kelas dimaksudkan untuk digunakan di beberapa lapisan UI, masalahnya bertambah. Namun jika ini adalah kelas tipe ViewModel, daripada entitas bisnis, maka tampaknya sesuai.
Graham
@ Braham, ya itu jelas risiko dengan pendekatan ini: itu bergantung pada hal itu mudah disuntikkan TellSceneToShowConfirmationAlertke instance Cat. Dalam situasi di mana itu bukan hal yang mudah (seperti dalam sistem multi-layered di mana fungsi ini terletak pada level yang dalam), maka pendekatan ini tidak akan menjadi yang baik.
David Arno
1
Persis apa yang saya maksudkan. Entitas bisnis vs kelas ViewModel. Dalam domain bisnis, Kucing seharusnya tidak tahu tentang kode terkait UI. Kucing keluarga saya tidak memperingatkan siapa pun. Terima kasih!
keelerjr12
1

Saya akan menyarankan untuk pergi untuk pola Pengunjung. Saya melakukan implementasi kecil di Jawa. Saya tidak terbiasa dengan Swift, tetapi Anda dapat mengadaptasinya dengan mudah.

Pengunjung

public interface AnimalVisitor<R>{
    R visitCat();
    R visitDog();
}

Model Anda

abstract class Animal { // can also be an interface like VisitableAnimal
    abstract <R> R accept(AnimalVisitor<R> visitor);
}

class Cat extends Animal {
    public <R> R accept(AnimalVisitor<R> visitor) {
         return visitor.visitCat();
     }
}

class Dog extends Animal {
    public <R> R accept(AnimalVisitor<R> visitor) {
         return visitor.visitDog();
     }
}

Memanggil pengunjung

public void tryToDelete(Animal animal) {
    animal.accept( new AnimalVisitor<Void>() {
        public Void visitCat() {
            tellSceneToShowConfirmation();
            return null;
        }

        public Void visitDog() {
            deleteModel(animal);
            return null;
        }
    });
}

Anda dapat memiliki implementasi AnimalVisitor sebanyak yang Anda inginkan.

Contoh:

public void isColorValid(Color color) {
    animal.accept( new AnimalVisitor<Boolean>() {
        public Boolean visitCat() {
            return Color.BLUE.equals(color);
        }

        public Boolean visitDog() {
            return true;
        }
    });
}
Grégory Elhaimer
sumber