Pola desain untuk "operasi pada objek diizinkan, hanya jika objek dalam keadaan tertentu"

8

Sebagai contoh:

Hanya lamaran pekerjaan yang belum ditinjau atau disetujui, yang dapat diperbarui. Dengan kata lain, seseorang dapat memperbarui formulir alat kerjanya sampai HR mulai memeriksanya, atau sudah diterima.

Jadi lamaran kerja bisa di 4 negara:

DITERAPKAN (kondisi awal), IN_REVIEW, DISETUJUI, DITERBITKAN

Bagaimana saya mencapai perilaku seperti itu?

Tentunya saya dapat menulis metode pembaruan () di kelas Aplikasi, memeriksa status Aplikasi, dan tidak melakukan apa pun atau melemparkan pengecualian jika Aplikasi tidak dalam status yang diperlukan

Tetapi kode semacam ini tidak membuatnya jelas aturan seperti itu ada, itu memungkinkan siapa saja untuk memanggil metode pembaruan (), dan hanya setelah gagal klien tahu operasi seperti itu tidak diizinkan. Oleh karena itu klien perlu menyadari bahwa upaya seperti itu mungkin gagal, oleh karena itu berhati-hatilah. Klien menyadari hal-hal seperti itu juga berarti bahwa logika bocor ke luar.

Saya mencoba membuat kelas yang berbeda untuk setiap negara (ApprovedApplication dll) dan menempatkan operasi yang diizinkan hanya pada kelas yang diizinkan, tetapi pendekatan semacam ini terasa salah juga.

Apakah ada pola desain resmi, atau sepotong kode sederhana, untuk menerapkan perilaku seperti itu?

uylmz
sumber
7
Hal-hal ini umumnya disebut StateMachines, dan implementasinya akan sedikit bervariasi tergantung pada kebutuhan Anda dan bahasa yang Anda gunakan.
Telastyn
dan, bagaimana Anda memastikan metode yang tepat tersedia di negara yang tepat?
uylmz
1
Itu tergantung pada bahasa. Kelas yang berbeda adalah implementasi umum untuk bahasa populer, meskipun "melempar jika tidak dalam keadaan benar" mungkin paling umum.
Telastyn
1
Di mana ada masalah dalam memasukkan metode "canUpdate" dan memeriksanya sebelum memanggil Pembaruan?
Euforia
1
this kind of code does not make it obvious such a rule exists- Inilah sebabnya mengapa kode memiliki dokumentasi. Penulis kode yang baik akan mengikuti saran Euphoric dan memberikan metode untuk membiarkan pihak luar menguji aturan sebelum mencobanya di perangkat keras.
Blrfl

Jawaban:

4

Situasi seperti ini cukup sering muncul. Misalnya, file hanya dapat dimanipulasi saat terbuka, dan jika Anda mencoba melakukan sesuatu dengan file setelah ditutup, Anda mendapatkan pengecualian runtime.

Keinginan Anda ( dinyatakan dalam pertanyaan Anda sebelumnya ) untuk menggunakan sistem jenis bahasa untuk memastikan bahwa hal yang salah tidak dapat terjadi adalah mulia, karena kesalahan waktu kompilasi selalu lebih baik daripada kesalahan runtime. Namun, tidak ada pola desain yang saya tahu untuk situasi seperti ini, mungkin karena itu akan menyebabkan lebih banyak masalah daripada yang akan dipecahkan. (Itu tidak praktis.)

Hal yang paling dekat dengan situasi Anda yang saya ketahui adalah memodelkan berbagai status objek yang sesuai dengan kemampuan berbeda melalui antarmuka tambahan, tetapi dengan cara ini Anda hanya mengurangi jumlah tempat dalam kode tempat kesalahan runtime dapat terjadi, Anda tidak menghapus kemungkinan kesalahan runtime.

Jadi, dalam situasi Anda, Anda akan mendeklarasikan sejumlah antarmuka yang menggambarkan apa yang dapat dilakukan dengan objek Anda di berbagai statusnya, dan objek Anda akan mengembalikan referensi ke antarmuka yang tepat saat transisi keadaan.

