Mana cara yang lebih baik untuk memanggil metode yang hanya tersedia untuk satu kelas yang mengimplementasikan antarmuka tetapi tidak yang lain?

11

Pada dasarnya saya perlu melakukan tindakan yang berbeda dengan syarat tertentu. Kode yang ada ditulis dengan cara ini

Antarmuka dasar

// DoSomething.java
interface DoSomething {

   void letDoIt(String info);
}

Implementasi kelas pekerja pertama

class DoItThisWay implements DoSomething {
  ...
}

Implementasi kelas pekerja kedua

class DoItThatWay implements DoSomething {
   ...
}

Kelas utama

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          worker = new DoItThisWay();
        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }

Kode ini berfungsi dengan baik dan mudah dimengerti.

Sekarang, karena persyaratan baru, saya perlu memberikan informasi baru yang masuk akal DoItThisWay.

Pertanyaan saya adalah: apakah gaya pengkodean berikut baik untuk menangani persyaratan ini.

Gunakan variabel dan metode kelas baru

// Use new class variable and method

class DoItThisWay implements DoSomething {
  private int quality;
  DoSomething() {
    quality = 0;
  }

  public void setQuality(int quality) {
    this.quality = quality;
  };

 public void letDoIt(String info) {
   if (quality > 50) { // make use of the new information
     ...
   } else {
     ...
   }
 } ;

}

Jika saya melakukannya dengan cara ini, saya perlu membuat perubahan yang sesuai dengan penelepon:

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          int quality = obtainQualityInfo();
          DoItThisWay tmp = new DoItThisWay();
          tmp.setQuality(quality)
          worker = tmp;

        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }

Apakah ini gaya pengkodean yang baik? Atau bisakah saya melemparkannya saja

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          int quality = obtainQualityInfo();
          worker = new DoItThisWay();
          ((DoItThisWay) worker).setQuality(quality)
        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }
Anthony Kong
sumber
19
Mengapa tidak masuk saja qualityke konstruktor DoItThisWay?
David Arno
Saya mungkin perlu merevisi pertanyaan saya ... karena untuk alasan kinerja pembangunan DoItThisWaydan DoItThatWaydilakukan sekali dalam konstruktor Main. Mainadalah kelas hidup yang panjang dan doingItdisebut berkali-kali.
Anthony Kong
Ya, memperbarui pertanyaan Anda akan menjadi ide bagus dalam kasus itu.
David Arno
Apakah Anda bermaksud mengatakan bahwa setQualitymetode ini akan dipanggil beberapa kali selama masa pakai DoItThisWayobjek?
jpmc26
1
Tidak ada perbedaan yang relevan antara dua versi yang Anda sajikan. Dari sudut pandang logis, mereka melakukan hal yang sama.
Sebastian Redl

Jawaban:

6

Saya berasumsi bahwa qualityperlu diatur di samping setiap letDoIt()panggilan pada a DoItThisWay().

Masalah yang saya lihat timbul di sini adalah ini: Anda memperkenalkan penggabungan sementara (yaitu apa yang terjadi jika Anda lupa menelepon setQuality()sebelum memanggil letDoIt()a DoItThisWay?). Dan implementasi untuk DoItThisWaydan DoItThatWayberbeda (yang satu perlu setQuality()menelepon, yang lain tidak).

Meskipun ini mungkin tidak menyebabkan masalah sekarang, itu mungkin kembali menghantui Anda pada akhirnya. Mungkin bermanfaat untuk melihat kembali letDoIt()dan mempertimbangkan apakah informasi berkualitas mungkin perlu menjadi bagian dari infoAnda melewatinya; tapi ini tergantung dari detailnya.

CharonX
sumber
15

Apakah ini gaya pengkodean yang baik?

Dari sudut pandang saya tidak ada versi Anda. Pertama yang harus dihubungi setQualitysebelum letDoItdapat dipanggil adalah temporal coupling . Anda buntu melihat DoItThisWaysebagai turunan dari DoSomething, tetapi tidak (setidaknya tidak secara fungsional), itu agak seperti

interface DoSomethingWithQuality {
   void letDoIt(String info, int quality);
}

Ini akan membuat Mainsesuatu seperti

class Main {
    // omitting the creation
    private DoSomething doSomething;
    private DoSomethingWithQuality doSomethingWithQuality;

    public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
            int quality = obtainQualityInfo();
            doSomethingWithQuality.letDoIt(info, quality);
        } else {
            doSomething.letDoIt(info);
        }
    }
}

Di lain pihak, Anda dapat meneruskan parameter ke kelas secara langsung (dengan asumsi ini mungkin) dan mendelegasikan keputusan yang akan digunakan untuk pabrik (ini akan membuat instance dipertukarkan lagi dan keduanya dapat diturunkan dari DoSomething). Ini akan membuat Mainterlihat seperti ini

class Main {
    private DoSomethingFactory doSomethingFactory;

     public doingIt(String info) {
         int quality = obtainQualityInfo();
         DoSomething doSomethingWorker = doSomethingFactory.Create(info, quality);
         doSomethingWorker.letDoIt();
     }
}

Saya sadar Anda yang menulisnya

karena untuk alasan kinerja pembangunan DoItThisWay dan DoItThatWay dilakukan sekali dalam konstruktor Main

