Paksa pengembang lain untuk memanggil metode setelah menyelesaikan pekerjaan mereka

34

Di perpustakaan di Java 7, saya memiliki kelas yang menyediakan layanan ke kelas lain. Setelah membuat turunan dari kelas layanan ini, satu metode dapat dipanggil beberapa kali (sebut saja doWork()metode). Jadi saya tidak tahu kapan pekerjaan kelas layanan selesai.

Masalahnya adalah kelas layanan menggunakan benda berat dan harus melepaskannya. Saya mengatur bagian ini dalam suatu metode (sebut saja release()), tetapi tidak dijamin bahwa pengembang lain akan menggunakan metode ini.

Apakah ada cara untuk memaksa pengembang lain untuk memanggil metode ini setelah menyelesaikan tugas kelas layanan? Tentu saja saya bisa mendokumentasikannya, tetapi saya ingin memaksa mereka.

Catatan: Saya tidak dapat memanggil release()metode dalam doWork()metode ini, karena doWork() membutuhkan objek tersebut ketika dipanggil selanjutnya.

hasanghaforian
sumber
46
Apa yang Anda lakukan di sana adalah bentuk kopling sementara dan biasanya dianggap sebagai bau kode. Anda mungkin ingin memaksakan diri untuk menghasilkan desain yang lebih baik.
Frank
1
@ Sejujurnya saya setuju, jika mengubah desain lapisan yang mendasarinya adalah pilihan maka itu akan menjadi taruhan terbaik. Tapi saya curiga ini adalah pembungkus layanan orang lain.
Dar Brett
Benda berat jenis apa yang dibutuhkan Layanan Anda? Jika kelas Layanan tidak dapat mengelola sumber daya secara otomatis dengan sendirinya (tanpa memerlukan penggabungan waktu) maka mungkin sumber daya tersebut harus dikelola di tempat lain dan disuntikkan ke Layanan. Sudahkah Anda menulis unit test untuk kelas Layanan?
DATANG DARI
18
Sangat "tidak-Jawa" untuk meminta itu. Java adalah bahasa dengan pengumpulan sampah dan sekarang Anda menemukan semacam alat yang mengharuskan pengembang untuk "membersihkan".
Pieter B
22
@Petereter mengapa? AutoCloseabledibuat untuk tujuan yang tepat ini.
BgrWorker

Jawaban:

48

Solusi pragmatis adalah membuat kelas AutoCloseable, dan menyediakan finalize()metode sebagai backstop (jika perlu ... lihat di bawah!). Kemudian Anda bergantung pada pengguna kelas Anda untuk menggunakan try-with-resource atau menelepon close()secara eksplisit.

Tentu saja saya bisa mendokumentasikannya, tetapi saya ingin memaksa mereka.

Sayangnya, tidak ada cara 1 di Jawa untuk memaksa programmer untuk melakukan hal yang benar. Yang terbaik yang bisa Anda lakukan adalah mengambil penggunaan yang salah dalam penganalisa kode statis.


Pada topik finalizers. Fitur Java ini memiliki sangat sedikit kasus penggunaan yang baik . Jika Anda bergantung pada finalizer untuk merapikan, Anda mengalami masalah yang bisa memakan waktu sangat lama untuk merapikannya. Finalizer hanya akan dijalankan setelah GC memutuskan bahwa objek tidak lagi dapat dijangkau. Itu mungkin tidak terjadi sampai JVM melakukan pengumpulan penuh .

Jadi, jika masalah yang ingin Anda selesaikan adalah merebut kembali sumber daya yang perlu dirilis lebih awal , maka Anda telah ketinggalan perahu dengan menggunakan finalisasi.

Dan kalau-kalau Anda tidak mendapatkan apa yang saya katakan di atas ... hampir tidak pernah tepat untuk menggunakan finalizer dalam kode produksi, dan Anda tidak boleh mengandalkan mereka!


