Tambahkan mesin aturan sederhana ke aplikasi berbasis Spring Anda

Setiap proyek perangkat lunak nontrivial berisi sejumlah nontrivial dari apa yang disebut logika bisnis. Apa sebenarnya yang membentuk logika bisnis masih bisa diperdebatkan. Di pegunungan kode yang dihasilkan untuk aplikasi perangkat lunak biasa, potongan-potongan di sana-sini benar-benar melakukan pekerjaan yang diminta perangkat lunak itu — memproses pesanan, mengontrol sistem senjata, menggambar, dll. Bit-bit itu sangat kontras dengan bit-bit lain yang berurusan dengan ketekunan , logging, transaksi, keanehan bahasa, kebiasaan kerangka kerja, dan informasi menarik lainnya dari aplikasi perusahaan modern.

Lebih sering daripada tidak, logika bisnis sangat bercampur dengan semua bagian lainnya. Ketika kerangka kerja yang berat dan mengganggu (seperti Enterprise JavaBeans) digunakan, membedakan di mana logika bisnis berakhir dan kode yang terinspirasi kerangka kerja dimulai menjadi sangat sulit.

Ada satu persyaratan perangkat lunak yang jarang dijabarkan dalam dokumen definisi persyaratan namun memiliki kekuatan untuk membuat atau menghancurkan proyek perangkat lunak apa pun: kemampuan beradaptasi, ukuran seberapa mudahnya mengubah perangkat lunak sebagai respons terhadap perubahan lingkungan bisnis.

Perusahaan modern dipaksa untuk menjadi cepat dan fleksibel, dan mereka menginginkan hal yang sama dari perangkat lunak perusahaan mereka. Aturan bisnis yang diterapkan dengan susah payah dalam logika bisnis kelas Anda hari ini akan menjadi usang besok dan perlu diubah dengan cepat dan akurat. Ketika kode Anda memiliki logika bisnis yang terkubur jauh di dalam berton-ton bit lainnya, modifikasi akan dengan cepat menjadi lambat, menyakitkan, dan rawan kesalahan.

Tidak heran beberapa bidang paling trendi dalam perangkat lunak perusahaan saat ini adalah mesin aturan dan berbagai sistem manajemen proses bisnis (BPM). Setelah Anda melihat-lihat pembicaraan pemasaran, alat-alat itu pada dasarnya menjanjikan hal yang sama: Holy Grail of Business Logic yang ditangkap dalam repositori, dipisahkan dengan rapi dan ada dengan sendirinya, siap dipanggil dari aplikasi apa pun yang mungkin Anda miliki di rumah perangkat lunak Anda.

Meskipun mesin aturan komersial dan sistem BPM memiliki banyak keunggulan, mereka juga memiliki banyak kekurangan. Yang mudah dipilih adalah harganya, yang terkadang bisa dengan mudah mencapai tujuh digit. Hal lainnya adalah kurangnya standardisasi praktis yang berlanjut hingga saat ini meskipun ada upaya industri besar dan beberapa standar di atas kertas yang tersedia. Dan, karena semakin banyak toko perangkat lunak yang mengadaptasi metodologi pengembangan yang gesit, ramping, dan cepat, alat kelas berat tersebut merasa sulit untuk menyesuaikannya.

Dalam artikel ini, kami membangun mesin aturan sederhana yang, di satu sisi, memanfaatkan pemisahan yang jelas dari logika bisnis yang khas untuk sistem semacam itu dan, di sisi lain — karena didukung oleh kerangka kerja J2EE yang populer dan kuat — tidak menderita karena kompleksitas dan "ketidaksenangan" dari penawaran komersial.

Waktu musim semi di alam semesta J2EE

Setelah kerumitan perangkat lunak perusahaan menjadi tak tertahankan dan masalah logika bisnis menjadi sorotan, Kerangka Musim Semi dan lainnya seperti itu lahir. Bisa dibilang, Spring adalah hal terbaik yang terjadi pada perusahaan Java dalam waktu yang lama. Spring menyediakan daftar panjang alat dan kemudahan kode kecil yang membuat pemrograman J2EE lebih berorientasi objek, lebih mudah, dan, lebih, lebih menyenangkan.

Di jantung Spring terletak prinsip Inversion of Control. Ini adalah nama yang mewah dan berlebihan, tetapi itu berasal dari ide-ide sederhana berikut:

  • Fungsi kode Anda dipecah menjadi beberapa bagian kecil yang dapat dikelola
  • Potongan-potongan itu diwakili oleh kacang Java standar yang sederhana (kelas Java sederhana yang menunjukkan beberapa, tetapi tidak semua, spesifikasi JavaBeans)
  • Anda tidak terlibat dalam mengelola kacang tersebut (membuat, menghancurkan, mengatur ketergantungan)
  • Sebaliknya, penampung Spring melakukannya untuk Anda berdasarkan beberapa definisi konteks yang biasanya disediakan dalam bentuk file XML

