Haruskah seorang pengambil rajutan pengecualian jika objeknya memiliki keadaan tidak valid?

55

Saya sering mengalami masalah ini, terutama di Jawa, bahkan jika saya pikir itu adalah masalah OOP umum. Yaitu: menaikkan pengecualian mengungkapkan masalah desain.

Misalkan saya memiliki kelas yang memiliki String namebidang dan String surnamebidang.

Kemudian ia menggunakan bidang-bidang itu untuk menulis nama lengkap seseorang untuk menampilkannya pada semacam dokumen, misalnya faktur.

public void String name;
public void String surname;

public String getCompleteName() {return name + " " + surname;}

public void displayCompleteNameOnInvoice() {
    String completeName = getCompleteName();
    //do something with it....
}

Sekarang saya ingin memperkuat perilaku kelas saya dengan melemparkan kesalahan jika displayCompleteNameOnInvoicedipanggil sebelum nama telah ditetapkan. Sepertinya ide yang bagus, bukan?

Saya dapat menambahkan kode yang meningkatkan pengecualian ke getCompleteNamemetode. Tetapi dengan cara ini saya melanggar kontrak 'implisit' dengan pengguna kelas; pada umumnya getter tidak seharusnya melempar pengecualian jika nilainya tidak disetel. Ok, ini bukan pengambil standar karena tidak mengembalikan satu bidang, tetapi dari sudut pandang pengguna perbedaannya mungkin terlalu halus untuk dipikirkan.

Atau saya bisa melempar pengecualian dari dalam displayCompleteNameOnInvoice. Tetapi untuk melakukannya saya harus menguji secara langsung nameatau surnamebidang dan melakukannya saya akan melanggar abstraksi yang diwakili oleh getCompleteName. Metode ini bertanggung jawab untuk memeriksa dan membuat nama lengkap. Bahkan bisa memutuskan, mendasarkan keputusan pada data lain, bahwa dalam beberapa kasus itu cukup surname.

Jadi satu-satunya kemungkinan tampaknya mengubah semantik metode getCompleteNameuntuk composeCompleteName, yang menunjukkan lebih 'aktif' perilaku dan, dengan itu, kemampuan melempar pengecualian.

Apakah ini solusi desain yang lebih baik? Saya selalu mencari keseimbangan terbaik antara kesederhanaan dan kebenaran. Apakah ada referensi desain untuk masalah ini?

AgostinoX
sumber
8
"secara umum getter tidak seharusnya melempar pengecualian jika nilainya tidak disetel." - Apa yang seharusnya mereka lakukan?
Rotem
2
@ Scriptin Kemudian mengikuti logika itu, displayCompleteNameOnInvoicebisa saja melempar pengecualian jika getCompleteNamekembali null, bukan?
Rotem
2
@Rem itu bisa. Pertanyaannya adalah apakah harus.
scriptin
22
Pada subjeknya, Programmer Falsehoods Believe About Names sangat baik membaca mengapa 'nama depan' dan 'nama keluarga' adalah konsep yang sangat sempit.
Lars Viklund
9
Jika objek Anda dapat memiliki status tidak valid, Anda telah mengacaukannya.
user253751

Jawaban:

151

Jangan izinkan kelas Anda dibangun tanpa memberikan nama.

DeadMG
sumber
9
Ini menarik tetapi solusi yang cukup radikal. Berkali-kali kelas Anda harus lebih cair, memungkinkan negara bagian yang menjadi masalah hanya jika dan ketika metode tertentu dipanggil, Anda akan berakhir dengan proliferasi kelas kecil yang membutuhkan terlalu banyak penjelasan untuk digunakan.
AgostinoX
71
Ini bukan komentar. Jika dia menerapkan saran dalam jawabannya, dia tidak akan lagi memiliki masalah yang dijelaskan. Itu adalah jawaban.
DeadMG
14
@ AgostinoX: Anda hanya harus membuat kelas Anda lancar jika benar-benar diperlukan. Kalau tidak, mereka harus sevariatif mungkin.
DeadMG
25
@gnat: Anda membuat karya hebat di sini mempertanyakan kualitas setiap pertanyaan dan setiap jawaban di PSE, tapi kadang-kadang Anda IMHO agak terlalu rumit.
Doc Brown
9
Jika mereka bisa opsional opsional, tidak ada alasan baginya untuk melemparkan di tempat pertama.
DeadMG
75

