Menggunakan pengecekan tipe statis untuk melindungi dari kesalahan bisnis

13

Saya penggemar berat tipe statis memeriksa. Ini mencegah Anda membuat kesalahan bodoh seperti ini:

// java code
Adult a = new Adult();
a.setAge("Roger"); //static type checker would complain
a.setName(42); //and here too

Tapi itu tidak mencegah Anda melakukan kesalahan bodoh seperti ini:

Adult a = new Adult();
// obviously you've mixed up these fields, but type checker won't complain
a.setAge(150); // nobody's ever lived this old
a.setWeight(42); // a 42lb adult would have serious health issues

Masalahnya muncul ketika Anda menggunakan jenis yang sama untuk mewakili jenis informasi yang jelas berbeda. Saya sedang berpikir solusi yang baik untuk ini akan memperluas Integerkelas, hanya untuk mencegah kesalahan logika bisnis, tetapi tidak menambah fungsionalitas. Sebagai contoh:

class Age extends Integer{};
class Pounds extends Integer{};

class Adult{
    ...
    public void setAge(Age age){..}
    public void setWeight(Pounds pounds){...}
}

Adult a = new Adult();
a.setAge(new Age(42));
a.setWeight(new Pounds(150));

Apakah ini dianggap praktik yang baik? Atau ada masalah teknik yang tidak terduga di jalan dengan desain yang ketat?

J-bob
sumber
5
a.SetAge( new Age(150) )Masih tidak mau mengkompilasi?
John Wu
1
Tipe int Java memiliki rentang tetap. Jelas Anda ingin memiliki integer dengan rentang kustom, misal Integer <18, 110>. Anda mencari jenis penyempitan atau tipe dependen, yang ditawarkan beberapa bahasa (non-mainstream).
Theodoros Chatzigiannakis
3
Apa yang Anda bicarakan adalah cara terbaik untuk melakukan desain! Sayangnya, Java memiliki sistem tipe primitif sehingga sulit dilakukan dengan benar. Dan bahkan jika Anda mencoba melakukannya, itu mungkin menghasilkan efek samping seperti kinerja yang lebih rendah atau produktivitas pengembang yang lebih rendah.
Euforia
@ JohnWu ya contoh Anda masih akan dikompilasi, tetapi itu adalah satu-satunya titik kegagalan untuk logika itu. Setelah Anda mendeklarasikan new Age(...)objek, Anda tidak dapat menetapkannya secara salah ke variabel tipe Weightdi tempat lain. Ini mengurangi jumlah tempat di mana kesalahan bisa terjadi.
J-bob

Jawaban:

12

Anda pada dasarnya meminta sistem unit (tidak, bukan tes unit, "unit" seperti dalam "unit fisik", seperti meter, volt, dll.).

Dalam kode Anda Agemewakili waktu dan Poundsmewakili massa. Ini mengarah ke hal-hal seperti konversi unit, unit dasar, presisi, dll.


Ada upaya untuk memasukkan hal semacam itu ke Jawa, misalnya:

Dua yang terakhir tampaknya hidup dalam hal github ini: https://github.com/unitsofmeasurement


C ++ memiliki unit melalui Boost


LabView hadir dengan banyak unit .


Ada contoh lain dalam bahasa lain. (suntingan dipersilakan)


Apakah ini dianggap praktik yang baik?

Seperti yang Anda lihat di atas, semakin besar kemungkinan bahasa digunakan untuk menangani nilai dengan unit, semakin asli mendukung unit. LabView sering digunakan untuk berinteraksi dengan perangkat pengukuran. Karena itu masuk akal untuk memiliki fitur seperti itu dalam bahasa dan menggunakannya tentu akan dianggap sebagai praktik yang baik.

Tetapi dalam bahasa tingkat tinggi tujuan umum apa pun, di mana permintaan rendah untuk jumlah keras seperti itu, itu mungkin tidak terduga.

Atau ada masalah teknik yang tidak terduga di jalan dengan desain yang ketat?

Dugaan saya adalah: kinerja / memori. Jika Anda berurusan dengan banyak nilai, overhead objek per nilai mungkin menjadi masalah. Tapi seperti biasa: Optimalisasi prematur adalah akar dari semua kejahatan .

Saya pikir "masalah" yang lebih besar membuat orang terbiasa, karena unit biasanya didefinisikan secara implisit seperti:

class Adult
{
    ...
    public void setAge(int ageInYears){..}

Orang-orang akan bingung ketika mereka harus melewati objek sebagai nilai untuk sesuatu yang tampaknya dapat digambarkan dengan sederhana int, ketika mereka tidak terbiasa dengan sistem unit.

batal
sumber
"Orang-orang akan bingung ketika mereka harus melewati sebuah objek sebagai nilai untuk sesuatu yang tampaknya dapat digambarkan dengan sederhana int..." --- yang mana kita memiliki Principal of Least Surprise sebagai panduan di sini. Tangkapan yang bagus.
Greg Burghardt
1
Saya pikir rentang kustom memindahkan ini dari ranah pustaka unit dan ke tipe dependen
jk.
6

Berbeda dengan jawaban nol, mendefinisikan tipe untuk "unit" bisa bermanfaat jika bilangan bulat tidak cukup untuk menggambarkan pengukuran. Misalnya, berat badan sering diukur dalam beberapa unit dalam sistem pengukuran yang sama. Pikirkan "pound" dan "ons" atau "kilogram" dan "gram".

Jika Anda membutuhkan tingkat pengukuran yang lebih terperinci, menentukan jenis untuk unit ini bermanfaat:

public struct Weight {
    private int pounds;
    private int ounces;

    public Weight(int pounds, int ounces) {
        // Value range checks go here
        // Throw exception if ounces is greater than 16?
    }

    // Getters go here
}

Untuk sesuatu seperti "usia", saya sarankan untuk menghitungnya pada waktu berjalan berdasarkan tanggal lahir orang tersebut:

public class Adult {
    private Date birthDate;

    public Interval getCurrentAge() {
        return calculateAge(Date.now());
    }

    public Interval calculateAge(Date date) {
        // Return interval between birthDate and date
    }
}
Greg Burghardt
sumber
2

Apa yang tampaknya Anda cari dikenal sebagai tipe yang ditandai . Mereka adalah cara untuk mengatakan "ini adalah bilangan bulat yang mewakili usia" sementara "ini juga bilangan bulat tetapi merupakan bobot" dan "Anda tidak dapat menetapkan satu ke yang lain". Perhatikan, bahwa ini lebih jauh daripada unit fisik seperti meter atau kilogram: Saya mungkin punya dalam program saya "ketinggian orang" dan "jarak antara titik-titik di peta", keduanya diukur dalam meter, tetapi tidak kompatibel satu sama lain karena menetapkan satu ke yang lain tidak masuk akal dari perspektif logika bisnis.

Beberapa bahasa, seperti jenis dukungan Tag Scala cukup mudah (lihat tautan di atas). Di tempat lain, Anda bisa membuat kelas pembungkus sendiri, tetapi ini kurang nyaman.

Validasi, misalnya memeriksa bahwa tinggi badan seseorang "masuk akal" adalah masalah lain. Anda dapat memasukkan kode tersebut di Adultkelas Anda (konstruktor atau setter), atau di dalam kelas tipe / pembungkus yang ditandai. Di satu sisi, kelas bawaan seperti URLatau UUIDmemenuhi peran seperti itu (antara lain, misalnya menyediakan metode utilitas).

Apakah menggunakan jenis yang ditandai atau kelas pembungkus benar-benar akan membantu membuat kode Anda lebih baik, akan tergantung pada beberapa faktor. Jika objek Anda sederhana dan memiliki beberapa bidang, risiko menetapkannya salah rendah dan kode tambahan yang diperlukan untuk menggunakan jenis yang ditandai mungkin tidak sepadan dengan usaha. Dalam sistem yang kompleks dengan struktur kompleks dan banyak bidang (terutama jika banyak dari mereka memiliki tipe primitif yang sama), sebenarnya dapat membantu.

Dalam kode yang saya tulis, saya sering membuat kelas wrapper jika saya membagikan peta. Jenis seperti Map<String, String>itu sangat buram sendiri, jadi membungkusnya di kelas dengan nama yang bermakna seperti NameToAddressbanyak membantu. Tentu saja, dengan jenis yang ditandai, Anda dapat menulis Map<Name, Address>dan tidak perlu pembungkus untuk seluruh peta.

Namun, untuk tipe sederhana seperti Strings atau Integer, saya menemukan kelas wrapper (di Jawa) terlalu merepotkan. Logika bisnis reguler tidak terlalu buruk, tetapi timbul sejumlah masalah dengan membuat serialisasi tipe-tipe ini ke JSON, memetakannya ke objek DB, dll. Anda dapat menulis pemetaan dan pengait untuk semua kerangka kerja besar (mis. Data Jackson dan Spring), tetapi kerja ekstra dan pemeliharaan terkait dengan kode ini akan mengimbangi apa pun yang Anda peroleh dari menggunakan pembungkus ini. Tentu saja, YMMV dan di sistem lain, keseimbangannya mungkin berbeda.

Michał Kosmulski
sumber