Spring juga menyediakan banyak fitur lainnya, seperti kerangka kerja Model-View-Controller yang lengkap dan kuat untuk aplikasi Web, pembungkus kenyamanan untuk pemrograman Konektivitas Database Java, dan lusinan kerangka kerja lainnya. Namun subjek tersebut menjangkau jauh di luar cakupan artikel ini.

Sebelum saya menjelaskan apa yang diperlukan untuk membuat mesin aturan sederhana untuk aplikasi berbasis Spring, mari pertimbangkan mengapa pendekatan ini adalah ide yang bagus.

Desain mesin aturan memiliki dua sifat menarik yang membuatnya berharga:

  • Pertama, mereka memisahkan kode logika bisnis dari area aplikasi lainnya
  • Kedua, mereka dapat dikonfigurasi secara eksternal, yang berarti bahwa definisi aturan bisnis dan bagaimana serta dalam urutan mana aturan tersebut diaktifkan disimpan secara eksternal ke aplikasi dan dimanipulasi oleh pembuat aturan, bukan pengguna aplikasi atau bahkan programmer.

Pegas sangat cocok untuk mesin aturan. Desain yang sangat terkomponen dari aplikasi Spring yang dikodekan dengan benar mempromosikan penempatan kode Anda menjadi potongan - potongan kecil, dapat dikelola, dan terpisah (kacang), yang dapat dikonfigurasi secara eksternal melalui definisi konteks Spring.

Baca terus untuk menjelajahi kecocokan yang baik antara apa yang dibutuhkan desain mesin-aturan dan apa yang sudah disediakan oleh desain Spring.

Desain mesin aturan berbasis pegas

Kami mendasarkan desain kami pada interaksi biji Java yang dikontrol pegas, yang kami sebut komponen mesin aturan. Mari tentukan dua jenis komponen yang mungkin kita perlukan:

  • Sebuah tindakan adalah komponen yang benar-benar melakukan sesuatu yang berguna dalam logika aplikasi kami
  • Sebuah aturan adalah komponen yang membuat keputusan dalam aliran logis dari tindakan

Karena kami adalah penggemar berat desain berorientasi objek yang bagus, kelas dasar berikut menangkap fungsionalitas dasar dari semua komponen yang akan datang, yaitu kemampuan untuk dipanggil oleh komponen lain dengan beberapa argumen:

public abstract class AbstractComponent { public abstract void execute(Object arg) throws Exception; }

Secara alami kelas dasar abstrak karena kita tidak akan pernah membutuhkannya sendiri.

Dan sekarang, kode untuk an AbstractAction, akan diperpanjang dengan tindakan konkret masa depan lainnya:

public abstract class AbstractAction extends AbstractComponent {

private AbstractComponent nextStep; public void execute(Object arg) throws Exception { this.doExecute(arg); if(nextStep != null) nextStep.execute(arg); } protected abstract void doExecute(Object arg) throws Exception;

public void setNextStep(AbstractComponent nextStep) { this.nextStep = nextStep; }

public AbstractComponent getNextStep() { return nextStep; }

}

Seperti yang Anda lihat, AbstractActionmelakukan dua hal: Menyimpan definisi dari komponen berikutnya yang akan dipanggil oleh mesin aturan kami. Dan, dalam execute()metodenya, ia memanggil doExecute()metode yang akan didefinisikan oleh subkelas konkret. Setelah doExecute()kembali, komponen selanjutnya dipanggil jika ada.

Kami AbstractRulejuga sederhana:

public abstract class AbstractRule extends AbstractComponent {

private AbstractComponent positiveOutcomeStep; private AbstractComponent negativeOutcomeStep; public void execute(Object arg) throws Exception { boolean outcome = makeDecision(arg); if(outcome) positiveOutcomeStep.execute(arg); else negativeOutcomeStep.execute(arg);

}

protected abstract boolean makeDecision(Object arg) throws Exception;

// Getters and setters for positiveOutcomeStep and negativeOutcomeStep are omitted for brevity

Dalam execute()metodenya, AbstractActionmemanggil makeDecision()metode, yang diimplementasikan subkelas, dan kemudian, bergantung pada hasil metode itu, memanggil salah satu komponen yang didefinisikan sebagai hasil positif atau negatif.

Desain kami selesai saat kami memperkenalkan SpringRuleEnginekelas ini :

public class SpringRuleEngine { private AbstractComponent firstStep; public void setFirstStep(AbstractComponent firstStep) { this.firstStep = firstStep; } public void processRequest(Object arg) throws Exception { firstStep.execute(arg); } }

Hanya itu yang ada di kelas utama mesin aturan kami: definisi komponen pertama dalam logika bisnis kami dan metode untuk memulai pemrosesan.

Tapi tunggu, di mana pipa ledeng yang menyambungkan semua kelas kita sehingga bisa berfungsi? Anda selanjutnya akan melihat bagaimana keajaiban Musim Semi membantu kami dalam tugas itu.

Mesin aturan berbasis pegas sedang beraksi

Mari kita lihat contoh konkret tentang bagaimana kerangka kerja ini dapat bekerja. Pertimbangkan kasus penggunaan ini: kita harus mengembangkan aplikasi yang bertanggung jawab untuk memproses aplikasi pinjaman. Kami harus memenuhi persyaratan berikut:

  • Kami memeriksa kelengkapan aplikasi dan menolaknya jika tidak
  • Kami memeriksa apakah aplikasi tersebut berasal dari pelamar yang tinggal di negara bagian di mana kami berwenang untuk berbisnis
  • Kami memeriksa apakah pendapatan bulanan pelamar dan pengeluaran bulanannya sesuai dengan rasio yang kami rasa nyaman
  • Aplikasi yang masuk disimpan dalam database melalui layanan persistensi yang tidak kita ketahui, kecuali untuk antarmukanya (mungkin pengembangannya dialihdayakan ke India)
  • Aturan bisnis dapat berubah, itulah mengapa desain mesin aturan diperlukan

Pertama, mari kita rancang kelas yang mewakili aplikasi pinjaman kita:

public class LoanApplication { public static final String INVALID_STATE = "Sorry we are not doing business in your state"; public static final String INVALID_INCOME_EXPENSE_RATIO = "Sorry we cannot provide the loan given this expense/income ratio"; public static final String APPROVED = "Your application has been approved"; public static final String INSUFFICIENT_DATA = "You did not provide enough information on your application"; public static final String INPROGRESS = "in progress"; public static final String[] STATUSES = new String[] { INSUFFICIENT_DATA, INVALID_INCOME_EXPENSE_RATIO, INVALID_STATE, APPROVED, INPROGRESS };

private String firstName; private String lastName; private double income; private double expences; private String stateCode; private String status; public void setStatus(String status) { if(!Arrays.asList(STATUSES).contains(status)) throw new IllegalArgumentException("invalid status:" + status); this.status = status; }

// Bunch of other getters and setters are omitted

}

Layanan ketekunan yang kami berikan dijelaskan oleh antarmuka berikut:

public interface LoanApplicationPersistenceInterface { public void recordApproval(LoanApplication application) throws Exception; public void recordRejection(LoanApplication application) throws Exception; public void recordIncomplete(LoanApplication application) throws Exception; }

Kami dengan cepat mengejek antarmuka ini dengan mengembangkan MockLoanApplicationPersistencekelas yang tidak melakukan apa pun selain memenuhi kontrak yang ditentukan oleh antarmuka.

Kami menggunakan subclass SpringRuleEnginekelas berikut untuk memuat konteks Spring dari file XML dan benar-benar memulai pemrosesan:

public class LoanProcessRuleEngine extends SpringRuleEngine { public static final SpringRuleEngine getEngine(String name) { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("SpringRuleEngineContext.xml"); return (SpringRuleEngine) context.getBean(name); } }

Saat ini, kami memiliki kerangka di tempatnya, jadi ini adalah waktu yang tepat untuk menulis pengujian JUnit, yang muncul di bawah. Beberapa asumsi dibuat: Kami berharap perusahaan kami beroperasi hanya di dua negara bagian, Texas dan Michigan. Dan kami hanya menerima pinjaman dengan rasio pengeluaran / pendapatan 70 persen atau lebih.

public class SpringRuleEngineTest extends TestCase {

public void testSuccessfulFlow() throws Exception { SpringRuleEngine engine = LoanProcessRuleEngine.getEngine("SharkysExpressLoansApplicationProcessor"); LoanApplication application = new LoanApplication(); application.setFirstName("John"); application.setLastName("Doe"); application.setStateCode("TX"); application.setExpences(4500); application.setIncome(7000); engine.processRequest(application); assertEquals(LoanApplication.APPROVED, application.getStatus()); } public void testInvalidState() throws Exception { SpringRuleEngine engine = LoanProcessRuleEngine.getEngine("SharkysExpressLoansApplicationProcessor"); LoanApplication application = new LoanApplication(); application.setFirstName("John"); application.setLastName("Doe"); application.setStateCode("OK"); application.setExpences(4500); application.setIncome(7000); engine.processRequest(application); assertEquals(LoanApplication.INVALID_STATE, application.getStatus()); } public void testInvalidRatio() throws Exception { SpringRuleEngine engine = LoanProcessRuleEngine.getEngine("SharkysExpressLoansApplicationProcessor"); LoanApplication application = new LoanApplication(); application.setFirstName("John"); application.setLastName("Doe"); application.setStateCode("MI"); application.setIncome(7000); application.setExpences(0.80 * 7000); //too high engine.processRequest(application); assertEquals(LoanApplication.INVALID_INCOME_EXPENSE_RATIO, application.getStatus()); } public void testIncompleteApplication() throws Exception { SpringRuleEngine engine = LoanProcessRuleEngine.getEngine("SharkysExpressLoansApplicationProcessor"); LoanApplication application = new LoanApplication(); engine.processRequest(application); assertEquals(LoanApplication.INSUFFICIENT_DATA, application.getStatus()); }