Jawaban yang diberikan oleh DeadMG cukup kuat, tetapi izinkan saya mengatakannya sedikit berbeda: Hindari memiliki benda dengan keadaan tidak valid. Objek harus "utuh" dalam konteks tugas yang dipenuhi. Atau seharusnya tidak ada.

Jadi jika Anda ingin fluiditas, gunakan sesuatu seperti Pola Builder . Atau hal lain yang merupakan reifikasi terpisah dari suatu objek dalam konstruksi yang bertentangan dengan "hal yang nyata", di mana yang terakhir dijamin memiliki keadaan yang valid dan merupakan salah satu yang benar-benar memperlihatkan operasi yang didefinisikan pada keadaan itu (misalnya getCompleteName).

back2dos
sumber
7
So if you want fluidity, use something like the builder pattern- fluiditas, atau kekekalan (kecuali itu yang Anda maksudkan entah bagaimana). Jika seseorang ingin membuat objek yang tidak dapat diubah, tetapi perlu menunda pengaturan beberapa nilai, maka pola yang harus diikuti adalah mengaturnya satu per satu pada objek pembangun dan hanya membuat objek yang tidak berubah setelah kami mengumpulkan semua nilai yang diperlukan untuk yang benar negara ...
Konrad Morawski
1
@KonradMorawski: Saya bingung. Apakah Anda mengklarifikasi atau bertentangan dengan pernyataan saya? ;)
back2dos
3
Saya mencoba melengkapinya ...
Konrad Morawski
40

Tidak memiliki objek dengan keadaan tidak valid.

Tujuan konstruktor atau pembangun adalah untuk mengatur keadaan objek menjadi sesuatu yang konsisten dan dapat digunakan. Tanpa jaminan ini, masalah muncul dengan keadaan inisialisasi objek yang tidak benar. Masalah ini menjadi diperparah jika Anda berurusan dengan konkurensi dan sesuatu dapat mengakses objek sebelum Anda selesai mengaturnya.

Jadi, pertanyaan untuk bagian ini adalah "mengapa Anda membiarkan nama atau nama keluarga menjadi nol?" Jika itu bukan sesuatu yang valid di kelas agar bisa berfungsi dengan baik, jangan izinkan. Memiliki konstruktor atau pembangun yang memvalidasi pembuatan objek dengan benar dan jika tidak benar, angkat masalah pada titik itu. Anda juga dapat menggunakan salah satu @NotNullanotasi yang ada untuk membantu mengomunikasikan bahwa "ini tidak boleh nol" dan menerapkannya dalam pengkodean dan analisis.

Dengan jaminan ini di tempat, menjadi lebih mudah untuk alasan tentang kode, apa yang dilakukannya, dan tidak harus membuang pengecualian di tempat-tempat aneh atau melakukan pemeriksaan berlebihan di sekitar fungsi pengambil.

Getters yang melakukan lebih banyak.

Ada sedikit di luar sana tentang hal ini. Anda sudah mendapatkan Berapa Banyak Logika dalam Getters dan Apa yang harus diizinkan di dalam getter dan setter? dari sini dan getter dan setter melakukan logika tambahan pada Stack Overflow. Ini adalah masalah yang muncul berulang kali dalam desain kelas.

Inti dari ini berasal dari:

pada umumnya getter tidak seharusnya melempar pengecualian jika nilainya tidak ditetapkan

Dan kamu benar. Seharusnya tidak. Mereka harus terlihat 'bodoh' ke seluruh dunia dan melakukan apa yang diharapkan. Menempatkan terlalu banyak logika di sana mengarah ke masalah di mana hukum yang paling mengejutkan dilanggar. Dan mari kita hadapi itu, Anda benar-benar tidak ingin membungkus getter dengan try catchkarena kita tahu betapa kita suka melakukan itu secara umum.

