Bagaimana cara menyederhanakan kelas stateful kompleks saya dan pengujiannya?

9

Saya dalam proyek sistem terdistribusi yang ditulis dalam java di mana kami memiliki beberapa kelas yang sesuai dengan objek bisnis dunia nyata yang sangat kompleks. Objek-objek ini memiliki banyak metode yang sesuai dengan tindakan yang dapat diterapkan pengguna (atau agen lain) pada objek tersebut. Akibatnya, kelas-kelas ini menjadi sangat kompleks.

Pendekatan arsitektur sistem umum telah menyebabkan banyak perilaku terkonsentrasi pada beberapa kelas dan banyak skenario interaksi yang mungkin.

Sebagai contoh dan untuk menjaga hal-hal mudah dan jelas, katakanlah Robot dan Mobil adalah kelas dalam proyek saya.

Jadi, di kelas Robot saya akan memiliki banyak metode dalam pola berikut:

  • tidur(); isSleepAvaliable ();
  • bangun(); isAwakeAvaliable ();
  • berjalan (Arah); isWalkAvaliable ();
  • menembak (Arah); isShootAvaliable ();
  • turnOnAlert (); isTurnOnAlertAvailable ();
  • turnOffAlert (); isTurnOffAlertAvailable ();
  • mengisi ulang (); isRechargeAvailable ();
  • matikan(); isPowerOffAvailable ();
  • stepInCar (Mobil); isStepInCarAvailable ();
  • stepOutCar (Mobil); isStepOutCarAvailable ();
  • Penghancuran diri(); isSelfDestructAvailable ();
  • mati(); isDieAvailable ();
  • hidup(); isAwake (); isAlertOn (); getBatteryLevel (); getCurrentRidingCar (); getAmmo ();
  • ...

Di kelas Mobil, akan serupa:

  • nyalakan(); isTurnOnAvaliable ();
  • matikan(); isTurnOffAvaliable ();
  • berjalan (Arah); isWalkAvaliable ();
  • mengisi bahan bakar (); isRefuelAvailable ();
  • Penghancuran diri(); isSelfDestructAvailable ();
  • jatuh(); isCrashAvailable ();
  • isOperational (); isOn (); getFuelLevel (); getCurrentPassenger ();
  • ...

Masing-masing (Robot dan Mobil) diimplementasikan sebagai mesin negara, di mana beberapa tindakan mungkin di beberapa negara dan beberapa tidak. Tindakan mengubah status objek. Metode tindakan melempar IllegalStateExceptionketika dipanggil dalam keadaan tidak valid dan isXXXAvailable()metode mengetahui apakah tindakan itu mungkin dilakukan saat itu. Meskipun beberapa mudah disimpulkan dari keadaan (misalnya, dalam kondisi tidur, tersedia terjaga), beberapa tidak (untuk menembak, itu harus terjaga, hidup, memiliki amunisi dan tidak mengendarai mobil).

Lebih lanjut, interaksi antara objek juga rumit. Misalnya, Mobil hanya dapat menampung satu penumpang Robot, jadi jika yang lain mencoba masuk, pengecualian harus dilemparkan; Jika mobil menabrak, penumpang harus mati; Jika robot mati di dalam kendaraan, dia tidak bisa keluar, bahkan jika mobil itu sendiri ok; Jika Robot ada di dalam Mobil, dia tidak bisa masuk yang lain sebelum melangkah keluar; dll.

Hasilnya, seperti yang sudah saya katakan, kelas-kelas ini menjadi sangat kompleks. Untuk memperburuk keadaan, ada ratusan skenario yang mungkin terjadi ketika Robot dan Mobil berinteraksi. Lebih jauh, banyak dari logika itu perlu mengakses data jarak jauh di sistem lain. Hasilnya adalah unit-testing menjadi sangat sulit dan kami memiliki banyak masalah pengujian, satu menyebabkan yang lain dalam lingkaran setan:

  • Pengaturan testcases sangat kompleks, karena mereka perlu menciptakan dunia yang sangat kompleks untuk berolahraga.
  • Jumlah tes sangat besar.
  • Test suite membutuhkan waktu beberapa jam untuk berjalan.
  • Cakupan pengujian kami sangat rendah.
  • Kode pengujian cenderung ditulis berminggu-minggu atau berbulan-bulan lebih lambat daripada kode yang mereka uji, atau tidak pernah sama sekali.
  • Banyak tes yang rusak juga, terutama karena persyaratan kode yang diuji berubah.
  • Beberapa skenario sangat kompleks, sehingga gagal pada batas waktu selama penyiapan (kami mengonfigurasi batas waktu dalam setiap pengujian, dalam kasus terburuk yang berlangsung 2 menit dan bahkan selama ini mereka kehabisan waktu, kami memastikan bahwa itu bukan loop yang tidak terbatas).
  • Bug secara teratur masuk ke lingkungan produksi.