Jadi, misalnya, approve()metode kelas Anda akan mengembalikan ApprovedApplicationantarmuka. Antarmuka akan diimplementasikan secara pribadi, (melalui kelas bersarang,) sehingga kode yang hanya memiliki referensi ke tidak Applicationdapat memanggil ApprovedApplicationmetode apa pun . Kemudian, kode yang memanipulasi aplikasi yang disetujui secara eksplisit menyatakan niatnya untuk melakukannya pada waktu kompilasi dengan mengharuskan ApprovedApplicationuntuk bekerja dengannya. Tapi tentu saja, jika Anda menyimpan antarmuka ini di suatu tempat, dan kemudian Anda melanjutkan untuk menggunakan antarmuka ini setelah decline()metode ini dipanggil, Anda masih akan mendapatkan kesalahan runtime. Saya tidak berpikir ada solusi sempurna untuk masalah Anda.

Mike Nakis
sumber
Sebagai catatan, haruskah itu application.approve (someoneWhoCanApprove) atau seseorangWhoCanApprove.approve (aplikasi)? Saya pikir ini harus menjadi yang pertama karena "seseorang" mungkin tidak memiliki akses ke bidang aplikasi untuk membuat penyesuaian yang diperlukan
uylmz
Saya tidak yakin, tetapi Anda juga harus memeriksa kemungkinan bahwa keduanya tidak mungkin benar. yaitu if( someone.hasApprovalPermission( application ) ) { application.approve(); } prinsip Separation of Concerns menunjukkan bahwa baik aplikasi, maupun seseorang, tidak perlu dipikirkan untuk membuat keputusan terkait izin dan keamanan.
Mike Nakis
3

Aku menganggukkan kepalaku pada bit yang berbeda dari berbagai jawaban tetapi OP tampaknya masih memiliki perhatian terhadap kontrol aliran. Ada terlalu banyak untuk mencoba menyatu dalam kata-kata. Saya hanya akan memperbaiki beberapa kode - Pola Negara.


Nama Negara sebagai Past Tense

"In_Review" mungkin bukan keadaan, melainkan transisi, atau proses. Kalau tidak, nama negara Anda harus konsisten: "Menerapkan", "Menyetujui", "Menolak", dll. ATAU memiliki "Ditinjau" juga. Atau tidak.

Negara Terapan melakukan transisi ulasan dan menetapkan negara untuk Ditinjau. Kondisi Ulasan melakukan transisi persetujuan dan menetapkan status ke Disetujui (atau Ditolak).


// Application class encapsulates state transition,
// the client is unable to directly set state.
public class Application {
    State currentState = null;

    State AppliedState    = new Applied(this);
    State DeclinedState   = new Declined(this);
    State ApprovedState   = new Approved(this);
    State ReviewedState   = new Reviewed(this);

    public class Application (ApplicationDocument myApplication) {
        if(myApplication != null && isComplete()) {
            currentState = AppliedState;
        } else {            
            throw new ArgumentNullException ("Your application is incomplete");
            // some kind of error communication would probably be better
        }
    }

    public apply()    { currentState.apply(); }
    public review()   { currentState.review(); }
    public approve()  { currentState.approve(); }
    public decline()  { currentState.decline(); }


    //These could be done via an enum. I like enums!
    protected void setSubmittingState() {}
    protected void setApproveState() {}
    // etc. ...
}

// could be an interface if we don't have any default or base behavior.
public abstract class State {   
    protected Application theApp;
    // maybe these return an object communicating errors / error state.
    public abstract void apply();
    public abstract void review();
    public abstract void accept();
    public abstract void decline();
}

public class Applied implements State {
    public Applied (Application newApp) {
        if(newApp != null)
            theApp = newApp;
        else
            throw new ArgumentNullException ("null application argument");
     }

    public override void apply() {
        // whatever is appropriate when already in "applied" state
        // do not do any work on behalf of other states!
        // throwing exceptions here is not appropriate, as others
        // have said.
      }

    public override void review() {
        if(recursiveBureaucracyBuckPassing())
            theApp.setReviewedState();
    }