Ada juga situasi di mana Anda harus menggunakan getFoometode seperti kerangka JavaBeans dan ketika Anda memiliki sesuatu dari panggilan EL yang mengharapkan mendapatkan kacang (sehingga <% bar.foo %>benar - benar memanggil getFoo()di kelas - mengesampingkan 'harus kacang harus melakukan komposisi atau harus yang dibiarkan begitu saja? 'karena seseorang dapat dengan mudah menemukan situasi di mana satu atau yang lainnya jelas dapat menjadi jawaban yang tepat)

Sadar juga bahwa adalah mungkin bagi seorang pengambil yang diberikan untuk ditentukan dalam antarmuka atau telah menjadi bagian dari API publik yang sebelumnya terbuka untuk kelas yang mendapatkan refactored (versi sebelumnya hanya memiliki 'completeName' yang dikembalikan dan sebuah refactoring pecah menjadi dua bidang).

Pada akhir hari...

Lakukan hal yang paling mudah untuk dipikirkan. Anda akan menghabiskan lebih banyak waktu mempertahankan kode ini daripada menghabiskan waktu mendesainnya (meskipun semakin sedikit waktu yang Anda habiskan untuk mendesain, semakin banyak waktu yang Anda habiskan untuk pemeliharaannya). Pilihan yang ideal adalah mendesain sedemikian rupa sehingga tidak akan memakan banyak waktu untuk mempertahankannya, tetapi jangan duduk di sana memikirkannya selama berhari-hari - kecuali jika ini benar-benar merupakan pilihan desain yang akan berimplikasi beberapa hari kemudian .

Mencoba untuk tetap dengan kemurnian semantik dari makhluk pengambil private T foo; T getFoo() { return foo; }akan membuat Anda mendapat masalah di beberapa titik. Terkadang kode itu tidak sesuai dengan model itu dan liuk yang dilalui seseorang untuk membuatnya tetap seperti itu tidak masuk akal ... dan pada akhirnya membuatnya lebih sulit untuk dirancang.

Kadang-kadang terima bahwa cita-cita desain tidak dapat diwujudkan seperti yang Anda inginkan dalam kode. Pedoman adalah pedoman - bukan belenggu.

Komunitas
sumber
2
Jelas contoh saya hanyalah alasan untuk meningkatkan gaya pengkodean dengan alasan, bukan masalah pemblokiran. Tapi saya cukup bingung dengan pentingnya yang Anda dan orang lain tampaknya berikan kepada konstruktor. Secara teori mereka menepati janji mereka. Dalam praktiknya, mereka menjadi rumit untuk dipertahankan dengan warisan, tidak dapat digunakan ketika Anda mengekstrak antarmuka dari kelas, tidak diadopsi oleh orang Jawa dan kerangka kerja lain seperti musim semi jika saya tidak salah. Di bawah payung ide 'kelas' hidup banyak kategori objek yang berbeda. Objek nilai mungkin memiliki konstruktor validasi, tetapi ini hanya satu tipe.
AgostinoX
2
Mampu alasan tentang kode adalah kunci untuk dapat menulis kode menggunakan API, atau mampu mempertahankan kode. Jika sebuah kelas memiliki konstruktor yang memungkinkannya berada dalam keadaan tidak valid, itu membuat jauh lebih sulit untuk memikirkan "baik, apa fungsinya?" karena sekarang Anda harus mempertimbangkan lebih banyak situasi daripada yang dipertimbangkan atau dimaksudkan oleh perancang kelas. Beban konseptual ekstra ini datang dengan penalti lebih banyak bug dan waktu yang lebih lama untuk menulis kode. Di sisi lain, jika Anda dapat melihat kode dan mengatakan "nama dan nama keluarga tidak akan pernah nol", menjadi lebih mudah untuk menulis kode menggunakan mereka.
2
Tidak lengkap tidak selalu berarti tidak valid. Faktur tanpa tanda terima dapat sedang dalam proses, dan API publik yang disajikannya akan mencerminkan bahwa itu adalah keadaan yang valid. Jika surnameatau namenull adalah keadaan yang valid untuk objek, maka tangani sesuai itu. Tetapi jika itu bukan keadaan yang valid - menyebabkan metode di dalam kelas untuk melempar pengecualian, itu harus dicegah berada dalam keadaan itu dari titik itu diinisialisasi. Anda bisa mengedarkan objek yang tidak lengkap, tetapi mengedarkan objek yang tidak valid bermasalah. Jika nama nol adalah luar biasa, cobalah untuk mencegahnya lebih awal daripada nanti.
2
Posting blog terkait untuk dibaca: Merancang Inisialisasi Objek dan perhatikan empat pendekatan untuk menginisialisasi variabel instan. Salah satunya adalah membiarkannya dalam keadaan tidak valid, meskipun perhatikan bahwa itu melempar pengecualian. Jika Anda mencoba untuk menghindari ini, pendekatan itu tidak valid dan Anda perlu bekerja dengan pendekatan lain - yang melibatkan memastikan objek yang valid setiap saat. Dari blog: "Objek yang memiliki status tidak valid lebih sulit digunakan dan lebih sulit dipahami daripada yang selalu valid." dan itu kuncinya.
5
"Lakukan hal yang paling mudah untuk dipikirkan." Itu
Ben
5