Tapi Anda bisa menyimpan bagian yang mahal untuk dibuat di pabrik dan mengirimkannya ke konstruktor juga.

Paul Kertscher
sumber
Argh, apakah Anda memata-matai saya saat saya mengetik jawaban? Kami membuat poin serupa (tapi milik Anda jauh lebih komprehensif dan fasih);)
CharonX
1
@DocBrown Ya, ini masih bisa diperdebatkan, tetapi karena DoItThisWaymembutuhkan kualitas yang harus ditetapkan sebelum memanggilnya letDoItberperilaku berbeda. Saya berharap DoSomethinguntuk bekerja dengan cara yang sama (tidak menetapkan kualitas sebelumnya) untuk semua turunannya. Apakah ini masuk akal? Tentu saja ini agak buram, karena jika kita memanggil setQualitydari metode pabrik, klien akan agnostik mengenai apakah kualitasnya harus ditetapkan atau tidak.
Paul Kertscher
2
@DocBrown Saya akan menganggap kontrak letDoIt()adalah (tidak memiliki info tambahan) "Saya dapat menjalankan dengan benar (melakukan apa pun yang saya lakukan) jika Anda memberi saya String info". Membutuhkan yang setQuality()dipanggil sebelumnya memperkuat prasyarat panggilan letDoIt(), dan dengan demikian akan menjadi pelanggaran terhadap LSP.
CharonX
2
@CharonX: jika, misalnya, akan ada kontrak yang menyiratkan bahwa " letDoIt" tidak akan mengeluarkan pengecualian, dan lupa memanggil "setQuality" akan membuat letDoItkemudian melemparkan pengecualian, maka saya akan setuju, implementasi seperti itu akan melanggar LSP. Tetapi semua itu tampak seperti asumsi yang sangat palsu bagi saya.
Doc Brown
1
Ini jawaban yang bagus. Satu-satunya hal yang saya tambahkan adalah beberapa komentar tentang betapa buruknya temporal coupling untuk basis kode. Ini adalah kontrak tersembunyi antara komponen yang tampaknya terpisah. Dengan kopling normal, paling tidak bersifat eksplisit dan mudah diidentifikasi. Melakukan ini pada dasarnya menggali lubang dan menutupinya dengan daun.
JimmyJames
10

Mari kita asumsikan qualitytidak dapat diteruskan ke konstruktor, dan panggilan ke setQualitydiperlukan.

Saat ini, potongan kode suka

    int quality = obtainQualityInfo();
    worker = new DoItThisWay();
    ((DoItThisWay) worker).setQuality(quality);

terlalu kecil untuk menanamkan terlalu banyak pikiran ke dalamnya. IMHO itu terlihat agak jelek, tetapi tidak terlalu sulit untuk dipahami.

Masalah muncul ketika potongan kode seperti itu tumbuh dan karena refactoring Anda berakhir dengan sesuatu seperti

    int quality = obtainQualityInfo();
    worker = CreateAWorkerForThisInfo();
    ((DoItThisWay) worker).setQuality(quality);

Sekarang, Anda tidak segera melihat apakah kode itu masih benar, dan kompiler tidak akan memberi tahu Anda. Jadi, memperkenalkan variabel sementara dari jenis yang benar, dan menghindari gips, sedikit lebih aman, tanpa upaya ekstra yang nyata.

Namun, saya sebenarnya akan memberikan tmpnama yang lebih baik:

      int quality = obtainQualityInfo();
      DoItThisWay workerThisWay = new DoItThisWay();
      workerThisWay.setQuality(quality)
      worker = workerThisWay;

Penamaan tersebut membantu membuat kode yang salah terlihat salah.

Doc Brown
sumber
tmp mungkin lebih baik dinamai sesuatu seperti "workerThatUsesQuality"
user949300
1
@ user949300: IMHO itu bukan ide yang baik: asumsikan DoItThisWaymendapat parameter lebih spesifik - dengan apa yang Anda sarankan, nama harus diubah setiap waktu. Lebih baik menggunakan nama untuk variabel yang membuat tipe objek menjadi jelas, bukan metode mana yang tersedia.
Doc Brown
Untuk memperjelas, itu akan menjadi nama variabel, bukan nama kelas. Saya setuju bahwa menempatkan semua kemampuan dalam nama kelas adalah ide yang buruk. Jadi saya menyarankan itu workerThisWaymenjadi sesuatu seperti usingQuality. Dalam cuplikan kode kecil itu, lebih aman untuk lebih spesifik. (Dan, jika ada frasa yang lebih baik dalam domain mereka daripada "usingQuality", gunakan.
user949300
2

Antarmuka umum di mana inisialisasi bervariasi berdasarkan data runtime biasanya cocok dengan pola pabrik.

DoThingsFactory factory = new DoThingsFactory(thingThatProvidesQuality);
DoSomething doSomething = factory.getDoer(info);

doSomething.letDoIt();

Kemudian DoThingsFactory dapat khawatir tentang mendapatkan dan mengatur informasi kualitas di dalam getDoer(info)metode sebelum mengembalikan objek DoThingsWithQuality yang asli dilemparkan ke antarmuka DoSomething.

Greg Burghardt
sumber