Skenario Robot dan Mobil itu terlalu menyederhanakan apa yang kita miliki dalam kenyataan. Jelas, situasi ini tidak dapat dikelola. Jadi, saya meminta bantuan dan saran untuk: 1, Mengurangi kompleksitas kelas; 2. Sederhanakan skenario interaksi antara objek saya; 3. Kurangi waktu pengujian dan jumlah kode yang akan diuji.

EDIT:
Saya pikir saya tidak jelas tentang mesin negara. Robot itu sendiri adalah mesin negara, dengan negara "tidur", "terjaga", "mengisi ulang", "mati", dll. Mobil adalah mesin negara lain.

EDIT 2: Jika Anda ingin tahu tentang apa sebenarnya sistem saya, kelas-kelas yang berinteraksi adalah hal-hal seperti Server, Alamat IP, Disk, Cadangan, Pengguna, SoftwareLicense, dll. Skenario Robot dan Mobil hanyalah kasus yang saya temukan itu akan cukup sederhana untuk menjelaskan masalah saya.

Victor Stafusa
sumber
apakah Anda mempertimbangkan untuk bertanya di Code Review.SE ? Selain itu, untuk desain seperti milik Anda, saya akan mulai berpikir tentang refactoring jenis Extract Class
nyamuk
Saya mempertimbangkan Tinjauan Kode, tetapi itu bukan tempat yang tepat. Masalah utama tidak ada dalam kode itu sendiri, masalahnya adalah dalam sistem pendekatan arsitektur umum yang mengarah ke banyak perilaku yang terkonsentrasi pada beberapa kelas dan banyak kemungkinan skenario interaksi.
Victor Stafusa
@gnat Bisakah Anda memberikan contoh bagaimana saya akan mengimplementasikan Kelas Ekstrak dalam skenario Robot dan Mobil yang diberikan?
Victor Stafusa
Saya akan mengekstraksi hal-hal yang berhubungan dengan Mobil dari Robot ke dalam kelas yang terpisah. Saya juga mengekstrak semua metode yang berkaitan dengan sleep + bangun ke kelas khusus. "Calon" lain yang tampaknya layak untuk diekstraksi adalah metode pengisian daya +, hal-hal terkait gerakan. Dll. Catatan karena ini adalah refactoring, API eksternal untuk robot mungkin harus tetap; pada tahap pertama saya hanya akan memodifikasi internal. BTDTGTTS
nyamuk
Ini bukan pertanyaan Peninjauan Kode - arsitektur di luar topik di sana.
Michael K

Jawaban:

8

The Negara pola desain mungkin berguna, jika Anda belum menggunakannya.

Ide utama adalah bahwa Anda membuat kelas batin untuk setiap negara berbeda - sehingga untuk melanjutkan contoh, Anda SleepingRobot, AwakeRobot, RechargingRobotdan DeadRobotsemua akan kelas, menerapkan antarmuka umum.

Metode pada Robotkelas (seperti sleep()dan isSleepAvaliable()) memiliki implementasi sederhana yang mendelegasikan ke kelas batin saat ini.

Perubahan status diimplementasikan dengan menukar kelas batin saat ini dengan yang berbeda.

Keuntungan dengan pendekatan ini adalah bahwa masing-masing kelas negara secara dramatis lebih sederhana (karena hanya mewakili satu negara yang mungkin), dan dapat diuji secara independen. Bergantung pada bahasa implementasi Anda (tidak ditentukan), Anda mungkin masih terkendala untuk memiliki semuanya dalam file yang sama, atau Anda mungkin dapat membagi hal-hal menjadi file sumber yang lebih kecil.