Saya bukan orang Jawa, tetapi ini tampaknya mematuhi kedua kendala yang Anda sajikan.

getCompleteNametidak melempar pengecualian jika nama tidak diinisialisasi, dan displayCompleteNameOnInvoicetidak.

public String getCompleteName()
{
    if (name == null || surname == null)
    {
        return null;
    }
    return name + " " + surname;
}

public void displayCompleteNameOnInvoice() 
{
    String completeName = getCompleteName();
    if (completeName == null)
    {
        //throw an exception.
    }
    //do something with it....
}
Rotem
sumber
Untuk memperluas mengapa ini adalah solusi yang tepat: Dengan cara ini penelepon getCompleteName()tidak perlu peduli apakah properti itu benar-benar disimpan atau jika itu dihitung dengan cepat.
Bart van Ingen Schenau
18
Untuk memperluas mengapa solusi ini gila, Anda harus memeriksa nol pada setiap panggilan getCompleteName, yang mengerikan.
DeadMG
Tidak, @DeadMG, Anda hanya perlu memeriksanya jika pengecualian harus dilemparkan jika getCompleteName mengembalikan nol. Begitulah seharusnya. Solusi ini benar.
Dawood mengatakan mengembalikan Monica
1
@DeadMG Ya, tapi kami tidak tahu apa yang LAIN gunakan dari getCompleteName()sisa kelas, atau bahkan di bagian lain dari program. Dalam beberapa kasus, pengembalian nullmungkin persis apa yang diperlukan. Tetapi sementara ada SATU metode yang specnya "melempar pengecualian dalam kasus ini", itu PERSIS apa yang harus kita kode.
Dawood mengatakan mengembalikan Monica
4
Mungkin ada situasi di mana ini adalah solusi terbaik, tetapi saya tidak bisa membayangkannya. Untuk kebanyakan kasus, ini tampaknya terlalu rumit: Satu metode melempar dengan data yang tidak lengkap, yang lain mengembalikan nol. Saya khawatir ini kemungkinan akan digunakan secara tidak benar oleh penelepon.
sleske
4

Sepertinya tidak ada yang menjawab pertanyaan Anda.
Tidak seperti apa yang orang suka percaya, "menghindari benda yang tidak valid" tidak sering merupakan solusi praktis.

Jawabannya adalah:

Ya seharusnya.

Contoh mudah: FileStream.Lengthdalam C # /. NET , yang melempar NotSupportedExceptionjika file tidak dapat dicari.

Mehrdad
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
maple_shaft
1

Anda memiliki seluruh nama memeriksa terbalik.

getCompleteName()tidak harus tahu fungsi mana yang akan memanfaatkannya. Dengan demikian, tidak memiliki namesaat memanggil displayCompleteNameOnInvoice()bukan getCompleteName()tanggung jawab.

Tapi, namemerupakan prasyarat yang jelas untuk displayCompleteNameOnInvoice(). Dengan demikian, ia harus mengambil tanggung jawab untuk memeriksa negara (Atau harus mendelegasikan tanggung jawab untuk memeriksa ke metode lain, jika Anda mau).

sampathsris
sumber