1 - Ada beberapa cara. Jika Anda siap untuk "menyembunyikan" objek layanan dari kode pengguna atau mengontrol siklus hidupnya dengan ketat (mis. Https://softwareengineering.stackexchange.com/a/345100/172 ) maka kode pengguna tidak perlu menelepon release(). Namun, API menjadi lebih rumit, dibatasi ... dan IMO "jelek". Juga, ini tidak memaksa programmer untuk melakukan hal yang benar . Ini menghilangkan kemampuan programmer untuk melakukan hal yang salah!

Stephen C
sumber
1
Finalizers mengajarkan pelajaran yang sangat buruk - jika Anda mengacaukannya, saya akan memperbaikinya untuk Anda. Saya lebih suka pendekatan netty -> parameter konfigurasi yang menginstruksikan (ByteBuffer) pabrik untuk secara acak membuat dengan probabilitas bytebuffers X% dengan finalizer yang mencetak lokasi di mana buffer ini diperoleh (jika belum dirilis). Jadi dalam produksi nilai ini dapat diatur ke 0% - yaitu tidak ada overhead, dan selama pengujian & dev ke nilai yang lebih tinggi seperti 50% atau bahkan 100% untuk mendeteksi kebocoran sebelum melepaskan produk
Svetlin Zarev
11
dan ingat bahwa finalizer tidak dijamin untuk dijalankan, bahkan jika instance objek sedang dikumpulkan!
jwenting
3
Perlu dicatat bahwa Eclipse memancarkan peringatan kompiler jika AutoCloseable tidak ditutup. Saya berharap Java IDE lain juga melakukannya.
meriton - saat mogok
1
@jwenting Dalam kasus apa mereka tidak berjalan ketika objeknya GC? Saya tidak dapat menemukan sumber daya yang menyatakan atau menjelaskan hal itu.
Cedric Reichenbach
3
@ Voo Anda salah mengerti komentar saya. Anda memiliki dua implementasi -> DefaultResourcedan FinalizableResourceyang memperpanjang yang pertama dan menambahkan finalizer. Dalam produksi Anda menggunakan DefaultResourceyang tidak memiliki finalizer-> tidak ada overhead. Selama pengembangan dan pengujian Anda mengonfigurasi aplikasi untuk menggunakan FinalizableResourceyang memiliki finalizer dan karenanya menambah biaya tambahan. Selama dev dan pengujian itu bukan masalah, tetapi itu bisa membantu Anda mengidentifikasi kebocoran dan memperbaikinya sebelum mencapai produksi. Namun jika kebocoran mencapai PRD, Anda dapat mengonfigurasi pabrik untuk membuat X% FinalizableResourceuntuk membocorkannya
Svetlin Zarev
55

Ini seperti use-case untuk AutoCloseableantarmuka dan try-with-resourcespernyataan yang diperkenalkan di Java 7. Jika Anda memiliki sesuatu seperti

public class MyService implements AutoCloseable {
    public void doWork() {
        // ...
    }

    @Override
    public void close() {
        // release resources
    }
}

maka konsumen Anda dapat menggunakannya sebagai

public class MyConsumer {
    public void foo() {
        try (MyService myService = new MyService()) {
            //...
            myService.doWork()
            //...
            myService.doWork()
            //...
        }
    }
}

dan tidak perlu khawatir tentang memanggil eksplisit releaseterlepas dari apakah kode mereka melempar pengecualian.


Jawaban tambahan

Jika yang benar-benar Anda cari adalah untuk memastikan tidak ada yang bisa lupa untuk menggunakan fungsi Anda, Anda harus melakukan beberapa hal

  1. Pastikan semua konstruktor MyServicebersifat pribadi.
  2. Tentukan satu titik masuk untuk digunakan MyServiceyang memastikan itu dibersihkan setelah.

Anda akan melakukan sesuatu seperti

public interface MyServiceConsumer {
    void accept(MyService value);
}

public class MyService implements AutoCloseable {
    private MyService() {
    }


    public static void useMyService(MyServiceConsumer consumer) {
        try (MyService myService = new MyService()) {
            consumer.accept(myService)
        }
    }
}

Anda kemudian akan menggunakannya sebagai

public class MyConsumer {
    public void foo() {
        MyService.useMyService(new MyServiceConsumer() {
            public void accept(MyService myService) {
                myService.doWork()
            }
        });
    }
}

Saya tidak merekomendasikan jalur ini pada awalnya karena itu mengerikan tanpa Java-8 lambdas dan antarmuka fungsional (di mana MyServiceConsumermenjadi Consumer<MyService>) dan itu cukup mengesankan pada konsumen Anda. Tetapi jika yang Anda inginkan hanyalah yang release()harus dipanggil, itu akan berhasil.

walpen
sumber
1
Jawaban ini mungkin lebih baik daripada jawaban saya. Jalan saya mengalami keterlambatan sebelum Tukang Sampah menendang masuk
Dar Brett
1
Bagaimana Anda memastikan bahwa klien akan menggunakan try-with-resourcesdan tidak hanya menghilangkan try, sehingga closepanggilan tidak terjawab?
Frank
@walpen Cara ini memiliki masalah yang sama. Bagaimana saya bisa memaksa pengguna untuk menggunakan try-with-resources?
hasanghaforian
1
@ Frank / @hasanghaforian, Ya, cara ini tidak memaksa dipanggil. Itu memang memiliki manfaat keakraban: pengembang Java tahu Anda benar-benar harus memastikan .close()dipanggil ketika kelas mengimplementasikan AutoCloseable.
walpen
1
Tidak bisakah Anda memasangkan finalizer dengan usingblok untuk finalisasi deterministik? Setidaknya dalam C #, ini menjadi pola yang cukup jelas bagi pengembang untuk terbiasa menggunakan ketika memanfaatkan sumber daya yang membutuhkan finalisasi deterministik. Sesuatu yang mudah dan membentuk kebiasaan adalah hal terbaik berikutnya ketika Anda tidak bisa memaksa seseorang untuk melakukan sesuatu. stackoverflow.com/a/2016311/84206
AaronLS
52

ya kamu bisa

Anda benar-benar dapat memaksa mereka untuk melepaskan, dan membuatnya jadi 100% mustahil untuk dilupakan, dengan membuatnya tidak mungkin bagi mereka untuk membuat Serviceinstance baru .

Jika mereka melakukan sesuatu seperti ini sebelumnya:

Service s = s.new();
try {
  s.doWork("something");
  if (...) s.doWork("something else");
  ...
}
finally {
  s.release(); // oops: easy to forget
}

Kemudian ubah sehingga mereka harus mengaksesnya seperti ini.

// Their code

Service.runWithRelease(new ServiceConsumer() {
  void run(Service s) {
    s.doWork("something");
    if (...) s.doWork("something else");
    ...
  }  // yay! no s.release() required.
}

// Your code

interface ServiceConsumer {
  void run(Service s);
}

class Service {

   private Service() { ... }      // now: private
   private void release() { ... } // now: private
   public void doWork() { ... }   // unchanged

   public static void runWithRelease(ServiceConsumer consumer) {
      Service s = new Service();
      try {
        consumer.run(s);
      }
      finally {
        s.release();
      } 
    } 
  }

Peringatan:

  • Pertimbangkan kodesemu ini, sudah lama sejak saya menulis Java.
  • Mungkin ada varian yang lebih elegan di sekitar hari ini, mungkin termasuk AutoCloseableantarmuka yang disebutkan seseorang; tetapi contoh yang diberikan harus bekerja di luar kotak, dan kecuali untuk keanggunan (yang merupakan tujuan yang bagus dalam dirinya sendiri) tidak boleh ada alasan utama untuk mengubahnya. Perhatikan bahwa ini bermaksud untuk mengatakan bahwa Anda dapat menggunakan AutoCloseabledi dalam implementasi pribadi Anda; manfaat dari solusi saya atas penggunaan pengguna Anda AutoCloseableadalah bahwa hal itu dapat, sekali lagi, tidak dilupakan.
  • Anda harus menyempurnakannya sesuai kebutuhan, misalnya untuk menyuntikkan argumen ke dalam new Servicepanggilan.
  • Seperti disebutkan dalam komentar, pertanyaan apakah konstruk seperti ini (yaitu, mengambil proses membuat dan menghancurkan a Service) berada di tangan si penelepon atau tidak berada di luar cakupan jawaban ini. Tetapi jika Anda memutuskan bahwa Anda benar-benar membutuhkan ini, ini adalah bagaimana Anda dapat melakukannya.
  • Seorang komentator memberikan informasi ini mengenai penanganan pengecualian: Google Books
AnoE
sumber
2
Saya senang dengan komentar untuk downvote, sehingga saya dapat meningkatkan jawabannya.
AnoE
2
Saya tidak downvote, tetapi desain ini cenderung lebih banyak masalah daripada nilainya. Ini menambah banyak kompleksitas dan membebani penelepon. Pengembang harus cukup akrab dengan kelas gaya coba-dengan-sumber daya yang menggunakannya adalah sifat kedua setiap kali kelas mengimplementasikan antarmuka yang sesuai, sehingga biasanya cukup baik.
jpmc26
30
@ jpmc26, lucu saya merasa bahwa pendekatan itu mengurangi kerumitan dan beban pada penelepon dengan menyelamatkan mereka dari memikirkan siklus hidup sumber daya sama sekali. Selain itu juga; OP tidak bertanya "haruskah saya memaksa pengguna ..." tetapi "bagaimana cara saya memaksa pengguna". Dan jika itu cukup penting, ini adalah salah satu solusi yang bekerja dengan sangat baik, secara struktural sederhana (hanya membungkusnya dengan penutupan / runnable), bahkan dapat ditransfer ke bahasa lain yang memiliki lambdas / procs / penutupan juga. Baiklah, saya serahkan pada demokrasi SE untuk menilai; OP sudah memutuskan. ;)
AnoE
14
Sekali lagi, @ jpmc26, saya tidak begitu tertarik untuk membahas apakah pendekatan ini benar untuk setiap kasus penggunaan tunggal di luar sana. OP bertanya bagaimana menegakkan hal semacam itu dan jawaban ini memberi tahu bagaimana cara menegakkan hal semacam itu . Bukan untuk kita memutuskan apakah pantas untuk melakukannya sejak awal. Teknik yang ditampilkan adalah alat, tidak lebih, tidak kurang, dan berguna untuk dimiliki dalam satu tas alat, saya percaya.
AnoE
11
Ini adalah jawaban yang benar, itu pada dasarnya pola hole-in-the-middle
jk.
11

Anda dapat mencoba memanfaatkan pola Command .

class MyServiceManager {

    public void execute(MyServiceTask tasks...) {
        // set up everyting
        MyService service = this.acquireService();
        // execute the submitted tasks
        foreach (Task task : tasks)
            task.executeWith(service);
        // cleanup yourself
        service.releaseResources();
    }

}

Ini memberi Anda kendali penuh atas akuisisi dan pelepasan sumber daya. Penelepon hanya mengirimkan tugas ke Layanan Anda, dan Anda sendiri yang bertanggung jawab untuk memperoleh dan membersihkan sumber daya.

Namun ada tangkapan. Penelepon masih dapat melakukan ini:

MyServiceTask t1 = // some task
manager.execute(t1);
MyServiceTask t2 = // some task
manager.execute(t2);

Tapi Anda bisa mengatasi masalah ini ketika sedang arisis. Ketika ada masalah kinerja dan Anda mengetahui bahwa beberapa penelepon melakukan ini, cukup tunjukkan cara yang tepat dan selesaikan masalah:

MyServiceTask t1 = // some task
MyServiceTask t2 = // some task
manager.execute(t1, t2);

Anda dapat membuat kompleks sewenang-wenang ini dengan menerapkan janji-janji untuk tugas-tugas yang bergantung pada tugas-tugas lain, tetapi kemudian melepaskan barang juga menjadi lebih rumit. Ini hanya titik awal.

Async

Seperti yang telah ditunjukkan dalam komentar, hal di atas tidak benar-benar berfungsi dengan permintaan asinkron. Itu benar. Tetapi ini dapat dengan mudah diselesaikan di Java 8 dengan menggunakan CompleteableFuture , terutama CompleteableFuture#supplyAsyncuntuk menciptakan masa depan individu dan CompleteableFuture#allOfuntuk merilis sumber daya setelah semua tugas selesai. Atau, seseorang selalu dapat menggunakan Utas atau Layanan Pelaksana untuk menggulung implementasi Futures / Janji mereka sendiri.

Polygnome
sumber
1
Saya akan sangat menghargai jika downvoters meninggalkan komentar mengapa mereka berpikir ini buruk, sehingga saya bisa langsung menanggapi masalah tersebut secara langsung atau setidaknya mendapatkan sudut pandang lain untuk masa depan. Terima kasih!
Polygnome
+1 upvote. Ini mungkin pendekatan, tetapi layanannya async dan konsumen perlu mendapatkan hasil pertama sebelum meminta layanan berikutnya.
hasanghaforian
1
Ini hanya template barebones. JS menyelesaikan ini dengan janji, Anda bisa melakukan hal serupa di Jawa dengan futures dan kolektor yang dipanggil ketika semua tugas selesai dan kemudian dibersihkan. Itu pasti mungkin untuk mendapatkan async dan bahkan dependensi di sana, tetapi juga banyak hal yang rumit.
Polygnome
3

Jika Anda bisa, jangan izinkan pengguna membuat sumber daya. Biarkan pengguna meneruskan objek pengunjung ke metode Anda, yang akan membuat sumber daya, meneruskannya ke metode objek pengunjung, dan lepaskan setelahnya.

9000
sumber
Terima kasih atas balasan Anda, tetapi maafkan saya, apakah maksud Anda lebih baik memberikan sumber daya kepada konsumen dan kemudian mendapatkannya lagi ketika dia meminta layanan? Maka masalahnya akan tetap ada: Konsumen mungkin lupa untuk merilis sumber daya tersebut.
hasanghaforian
Jika Anda ingin mengendalikan sumber daya, jangan lepaskan sumber daya itu, jangan serahkan sepenuhnya ke aliran eksekusi orang lain. Salah satu cara untuk melakukannya adalah dengan menjalankan metode yang telah ditentukan dari objek pengunjung pengguna. AutoCloseablemelakukan hal yang sangat mirip di balik layar. Di bawah sudut lain, ini mirip dengan injeksi ketergantungan .
9000
1

Ide saya mirip dengan Polygnome.

Anda dapat memiliki metode "do_something ()" di kelas Anda hanya menambahkan perintah ke daftar perintah pribadi. Kemudian, miliki metode "commit ()" yang benar-benar berfungsi, dan panggil "release ()".

Jadi, jika pengguna tidak pernah memanggil commit (), pekerjaan itu tidak pernah dilakukan. Jika mereka melakukannya, sumber dayanya akan dibebaskan.

public class Foo
{
  private ArrayList<ICommand> _commands;

  public Foo doSomething(int arg) { _commands.Add(new DoSomethingCommand(arg)); return this; }
  public Foo doSomethingElse() { _commands.Add(new DoSomethingElseCommand()); return this; }

  public void commit() { 
     for(ICommand c : _commands) c.doWork();
     release();
     _commands.clear();
  }

}

Anda kemudian dapat menggunakannya seperti foo.doSomething.doSomethineElse (). Commit ();

Sava B.
sumber
Ini adalah solusi yang sama, tetapi alih-alih meminta penelepon untuk mempertahankan daftar tugas yang Anda pertahankan secara internal. Konsep inti secara harfiah sama. Tapi ini tidak mengikuti konvensi pengkodean untuk Java yang pernah saya lihat dan sebenarnya cukup sulit untuk dibaca (loop body pada baris yang sama dengan loop header, _ pada awalnya).
Polygnome
Ya, itu adalah pola perintah. Saya hanya meletakkan beberapa kode untuk memberikan inti dari apa yang saya pikirkan dalam cara sesingkat mungkin. Saya lebih banyak menulis C # hari ini, di mana variabel pribadi sering diawali dengan _.
Sava B.
-1

Alternatif lain dari yang disebutkan adalah penggunaan "referensi pintar" untuk mengelola sumber daya Anda yang telah dienkapsulasi dengan cara yang memungkinkan pengumpul sampah untuk secara otomatis membersihkan tanpa secara manual membebaskan sumber daya. Kegunaannya tentu saja tergantung pada implementasi Anda. Baca lebih lanjut tentang topik ini di posting blog yang luar biasa ini: https://community.oracle.com/blogs/enicholas/2006/05/04/understanding-weak-references

Dracam
sumber
Ini adalah ide yang menarik, tetapi saya tidak bisa melihat bagaimana menggunakan referensi yang lemah memecahkan masalah OP. Kelas layanan tidak tahu apakah itu akan mendapatkan permintaan doWork () lain. Jika ia melepaskan referensi ke objek yang berat, dan kemudian mendapat panggilan doWork () lainnya, itu akan gagal.
Jay Elston
-4

Untuk bahasa Berorientasi Kelas lainnya saya akan mengatakan memasukkannya ke dalam destructor, tetapi karena itu bukan pilihan saya pikir Anda dapat menimpa metode finalisasi. Dengan begitu ketika instance keluar dari ruang lingkup dan dikumpulkan sampah itu akan dirilis.

@Override
public void finalize() {
    this.release();
}

Saya tidak terlalu terbiasa dengan Java, tetapi saya pikir ini adalah tujuan finalisasi yang dimaksudkan.

http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#finalize ()

Dar Brett
sumber
7
Ini adalah solusi buruk untuk masalah itu karena alasan yang dikatakan Stephen dalam jawabannya -If you rely on finalizers to tidy up, you run into the problem that it can take a very long time for the tidy-up to happen. The finalizer will only be run after the GC decides that the object is no longer reachable. That may not happen until the JVM does a full collection.
Daenyth
4
Dan bahkan finalizer mungkin tidak benar-benar berjalan ...
jwenting
3
Finalizers terkenal tidak bisa diandalkan: mereka dapat berlari, mereka mungkin tidak pernah berlari, jika mereka berlari tidak ada jaminan kapan mereka akan berjalan. Bahkan arsitek Jawa seperti Josh Bloch mengatakan finalizers adalah ide yang buruk (referensi: Java Efektif (2nd Edition) ).
Masalah seperti ini membuat saya kangen C ++ dengan kehancuran deterministik dan RAII ...
Mark K Cowan