    public override void decline() { // ditto  }
}

public class Reviewed implements State {}
public class Approved implements State {}
public class Declined implements State {}

Edit - Penanganan Kesalahan Komentar

Komentar terbaru:

... jika Anda mencoba untuk meminjamkan buku yang sudah diterbitkan kepada orang lain, model Buku akan berisi logika untuk mencegah keadaannya berubah. Ini mungkin melalui nilai balik (mis. Boolean berhasil yay / tidak, atau kode status) atau pengecualian (misalnya IllegalStateChangeException) atau cara lain. Terlepas dari cara yang dipilih, aspek ini tidak tercakup sebagai bagian dari jawaban ini (atau apa pun).

Dan dari pertanyaan awal:

Tetapi kode semacam ini tidak membuatnya jelas aturan seperti itu ada, itu memungkinkan siapa saja untuk memanggil metode pembaruan (), dan hanya setelah gagal klien tahu operasi seperti itu tidak diizinkan.

Ada banyak pekerjaan desain yang harus dilakukan. Tidak ada Unified Field Theory Pattern. Kebingungan datang dari asumsi kerangka transisi keadaan akan melakukan fungsi aplikasi umum dan penanganan kesalahan. Itu terasa salah karena itu. Jawaban yang ditampilkan dirancang untuk mengendalikan perubahan kondisi.


Tentunya saya dapat menulis metode pembaruan () di kelas Aplikasi, memeriksa status Aplikasi, dan tidak melakukan apa pun atau melemparkan pengecualian jika Aplikasi tidak dalam status yang diperlukan

Ini menunjukkan ada tiga fungsi yang bekerja di sini: Negara, Memperbarui, dan interaksi keduanya. Dalam hal Applicationini bukan kode yang saya tulis. Mungkin menggunakannya untuk menentukan kondisi saat ini. Applicationbukan applicationPaperworkkeduanya. Applicationbukan interaksi keduanya, tetapi bisa menjadi StateContextEvaluatorkelas umum . Sekarang Applicationakan mengatur interaksi komponen ini dan kemudian bertindak sesuai, seperti memancarkan pesan kesalahan.

Akhiri Edit

radarbob
sumber
Apakah saya melewatkan sesuatu? Ini tampaknya mengizinkan pemanggilan keempat metode, apa pun keadaannya, tanpa petunjuk bagaimana pengaturan ini akan digunakan untuk berkomunikasi dengan metode pemanggilan yang panggilan yang diterapkan () tidak berhasil karena telah diterapkan misalnya.
kwah
1
ijinkan memanggil keempat metode, terlepas dari kondisi Ya. Itu harus. tanpa petunjuk bagaimana pengaturan ini akan digunakan untuk berkomunikasi dengan metode pemanggilan. Lihat komentar di Applicationkonstruktor di mana pengecualian dilemparkan. Mungkin menelepon AppliedState.Approve()mungkin menghasilkan pesan pengguna "Aplikasi harus ditinjau sebelum dapat disetujui."
radarbob
1
... panggilan apply () tidak berhasil karena sudah diterapkan misalnya . Itu pemikiran yang salah. Panggilan berhasil. Tetapi ada perilaku yang berbeda untuk negara yang berbeda. Itu , adalah Pola Negara ...... Namun, programmer harus memutuskan perilaku apa yang sesuai. Tapi itu salah berpikir bahwa "OMG sebuah kesalahan !!! Saya berharap AppliedState.apply()akan dengan lembut mengingatkan pengguna bahwa aplikasi telah dikirimkan dan sedang menunggu peninjauan. Dan program terus berjalan.
radarbob
Menganggap pola keadaan sedang digunakan sebagai model, "kegagalan" harus dikomunikasikan ke antarmuka pengguna. Misalnya, jika Anda mencoba meminjamkan buku yang sudah diterbitkan kepada orang lain, model Buku akan berisi logika untuk mencegah keadaan buku itu berubah. Ini mungkin melalui nilai balik (mis. Boolean berhasil yay / tidak, atau kode status) atau pengecualian (misalnya IllegalStateChangeException) atau cara lain. Terlepas dari cara yang dipilih, aspek ini tidak tercakup sebagai bagian dari jawaban ini (atau apa pun).
kwah
Terima kasih Tuhan seseorang mengatakannya. "Aku butuh perilaku berbeda berdasarkan keadaan suatu objek ... Ya, ya. Kamu menginginkan pola keadaan ." ++ kacang tua.
RubberDuck
1

Secara umum, apa yang Anda gambarkan adalah alur kerja. Lebih khusus lagi, fungsi-fungsi bisnis yang diwujudkan oleh negara-negara seperti DIREVIEWKAN DISETUJUI atau DITANGGUHKAN berada di bawah judul "aturan bisnis" atau "logika bisnis."

Tetapi untuk menjadi jelas, aturan bisnis tidak boleh dikodekan ke dalam pengecualian. Untuk melakukannya adalah dengan menggunakan pengecualian untuk kontrol aliran program, dan ada banyak alasan bagus mengapa Anda tidak harus melakukannya. Pengecualian harus digunakan untuk kondisi luar biasa, dan status INVALID suatu aplikasi sepenuhnya tidak eksklusif dari sudut pandang bisnis.

Gunakan pengecualian dalam kasus di mana program tidak dapat pulih dari kondisi kesalahan tanpa campur tangan pengguna ("file tidak ditemukan," misalnya).

Tidak ada pola khusus untuk menulis logika bisnis, selain teknik biasa untuk mengatur sistem pemrosesan data bisnis dan kode penulisan untuk mengimplementasikan proses Anda. Jika aturan bisnis dan alur kerjanya rumit, pertimbangkan untuk menggunakan semacam server alur kerja atau mesin aturan bisnis.

Dalam kasus apa pun, status REVIEW, DISETUJUI, DITANGGUNG, dll. Dapat diwakili oleh variabel pribadi tipe enum di kelas Anda. Jika Anda menggunakan metode pengambil / penyetel, Anda dapat mengontrol apakah setter akan mengizinkan perubahan dengan terlebih dahulu memeriksa nilai variabel enum. Jika seseorang mencoba menulis ke setter ketika nilai enum dalam kondisi yang salah, maka Anda dapat melempar pengecualian.

Robert Harvey
sumber
Ada objek, yang disebut "Aplikasi", sifat-sifatnya hanya dapat diubah jika "Status" -nya adalah "AWAL". Ini bukan alur kerja yang besar, seperti dokumen yang mengalir dari satu departemen ke departemen lain. Apa yang saya gagal lakukan adalah, untuk mencerminkan perilaku ini dalam arti berorientasi objek.
uylmz
Aplikasi @Reek harus mengekspos antarmuka baca / tulis, dan logika iteraksi harus terjadi di tingkat yang lebih tinggi. Baik pelamar dan SDM menggunakan objek yang sama, tetapi memiliki hak istimewa yang berbeda - objek aplikasi tidak perlu khawatir tentang hal itu. Pengecualian batiniah dapat digunakan untuk melindungi integrasi sistem, tetapi saya tidak akan bersikap defensif (mengedit informasi kontak mungkin diperlukan bahkan untuk aplikasi yang disetujui - hanya perlu tingkat akses yang lebih tinggi).
gemetaran
1

Applicationbisa berupa antarmuka, dan Anda bisa memiliki implementasi untuk masing-masing negara. Antarmuka dapat memiliki moveToNextState()metode, dan ini akan menyembunyikan semua logika alur kerja.

Untuk kebutuhan klien, mungkin ada juga metode yang mengembalikan secara langsung apa yang dapat Anda lakukan dan tidak (yaitu seperangkat booleans), bukan hanya keadaan, sehingga Anda tidak memerlukan "daftar periksa" di klien (saya berasumsi klien menjadi pengontrol MVC atau UI).

Namun, alih-alih melempar pengecualian, Anda bisa melakukan apa saja dan mencatatnya. Ini aman saat runtime, aturan diberlakukan dan klien memiliki cara untuk menyembunyikan kontrol "pembaruan".

batu besar
sumber
1

Salah satu pendekatan untuk masalah ini yang telah sangat sukses di alam liar adalah hypermedia - representasi keadaan entitas disertai dengan kontrol hypermedia yang menggambarkan jenis transisi yang saat ini diperbolehkan. Konsumen menanyakan kontrol untuk menemukan apa yang bisa dilakukan.

Ini adalah mesin negara, dengan kueri di antarmuka yang memungkinkan Anda menemukan peristiwa apa yang diizinkan untuk diaktifkan.

Dengan kata lain: kami sedang menggambarkan web (REST).

Pendekatan lain adalah mengambil gagasan Anda tentang antarmuka yang berbeda untuk negara yang berbeda, dan memberikan kueri yang memungkinkan Anda mendeteksi antarmuka mana yang saat ini tersedia. Pikirkan IUnknown :: QueryInterface, atau down casting. Kode klien berperan sebagai Mother May I dengan status untuk mengetahui apa yang diizinkan.

Ini pada dasarnya pola yang sama - hanya menggunakan antarmuka untuk mewakili kontrol hypermedia.

VoiceOfUnreason
sumber
Saya suka ini. Ini dapat dikombinasikan dengan pola Negara untuk mengembalikan koleksi Negara yang valid yang dapat dialihkan ke. Chain of Command muncul dalam pikiran dengan cara.
RubberDuck
1
Dugaan saya adalah bahwa Anda tidak ingin "kumpulan status yang valid" tetapi "kumpulan tindakan yang valid". Pikirkan grafik: Anda ingin simpul saat ini (keadaan) dan daftar tepi (tindakan). Anda akan mengetahui status berikutnya saat memilih tindakan.
VoiceOfUnreason
Iya. Kamu benar. Kumpulan tindakan yang valid di mana tindakan itu sebenarnya merupakan transisi negara (atau sesuatu yang memicu satu).
RubberDuck
1

Berikut adalah contoh bagaimana Anda dapat mendekati ini dari perspektif fungsional, dan bagaimana hal itu membantu menghindari potensi jebakan. Saya bekerja di Haskell, yang saya anggap Anda tidak tahu, jadi saya akan menjelaskannya secara rinci saat saya melanjutkan.

data Application = Applied ApplicationDetails |
                   InReview ApplicationDetails |
                   Approved ApplicationDetails |
                   Declined ApplicationDetails

Ini mendefinisikan tipe data yang bisa di salah satu dari empat negara yang sesuai dengan negara aplikasi Anda. ApplicationDetailsdiasumsikan sebagai tipe yang ada yang berisi informasi terperinci.

newtype UpdatableApplication = UpdatableApplication Application

Jenis alias yang perlu konversi eksplisit ke dan dari Application. Ini berarti bahwa jika kita mendefinisikan fungsi berikut yang menerima dan membuka UpdatableApplicationdan melakukan sesuatu yang berguna dengannya,

updateApplication :: UpdatableApplication -> ApplicationDetails -> Application
updateApplication (UpdatableApplication app) details = ...

maka kita harus secara eksplisit mengonversi Aplikasi ke Aplikasi yang Dapat Diperbarui sebelum kita dapat menggunakannya. Ini dilakukan dengan menggunakan fungsi ini:

findUpdatableApplication :: Application -> Maybe UpdatableApplication
findUpdatableApplication app@(Applied _) = Just (UpdatableApplication app)
findUpdatableApplication _               = Nothing

Di sini kami melakukan tiga hal menarik:

  • Kami memeriksa status aplikasi (menggunakan pencocokan pola, yang sangat berguna untuk kode semacam ini), dan
  • jika dapat diperbarui, kami membungkusnya dalam UpdatableApplication(yang hanya melibatkan catatan jenis kompilasi dari perubahan jenis yang ditambahkan, karena Haskell memiliki fitur khusus untuk melakukan tipuan tingkat-jenis seperti ini, tidak memerlukan biaya saat runtime) , dan
  • kami mengembalikan hasilnya dalam "Mungkin" (mirip dengan Optiondi C # atau Optionaldi Jawa - itu adalah objek yang membungkus hasil yang bisa hilang).

Sekarang, untuk benar-benar menyatukan ini, kita perlu memanggil fungsi ini dan, jika hasilnya berhasil, meneruskannya ke fungsi pembaruan ...

case findUpdatableApplication application of
    Just updatableApplication -> do
        storeApplicationInDatabase (updateApplication updatableApplication)
        showConfirmationPage
    Nothing -> do
        showErrorPage

Karena updateApplicationfungsi membutuhkan objek yang dibungkus, kita tidak bisa lupa untuk memeriksa prasyarat. Dan karena fungsi pemeriksaan prakondisi mengembalikan objek yang dibungkus di dalam Maybeobjek, kita tidak bisa lupa untuk memeriksa hasilnya dan merespons jika gagal.

Sekarang ... Anda bisa melakukan ini dalam bahasa berorientasi objek. Tapi itu kurang nyaman:

  • Tak satu pun dari bahasa OO yang saya coba memiliki sintaks sederhana untuk membuat tipe pembungkus yang aman, jadi itu boilerplate.
  • Ini juga akan menjadi kurang efisien, karena setidaknya untuk sebagian besar bahasa mereka tidak akan dapat menghilangkan jenis pembungkus, karena akan diperlukan untuk ada dan dapat dideteksi pada saat runtime (Haskell tidak memiliki pengecekan jenis runtime, semua jenis cek adalah dilakukan pada waktu kompilasi).
  • Sementara beberapa bahasa OO memiliki tipe yang setara dengan Maybemereka biasanya tidak memiliki cara yang nyaman untuk mengekstraksi data dan memilih jalur yang akan diambil pada saat yang sama. Pencocokan pola juga sangat berguna di sini.
Jules
sumber
1

Anda bisa menggunakan pola «perintah», dan kemudian meminta Invoker untuk memberikan daftar fungsi yang valid sesuai dengan keadaan kelas penerima.

Saya menggunakan hal yang sama untuk menyediakan fungsionalitas untuk antarmuka yang berbeda yang seharusnya memanggil kode saya, beberapa opsi tidak tersedia tergantung dari status catatan saat ini, jadi penyerbu saya memperbarui daftar dan dengan cara itu setiap GUI meminta Invoker opsi mana yang tersedia dan mereka mengecatnya sendiri.

bns
sumber