Artikel ini akan membahas penggunaan pola Pipa & Filter.
Pertama, kita akan menganalisis contoh fungsi, yang akan kita tulis ulang nanti menggunakan pola yang disebutkan di atas. Perubahan kode akan terjadi secara bertahap dan setiap kali kita akan membuat versi yang bisa diterapkan sampai kita memikirkan solusi menggunakan DI (dalam contoh Musim Semi ini).
Dengan demikian, kami akan membuat beberapa solusi, memberikan peluang untuk menggunakan apa saja.
Pada akhirnya, kami membandingkan implementasi awal dan akhir, melihat contoh aplikasi dalam proyek nyata dan merangkumnya.
Tantangan
Misalkan kita memiliki banyak pakaian yang kita dapatkan dari pengeringan dan yang sekarang kita perlu pindah ke lemari. Ternyata data (pakaian) berasal dari layanan terpisah dan tugasnya adalah memberikan data ini kepada klien dalam bentuk yang benar (di lemari tempat ia bisa mendapatkan pakaian).
Dalam kebanyakan kasus, Anda tidak dapat menggunakan data yang diterima dalam bentuk yang diterima oleh kami. Data ini perlu diperiksa, diubah, disortir, dll.
Misalkan pelanggan menuntut pakaian yang harus disetrika jika mereka mint.
Kemudian untuk pertama kalinya kami membuat Modifier
, di mana kami meresepkan perubahan:
public class Modifier { public List<> modify(List<> ){ (); return ; } private void (List<> ) { .stream() .filter(::) .forEach(o -> {
Pada tahap ini, semuanya sederhana dan jelas. Mari kita menulis tes yang memeriksa bahwa semua pakaian yang kusut telah disetrika.
Namun seiring waktu, persyaratan baru muncul dan setiap kali fungsi kelas Modifier
meluas:
- Jangan menaruh cucian kotor di lemari.
- Kemeja, jaket dan celana panjang harus digantung di bahu.
- Kaus kaki bocor harus dijahit terlebih dahulu
- ...
Urutan perubahan juga penting. Misalnya, Anda tidak bisa menggantungkan pakaian terlebih dahulu di bahu mereka, lalu menyeterika.
Jadi, pada titik tertentu, Modifier
dapat mengambil bentuk berikut:
public class Modifier { private static final Predicate<> ___ = ((Predicate<>).class::isInstance) .or(.class::isInstance) .or(.class::isInstance) ; public List<> modify(List<> ){ (); (); (); ();
Sejalan dengan itu, tes menjadi lebih rumit, yang sekarang setidaknya harus memeriksa setiap langkah secara individual.
Dan ketika persyaratan baru tiba, melihat kode, kami memutuskan bahwa saatnya telah tiba untuk Refactoring.
Refactoring
Hal pertama yang menarik perhatian Anda adalah seringnya penghancuran semua pakaian. Jadi langkah pertama, kami memindahkan semuanya dalam satu siklus, dan juga mentransfer pemeriksaan kebersihan ke awal siklus:
public class Modifier { private static final Predicate<> ___ = ((Predicate<>).class::isInstance) .or(.class::isInstance) .or(.class::isInstance) ; public List<> modify(List<> ){ List<> result = new ArrayList<>(); for(var o : ){ if(o.()){ continue; } result.add(o); (o); (o); (o);
Sekarang, waktu pemrosesan untuk pakaian berkurang, tetapi kode ini masih terlalu lama untuk satu kelas dan untuk siklus tubuh. Mari kita coba memperpendek tubuh siklus pertama.
Setelah memeriksa kebersihan, Anda dapat melakukan semua panggilan dengan metode modify( )
terpisah modify( )
:
public List<> modify(List<> ){ List<> result = new ArrayList<>(); for(var o : ){ if(o.()){ continue; } result.add(o); modify(o); } return result; } private void modify( o) { (o); (o); (o);
Anda dapat menggabungkan semua panggilan menjadi satu Consumer
:
private Consumer<> modification = ((Consumer<>) this::) .andThen(this::) .andThen(this::);
Blunt: mengintip
Saya menggunakan mengintip singkatnya. Sonar akan mengatakan bahwa kode seperti itu tidak boleh dilakukan, karena Javadoc memberitahu mengintip bahwa metode ini ada terutama untuk debug. Tetapi jika Anda menulis ulang di peta: .map (o -> {modifikasi.accept (o); return o;}), maka IDEA akan mengatakan bahwa lebih baik menggunakan mengintip
Gagap: Konsumen
Contoh dengan Konsumen (dan selanjutnya dengan Fungsi) diberikan untuk menunjukkan kemampuan bahasa.
Sekarang tubuh siklus menjadi lebih pendek, tetapi sejauh ini kelas itu sendiri masih terlalu besar dan mengandung terlalu banyak informasi (pengetahuan tentang semua langkah).
Mari kita coba selesaikan masalah ini menggunakan pola pemrograman yang sudah ada. Dalam hal ini, kami akan menggunakan Pipes & Filters
.
Pipa & filter
Templat saluran dan filter menjelaskan pendekatan di mana data yang masuk melewati beberapa langkah pemrosesan.
Mari kita coba menerapkan pendekatan ini ke kode kita.
Langkah 1
Sebenarnya, kode kami sudah dekat dengan pola ini. Data yang diperoleh melalui beberapa langkah independen. Sejauh ini, setiap metode adalah filter, dan modify
sendiri menggambarkan saluran, pertama-tama menyaring semua pakaian kotor.
Sekarang kita akan mentransfer setiap langkah ke kelas yang terpisah dan melihat apa yang kita dapatkan:
public class Modifier { private final ; private final ; private final ;
Dengan demikian, kami menempatkan kode dalam kelas yang terpisah, menyederhanakan tes untuk transformasi individu (dan menciptakan kemungkinan menggunakan kembali langkah-langkah). Urutan panggilan menentukan urutan langkah-langkah.
Tetapi kelas itu sendiri masih tahu semua langkah individu, mengontrol urutan dan dengan demikian memiliki daftar besar dependensi. Selain menambahkan langkah baru, kami akan dipaksa untuk tidak hanya menulis kelas baru, tetapi juga menambahkannya ke Modfier
.
Langkah 2
Sederhanakan kode menggunakan Spring.
Pertama, buat antarmuka untuk setiap langkah:
interface Modification { void modify( ); }
Modifier
itu sendiri sekarang akan jauh lebih pendek:
public class Modifier { private final List<Modification> steps; @Autowired public Modifier(List<Modification> steps) { this.steps = steps; } public List<> modify(List<> ) { return .stream() .filter(o -> !o.()) .peek(o -> { steps.forEach(m -> m.modify(o)); }) .collect(Collectors.toList()); } }
Sekarang, untuk menambahkan langkah baru, Anda hanya perlu menulis kelas baru yang mengimplementasikan antarmuka Modification
dan meletakkan @Component
di atasnya. Spring akan menemukannya dan menambahkannya ke daftar.
Modifer
sendiri tidak tahu apa-apa tentang langkah-langkah individu, yang menciptakan "koneksi lemah" antara komponen.
Satu-satunya kesulitan adalah mengatur urutan. Untuk melakukan ini, Spring memiliki anotasi @Order
yang Anda bisa berikan nilai int. Daftar ini diurutkan dalam urutan menaik.
Dengan demikian, mungkin terjadi bahwa dengan menambahkan langkah baru di tengah daftar, Anda harus mengubah nilai pengurutan untuk langkah-langkah yang ada.
Pegas bisa dikeluarkan jika semua implementasi yang diketahui secara manual diteruskan ke konstruktor Modifier. Ini akan membantu menyelesaikan masalah penyortiran, tetapi sekali lagi menyulitkan penambahan langkah baru.
Langkah 3
Sekarang kami lulus ujian kebersihan di langkah terpisah. Untuk melakukan ini, kami menulis ulang antarmuka kami sehingga selalu memberikan nilai:
interface Modification { modify( ); }
Periksa kebersihan:
@Component @Order(Ordered.HIGHEST_PRECEDENCE) class CleanFilter implements Modification { modify( ) { if(.()){ return null; } return ; } }
Modifier.modify
:
public List<> modify(List<> ) { return .stream() .map(o -> { var modified = o; for(var step : steps){ modified = step.modify(o); if(modified == null){ return null; } } return modified; }) .filter(Objects::nonNull) .collect(Collectors.toList()); }
Dalam versi ini, Modifier
tidak memiliki informasi data apa pun. Dia hanya meneruskannya ke setiap langkah yang diketahui dan mengumpulkan hasilnya.
Jika salah satu langkah mengembalikan nol, maka pemrosesan untuk garmen ini terganggu.
Prinsip serupa digunakan dalam Spring for HandlerInterceptors. Sebelum dan sesudah panggilan pengontrol, semua Interceptor yang sesuai untuk URL ini dipanggil. Pada saat yang sama, ia mengembalikan benar atau salah dalam metode preHandle untuk menunjukkan apakah memproses dan memanggil Interceptor berikutnya dapat melanjutkan
Langkah n
Langkah selanjutnya adalah menambahkan metode matches
ke antarmuka Modification
, di mana langkah-langkah untuk atribut yang terpisah dari pakaian diperiksa:
interface Modification { modify( ); default matches( ) {return true;} }
Karena ini, Anda dapat sedikit menyederhanakan logika dalam modify
metode dengan memindahkan pemeriksaan untuk kelas dan properti ke metode yang terpisah.
Pendekatan serupa digunakan dalam Filter Pegas (Permintaan), tetapi perbedaan utama adalah bahwa setiap Filter adalah pembungkus berikutnya dan secara eksplisit memanggil FilterChain.doFilter untuk melanjutkan pemrosesan.
Total
Hasil akhirnya sangat berbeda dari versi awal. Membandingkannya, kita bisa menarik kesimpulan berikut:
- Implementasi berdasarkan Pipes & Filter menyederhanakan kelas
Modifier
itu sendiri. - Tanggung jawab yang didistribusikan dengan lebih baik dan koneksi βlemahβ antara komponen.
- Lebih mudah untuk menguji langkah-langkah individual.
- Lebih mudah untuk menambah dan menghapus langkah.
- Agak sulit untuk menguji seluruh rangkaian filter. Kami sudah membutuhkan IntegrationTests.
- Lebih banyak kelas
Pada akhirnya, opsi yang lebih nyaman dan fleksibel daripada yang asli.
Selain itu, Anda dapat memparalelkan pemrosesan data menggunakan parallelStream yang sama.
Apa contoh ini tidak terpecahkan
- Deskripsi pola mengatakan bahwa filter individual dapat digunakan kembali dengan membuat rantai filter lain (saluran).
- Di satu sisi, ini mudah dilakukan menggunakan
@Qualifier
. - Di sisi lain, pengaturan urutan berbeda dengan
@Order
akan gagal.
- Untuk contoh yang lebih kompleks, Anda harus menggunakan beberapa rantai, menggunakan rantai bersarang, dan masih mengubah implementasi yang ada.
- Jadi misalnya, tugas: "untuk setiap kaus kaki, cari sepasang dan masukkan ke dalam satu contoh <? Extends Clothing>" tidak akan cocok dengan implementasi yang dijelaskan, karena Sekarang, untuk setiap jari kaki, Anda harus memilah-milah semua linen dan mengubah daftar data awal.
- Untuk mengatasinya, Anda dapat menulis antarmuka baru yang menerima dan mengembalikan Daftar <Clothing> dan mentransfernya ke rantai baru. Tetapi Anda perlu berhati-hati dengan urutan panggilan rantai itu sendiri, jika kaus kaki hanya dapat dijahit oleh hotel.
Terima kasih atas perhatian anda