Bevan
sumber
Saya menggunakan java.
Victor Stafusa
Saran yang bagus Dengan cara ini setiap implementasi memiliki fokus yang jelas yang dapat diuji secara individual tanpa memiliki kelas junit 2.000 baris menguji semua negara secara bersamaan.
OliverS
3

Saya tidak tahu kode Anda, tetapi dengan mengambil contoh metode "sleep", saya akan menganggap itu adalah sesuatu seperti kode "simplistic" berikut:

public void sleep() {
 if(!dead && awake) {
  sleeping = true;
  awake = false;
  this.updateState(SLEEPING);
 }
 throw new IllegalArgumentException("robot is either dead or not awake");
}

Saya pikir Anda harus membuat perbedaan antara tes integrasi dan tes unit . Menulis tes yang berjalan melalui seluruh kondisi mesin Anda tentu merupakan tugas besar. Menulis tes unit yang lebih kecil yang menguji apakah metode tidur Anda berfungsi dengan benar lebih mudah. Pada titik ini Anda tidak perlu tahu apakah status mesin telah diperbarui dengan benar atau apakah "mobil" dengan benar merespons kenyataan bahwa keadaan mesin telah diperbarui oleh "robot" ... dll. Anda mendapatkannya.

Dengan kode di atas, saya akan mengejek objek "machineState" dan tes pertama saya adalah:

testSleep_dead() {
 robot.dead = true;
 robot.awake = false;
 robot.setState(AWAKE);
 try {
  robot.sleep();
  fail("should have got an exception");
 } catch(Exception e) {
  assertTrue(e instanceof IllegalArgumentException);
  assertEquals("robot is either dead or not awake", e.getMessage());
 }
}

Pendapat pribadi saya adalah menulis tes unit sekecil itu harus menjadi hal pertama yang harus dilakukan. Anda menulis itu:

Pengaturan testcases sangat kompleks, karena mereka perlu menciptakan dunia yang sangat kompleks untuk berolahraga.

Menjalankan tes kecil ini harus sangat cepat, dan Anda seharusnya tidak memiliki apa pun untuk diinisialisasi sebelumnya seperti "dunia kompleks" Anda. Misalnya, jika ini adalah aplikasi yang didasarkan pada wadah IOC (katakanlah, Spring), Anda tidak perlu menginisialisasi konteks selama tes unit.

Setelah Anda menyelesaikan persentase yang baik dari kode kompleks Anda dengan tes unit, Anda dapat mulai membangun tes integrasi yang lebih memakan waktu dan lebih kompleks.

Akhirnya, ini bisa dilakukan apakah kode Anda rumit (seperti yang Anda katakan sekarang), atau setelah Anda refactored.

Jalayn
sumber
Saya pikir saya tidak jelas tentang mesin negara. Robot itu sendiri adalah mesin negara, dengan negara "tidur", "terjaga", "mengisi ulang", "mati", dll. Mobil adalah mesin negara lain.
Victor Stafusa
@ Viktor OK, jangan ragu untuk memperbaiki kode contoh saya jika Anda mau. Kecuali Anda memberi tahu saya sebaliknya, saya pikir poin saya pada unit test masih valid, saya harap setidaknya.
Jalayn
Saya mengoreksi contohnya. Saya tidak memiliki hak istimewa untuk membuatnya mudah terlihat, jadi harus ditinjau oleh rekan sebelumnya. Komentar Anda sangat membantu.
Victor Stafusa
2

Saya sedang membaca bagian "Asal" dari artikel Wikipedia tentang Prinsip Segregasi Antarmuka , dan saya teringat akan pertanyaan ini.

Saya akan mengutip artikel itu. Masalahnya: "... satu kelas Pekerjaan utama .... kelas gemuk dengan banyak metode khusus untuk berbagai klien yang berbeda." Solusinya: "... lapisan antarmuka antara kelas Pekerjaan dan semua kliennya ..."

Masalah Anda tampaknya permutasi dari yang dimiliki Xerox. Alih-alih satu kelas gemuk Anda memiliki dua, dan dua kelas gemuk ini berbicara satu sama lain, bukan banyak klien.

Bisakah Anda mengelompokkan metode berdasarkan jenis interaksi dan kemudian membuat kelas antarmuka untuk setiap jenis? Misalnya: RobotPowerInterface, RobotNavigationInterface, RobotAlarmInterface kelas?

Jeff
sumber