Gaya untuk aliran kontrol dengan pemeriksaan validasi


27

Saya menemukan diri saya menulis banyak kode seperti ini:

int myFunction(Person* person) {
  int personIsValid = !(person==NULL);
  if (personIsValid) {
     // do some stuff; might be lengthy
     int myresult = whatever;
     return myResult;
  }
  else {
    return -1;
  }
}

Ini bisa menjadi sangat berantakan, terutama jika beberapa cek terlibat. Dalam kasus seperti itu, saya telah bereksperimen dengan gaya alternatif, seperti yang ini:

int netWorth(Person* person) {
  if (Person==NULL) {
    return -1;
  }
  if (!(person->isAlive))  {
    return -1;
  }
  int assets = person->assets;
  if (assets==-1)  {
    return -1;
  }
  int liabilities = person->liabilities;
  if (liabilities==-1) {
    return -1;
  }
  return assets - liabilities;
}

Saya tertarik pada komentar tentang pilihan gaya di sini. [Jangan terlalu khawatir tentang perincian pernyataan individu; itu adalah aliran kontrol keseluruhan yang menarik minat saya.]


8
Izinkan saya menunjukkan bahwa Anda memiliki kesalahan spesifikasi yang cukup serius dalam contoh Anda. Jika, misalnya, aset == 42 dan kewajiban == 43, Anda akan menyatakan orang tersebut tidak ada.
John R. Strohm

Tidak akan membuang pengecualian dan membiarkan kode klien mengelola validasi menjadi lebih baik?
Tulains Córdova

@ TulainsCórdova Pengecualian mungkin tidak tersedia, atau mungkin data yang tidak valid tidak cukup luar biasa sehingga dampak kinerja membangun jejak tumpukan dll. Dapat diterima.
Hulk

Jawaban:


27

Untuk masalah seperti ini Martin Fowler mengusulkan Pola spesifikasi :

... pola desain, di mana aturan bisnis dapat digabungkan kembali dengan merantai aturan bisnis bersama-sama menggunakan logika boolean.
 
Pola spesifikasi menguraikan aturan bisnis yang dapat digabungkan dengan aturan bisnis lainnya. Dalam pola ini, unit logika bisnis mewarisi fungsinya dari kelas spesifikasi agregat abstrak. Kelas Spesifikasi Komposit memiliki satu fungsi yang disebut IsSatisfiedBy yang mengembalikan nilai boolean. Setelah instantiasi, spesifikasi "dirantai" dengan spesifikasi lain, membuat spesifikasi baru mudah dipelihara, namun logika bisnis dapat disesuaikan. Lebih jauh lagi setelah instantiation, logika bisnis dapat, melalui metode invokasi atau inversi kontrol, dapat diubah kondisinya untuk menjadi delegasi kelas lain seperti repositori persistensi ...

Di atas kedengarannya agak tinggi-alis (setidaknya bagi saya), tetapi ketika saya mencobanya dalam kode saya itu berjalan cukup lancar dan ternyata mudah diimplementasikan dan dibaca.

Cara saya melihatnya, ide utama adalah "mengekstrak" kode yang melakukan pemeriksaan ke dalam metode / objek khusus.

Dengan netWorthcontoh Anda , ini bisa terlihat sebagai berikut:

int netWorth(Person* person) {
  if (isSatisfiedBySpec(person)) {
    return person->assets - person->liabilities;
  }
  log("person doesn't satisfy spec");
  return -1;
}

#define BOOLEAN int // assuming C here
BOOLEAN isSatisfiedBySpec(Person* person) {
  return Person != NULL
      && person->isAlive
      && person->assets != -1
      && person->liabilities != -1;
}

Kasing Anda terlihat agak sederhana sehingga semua cek terlihat OK untuk masuk dalam daftar polos dalam satu metode. Saya sering harus membagi lebih banyak metode untuk membuatnya lebih baik membaca.

Saya juga biasanya mengelompokkan / mengekstrak "spec" metode terkait dalam objek khusus, meskipun kasing Anda terlihat oke tanpa itu.

  // ...
  Specification s, *spec = initialize(s, person);
  if (spec->isSatisfied()) {
    return person->assets - person->liabilities;
  }
  log("person doesn't satisfy spec");
  return -1;
  // ...

Pertanyaan ini di Stack Overflow merekomendasikan beberapa tautan selain yang disebutkan di atas: Contoh Pola Spesifikasi . Secara khusus, jawaban menyarankan Dimecasts 'Mempelajari Pola Spesifikasi' untuk panduan contoh dan menyebutkan makalah "Spesifikasi" yang ditulis oleh Eric Evans dan Martin Fowler .


8

Saya merasa lebih mudah untuk memindahkan validasi ke fungsinya sendiri, ini membantu menjaga maksud fungsi lainnya lebih bersih, jadi contoh Anda akan seperti ini.

int netWorth(Person* person) { 
    if(validPerson(person)) {
        int assets = person->assets;
        int liabilities = person->liabilities;
        return assets - liabilities;
    }
    else {
        return -1;
    }
}

bool validPerson(Person* person) { 
    if(person!=NULL && person->isAlive
      && person->assets !=-1 && person->liabilities != -1)
        return true;
    else
        return false;
}

2
Mengapa Anda memiliki di ifdalamnya validPerson? Cukup kembali person!=NULL && person->isAlive && person->assets !=-1 && person->liabilities != -1saja.
David Hammen

3

Satu hal yang saya lihat berhasil dengan baik adalah memperkenalkan lapisan validasi ke dalam kode Anda. Pertama, Anda memiliki metode yang melakukan semua validasi yang berantakan dan mengembalikan kesalahan (seperti -1dalam contoh Anda di atas) ketika terjadi kesalahan. Ketika validasi selesai, fungsi memanggil fungsi lain untuk melakukan pekerjaan yang sebenarnya. Sekarang fungsi ini tidak perlu melakukan semua langkah validasi itu karena sudah harus dilakukan. Artinya, fungsi kerja mengasumsikan bahwa input tersebut valid. Bagaimana seharusnya Anda menangani asumsi? Anda menegaskannya dalam kode.

Saya pikir ini membuat kodenya sangat mudah dibaca. Metode validasi berisi semua kode berantakan untuk menangani kesalahan di sisi pengguna. Metode kerja dengan bersih mendokumentasikan asumsi dengan penegasan dan kemudian tidak harus bekerja dengan data yang berpotensi tidak valid.

Pertimbangkan refactoring dari contoh Anda ini:

int myFunction(Person* person) {
  int personIsValid = !(person==NULL);
  if (personIsValid) {
     return myFunctionWork(person)
  }
  else {
    return -1;
  }
}

int myFunction(Person *person) {
  assert( person != NULL);  
  // Do work and return
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.