Saya sedang berupaya mempertimbangkan ulang aspek-aspek tertentu dari layanan web yang ada. Cara API layanan diimplementasikan adalah dengan memiliki semacam "pipeline pemrosesan", di mana ada tugas yang dilakukan secara berurutan. Tidak mengherankan, tugas-tugas selanjutnya mungkin memerlukan informasi yang dihitung oleh tugas-tugas sebelumnya, dan saat ini cara ini dilakukan adalah dengan menambahkan bidang ke kelas "keadaan pipa".
Saya telah berpikir (dan berharap?) Bahwa ada cara yang lebih baik untuk berbagi informasi antara langkah-langkah pipa daripada memiliki objek data dengan miliaran bidang, beberapa di antaranya masuk akal untuk beberapa langkah pemrosesan dan tidak kepada yang lain. Akan sangat menyusahkan untuk membuat kelas ini aman dari thread (saya tidak tahu apakah itu mungkin), tidak ada cara untuk alasan tentang invariannya (dan kemungkinan tidak ada).
Saya mencari-cari buku pola desain Gang of Four untuk menemukan beberapa inspirasi, tetapi saya tidak merasa ada solusi di sana (Memento agak bersemangat, tetapi tidak sepenuhnya). Saya juga mencari online, tetapi saat Anda mencari "pipeline" atau "workflow" Anda kebanjiran informasi pipa Unix, atau mesin alur kerja dan kerangka kerja.
Pertanyaan saya adalah - bagaimana Anda akan mendekati masalah pencatatan status eksekusi dari pipeline pemrosesan perangkat lunak, sehingga tugas-tugas selanjutnya dapat menggunakan informasi yang dihitung oleh yang sebelumnya? Saya kira perbedaan utama dengan pipa Unix adalah bahwa Anda tidak hanya peduli dengan hasil dari tugas yang sebelumnya segera.
Seperti yang diminta, beberapa pseudocode untuk menggambarkan kasus penggunaan saya:
Objek "konteks pipa" memiliki banyak bidang yang dapat diisi / dibaca oleh langkah-langkah pipa yang berbeda:
public class PipelineCtx {
... // fields
public Foo getFoo() { return this.foo; }
public void setFoo(Foo aFoo) { this.foo = aFoo; }
public Bar getBar() { return this.bar; }
public void setBar(Bar aBar) { this.bar = aBar; }
... // more methods
}
Setiap langkah pipa juga merupakan objek:
public abstract class PipelineStep {
public abstract PipelineCtx doWork(PipelineCtx ctx);
}
public class BarStep extends PipelineStep {
@Override
public PipelineCtx doWork(PipelieCtx ctx) {
// do work based on the stuff in ctx
Bar theBar = ...; // compute it
ctx.setBar(theBar);
return ctx;
}
}
Demikian pula untuk hipotetis FooStep
, yang mungkin perlu Bar dihitung oleh BarStep sebelum itu, bersama dengan data lainnya. Dan kemudian kita memiliki panggilan API yang sebenarnya:
public class BlahOperation extends ProprietaryWebServiceApiBase {
public BlahResponse handle(BlahRequest request) {
PipelineCtx ctx = PipelineCtx.from(request);
// some steps happen here
// ...
BarStep barStep = new BarStep();
barStep.doWork(crx);
// some more steps maybe
// ...
FooStep fooStep = new FooStep();
fooStep.doWork(ctx);
// final steps ...
return BlahResponse.from(ctx);
}
}
sumber
Jawaban:
Alasan utama untuk menggunakan desain pipa adalah karena Anda ingin memisahkan tahapan. Entah karena satu tahap dapat digunakan dalam banyak pipa (seperti alat shell Unix), atau karena Anda mendapatkan beberapa manfaat penskalaan (yaitu, Anda dapat dengan mudah berpindah dari arsitektur satu-simpul ke arsitektur multi-simpul).
Dalam kedua kasus tersebut, setiap tahap dalam pipa perlu diberikan segala yang dibutuhkan untuk melakukan tugasnya. Tidak ada alasan bahwa Anda tidak dapat menggunakan toko eksternal (misalnya, basis data), tetapi dalam kebanyakan kasus lebih baik untuk meneruskan data dari satu tahap ke tahap lainnya.
Namun, itu tidak berarti bahwa Anda harus atau harus melewati satu objek pesan besar dengan setiap bidang yang mungkin (meskipun lihat di bawah). Sebagai gantinya, setiap tahap dalam pipeline harus mendefinisikan interface untuk input dan output pesannya, yang mengidentifikasi hanya data yang dibutuhkan stage.
Anda kemudian memiliki banyak fleksibilitas dalam cara Anda mengimplementasikan objek pesan yang sebenarnya. Salah satu pendekatan adalah menggunakan objek data besar yang mengimplementasikan semua antarmuka yang diperlukan. Lain adalah membuat kelas pembungkus di sekitar yang sederhana
Map
. Yang lain adalah membuat kelas pembungkus di sekitar database.sumber
Ada beberapa pemikiran yang terlintas di benak saya, pertama adalah bahwa saya tidak memiliki informasi yang cukup.
Jawabannya mungkin akan membuat saya berpikir lebih hati-hati tentang desain, namun berdasarkan apa yang Anda katakan ada 2 pendekatan yang mungkin saya pertimbangkan pertama.
Struktur setiap tahap sebagai objeknya sendiri. Tahap ke-n akan memiliki tahap 1 hingga n-1 sebagai daftar delegasi. Setiap tahap merangkum data dan pemrosesan data; mengurangi keseluruhan kompleksitas dan bidang dalam setiap objek. Anda juga dapat memiliki tahap selanjutnya mengakses data yang diperlukan dari tahap sebelumnya dengan melintasi delegasi. Anda masih memiliki kopling yang cukup ketat di semua objek karena merupakan hasil dari tahapan (mis. Semua attr) yang penting, tetapi berkurang secara signifikan dan setiap tahap / objek mungkin lebih mudah dibaca dan dimengerti. Anda dapat membuatnya aman dengan membuat daftar delegasi malas dan menggunakan antrian aman utas untuk mengisi daftar delegasi di setiap objek sesuai kebutuhan.
Atau saya mungkin akan melakukan sesuatu yang mirip dengan apa yang Anda lakukan. Objek data besar yang melewati fungsi yang mewakili setiap tahap. Ini seringkali jauh lebih cepat dan ringan, tetapi lebih kompleks dan rentan kesalahan karena itu hanya setumpuk besar atribut data. Jelas bukan thread-safe.
Jujur saya lakukan nanti lebih sering untuk ETL dan beberapa masalah serupa lainnya. Saya fokus pada kinerja karena jumlah data daripada pemeliharaan. Juga, mereka satu kali yang tidak akan digunakan lagi.
sumber
Ini terlihat seperti Pola Rantai di GoF.
Titik awal yang baik adalah untuk melihat apa yang dilakukan oleh commons-chain .
sumber
Solusi pertama yang bisa saya bayangkan adalah membuat langkah-langkah eksplisit. Masing-masing dari mereka menjadi objek yang dapat memproses sepotong data dan mengirimkannya ke objek proses selanjutnya. Setiap proses menghasilkan produk baru (idealnya tidak berubah), sehingga tidak ada interaksi antara proses dan kemudian tidak ada risiko karena berbagi data. Jika beberapa proses lebih memakan waktu daripada yang lain, Anda dapat menempatkan beberapa buffer di antara dua proses. Jika Anda mengeksploitasi penjadwal dengan benar untuk multithreading, itu akan mengalokasikan lebih banyak sumber daya untuk membilas buffer.
Solusi kedua bisa dengan memikirkan "pesan" alih-alih pipa, mungkin dengan kerangka kerja khusus. Anda kemudian memiliki beberapa "aktor" yang menerima pesan dari aktor lain dan mengirim pesan lain ke aktor lain. Anda mengatur aktor Anda dalam saluran pipa dan memberikan data primer Anda kepada aktor pertama yang memulai rantai. Tidak ada berbagi data karena berbagi diganti dengan pengiriman pesan. Saya tahu model aktor Scala dapat digunakan di Jawa, karena tidak ada yang spesifik Scala di sini, tapi saya tidak pernah menggunakannya dalam program Java.
Solusi serupa dan Anda dapat menerapkan yang kedua dengan yang pertama. Pada dasarnya, konsep utama adalah untuk berurusan dengan data yang tidak dapat diubah untuk menghindari masalah tradisional karena berbagi data dan untuk membuat entitas yang eksplisit dan independen yang mewakili proses dalam pipa Anda. Jika Anda memenuhi persyaratan ini, Anda dapat dengan mudah membuat jaringan pipa yang jelas dan sederhana dan menggunakannya dalam program paralel.
sumber