Strategi implementasi yang baik untuk merangkum data bersama dalam pipa perangkat lunak

13

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);
    }
}
RuslanD
sumber
6
jangan lintas pos tetapi beri tanda agar mod bergerak
ratchet freak
1
Akan melakukan maju, saya kira saya harus menghabiskan lebih banyak waktu membiasakan diri dengan aturan. Terima kasih!
RuslanD
1
Apakah Anda menghindari penyimpanan data terus-menerus untuk implementasi Anda, atau ada sesuatu yang bisa diambil pada saat ini?
CokoBWare
1
Hai Ruslan dan selamat datang! Ini memang lebih cocok untuk Programmer daripada Stack Overflow, jadi kami menghapus versi SO. Ingatlah apa yang @ratchetfreak sebutkan, Anda dapat menandai untuk perhatian moderat dan meminta pertanyaan untuk dimigrasi ke situs yang lebih cocok, tidak perlu melewati pos. Aturan praktis untuk memilih antara dua situs adalah bahwa Programmer adalah untuk masalah yang Anda hadapi ketika Anda berada di depan papan tulis yang merancang proyek Anda, dan Stack Overflow untuk masalah yang lebih teknis (misalnya masalah implementasi). Untuk lebih jelasnya lihat FAQ kami .
yannis
1
Jika Anda mengubah arsitektur menjadi DAG pemrosesan (grafik asiklik terarah) alih-alih pipa, Anda dapat secara eksplisit meneruskan hasil langkah sebelumnya.
Patrick

Jawaban:

4

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.

parsifal
sumber
1

Ada beberapa pemikiran yang terlintas di benak saya, pertama adalah bahwa saya tidak memiliki informasi yang cukup.

  • Apakah setiap langkah menghasilkan data yang digunakan di luar jalur pipa, atau apakah kita hanya peduli dengan hasil dari tahap terakhir?
  • Apakah ada banyak masalah data besar? yaitu. masalah memori, masalah kecepatan, dll

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.

dietbuddha
sumber
1

Ini terlihat seperti Pola Rantai di GoF.

Titik awal yang baik adalah untuk melihat apa yang dilakukan oleh commons-chain .

Teknik populer untuk mengatur pelaksanaan aliran proses yang kompleks adalah pola "Rantai Tanggung Jawab", seperti yang dijelaskan (di antara banyak tempat lain) dalam buku pola desain "Gang Empat" klasik. Meskipun kontrak API dasar yang diperlukan untuk menerapkan pola desain ini sangat sederhana, akan bermanfaat jika memiliki API dasar yang memfasilitasi penggunaan pola, dan (yang lebih penting) mendorong komposisi implementasi perintah dari berbagai sumber berbeda.

Untuk itu, Chain API memodelkan perhitungan sebagai serangkaian "perintah" yang dapat digabungkan menjadi "rantai". API untuk suatu perintah terdiri dari metode tunggal ( execute()), yang meneruskan parameter "konteks" yang berisi keadaan dinamis dari perhitungan, dan yang nilai baliknya adalah boolean yang menentukan apakah proses untuk rantai saat ini telah selesai atau tidak ( true), atau apakah pemrosesan harus didelegasikan ke perintah berikutnya dalam rantai (false).

Abstraksi "konteks" dirancang untuk mengisolasi implementasi perintah dari lingkungan di mana mereka dijalankan (seperti perintah yang dapat digunakan dalam Servlet atau Portlet, tanpa terikat langsung dengan kontrak API dari salah satu lingkungan ini). Untuk perintah yang perlu mengalokasikan sumber daya sebelum pendelegasian, dan kemudian melepaskannya setelah kembali (bahkan jika perintah yang didelegasikan ke melempar pengecualian), ekstensi "filter" ke "perintah" menyediakan postprocess()metode untuk pembersihan ini. Akhirnya, perintah dapat disimpan dan dilihat dalam "katalog" untuk memungkinkan penangguhan keputusan yang perintah (atau rantai) sebenarnya dieksekusi.

Untuk memaksimalkan kegunaan API pola Tanggung Jawab, kontrak antarmuka dasar didefinisikan dengan cara tanpa ketergantungan nol selain JDK yang sesuai. Implementasi kelas dasar dari API ini disediakan, serta implementasi yang lebih khusus (tetapi opsional) untuk lingkungan web (yaitu servlets dan portlets).

Mengingat bahwa implementasi perintah dirancang untuk memenuhi rekomendasi ini, maka layak untuk menggunakan Rantai Tanggung Jawab API dalam "pengontrol depan" dari kerangka kerja aplikasi web (seperti Struts), tetapi juga dapat menggunakannya dalam bisnis tingkatan logika dan kegigihan untuk memodelkan persyaratan komputasi yang kompleks melalui komposisi. Selain itu, pemisahan perhitungan menjadi perintah diskrit yang beroperasi pada konteks tujuan umum memungkinkan pembuatan perintah yang lebih mudah, yang dapat diuji unit, karena dampak mengeksekusi perintah dapat diukur secara langsung dengan mengamati perubahan keadaan terkait dalam konteks yang disediakan. ...

Aldrin Leal
sumber
0

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.

mgoeminne
sumber
Hai, saya memperbarui pertanyaan saya dengan beberapa pseudocode - kami sebenarnya memiliki langkah-langkah eksplisit.
RuslanD