Sekali waktu di satu bank yang jauh, jauh ...
Selamat siang, Habr. Hari ini, akhirnya, tangan saya sampai di sini lagi untuk menulis. Tetapi tidak seperti tutorial sebelumnya - artikel hari ini saya ingin berbagi pengalaman saya dan menunjukkan kekuatan mekanisme seperti generik, yang, bersama dengan sihir musim semi, menjadi lebih kuat. Saya ingin segera memperingatkan Anda bahwa untuk memahami artikel Anda perlu mengetahui dasar-dasar pegas dan memiliki ide-ide tentang obat generik yang lebih dari sekadar "Generik, yah, apa yang kami tunjukkan dalam tanda kutip di ArrayList".
Episode 1:
Untuk memulainya, tugas yang saya miliki di tempat kerja adalah sesuatu seperti ini: ada sejumlah besar transfer uang dengan sejumlah bidang umum tertentu. Selain itu, setiap terjemahan terkait dengan kelas - permintaan untuk transfer dari satu negara ke negara lain dan pengalihan ke api lain. Oleh karena itu, ada pembangun yang terlibat dalam konversi.
Saya memecahkan masalah dengan bidang umum hanya dengan warisan. Jadi saya mendapat kelas:
public class Transfer { private TransferType transferType; ... } public enum TransferType { INTERNAL, SWIFT, ...; } public class InternalTransfer extends Transfer { ... } public class BaseRequest { ... } public class InternalRequest extends BaseRequest { ... } ...
Episode 2:
Lalu ada masalah dengan pengendali - mereka semua harus memiliki metode yang sama - checkTransfer, approveTransfer, dll. Di sini, untuk yang pertama, tetapi bukan yang terakhir, obat generik bermanfaat bagi saya: Saya membuat pengendali umum dengan metode yang diperlukan, dan mewarisi sisanya dari itu:
@AllArgsConstructor public class TransferController<T extends Transfer> { private final TransferService<T> service; public CheckResponse checkTransfer(@RequestBody @Valid T payment) { return service.checkTransfer(payment); } ... } public class InternalTransferController extends TransferController<InternalTransfer> { public InternalTransferController(TransferService<InternalTransfer> service) { super(service); } }
Sebenarnya layanan ini:
public interface TransferService<T extends Transfer> { CheckResponse checkTransfer(T payment); ApproveResponse approveTransfer(T payment); ... }
Dengan demikian, masalah copy-paste berkurang hanya untuk memanggil superconstructor, dan dalam layanan kami biasanya kehilangan itu.
Tapi!
Episode 3:
Masih ada masalah di dalam layanan:
Bergantung pada jenis transfer, berbagai pembangun harus dipanggil:
RequestBuilder builder; switch (type) { case INTERNAL: { builder = beanFactory.getBean(InternalRequestBuilder.class); break; } case SWIFT: { builder = beanFactory.getBean(SwiftRequestBuilder.class); break; } default: { log.info("Unknown payment type"); throw new UnknownPaymentTypeException(); } }
antarmuka pembangun umum:
public interface RequestBuilder<T extends BaseRequest, U extends Transfer> { T createRequest(U transfer); }
Metode pabrik muncul untuk optimasi di sini, sebagai akibatnya, sakelar / kasing berada dalam kelas yang terpisah. Tampaknya lebih baik, tetapi masalahnya tetap sama - ketika Anda menambahkan terjemahan baru, Anda harus memodifikasi kode, dan saklar besar / kasing tidak cocok untuk saya.
Episode 4:
Apa jalan keluarnya? Pada awalnya, terpikir oleh saya untuk menentukan jenis terjemahan dengan nama kelas dan memanggil pembangun yang diinginkan menggunakan refleksi, yang akan memaksa pengembang yang akan bekerja dengan proyek untuk memenuhi persyaratan tertentu untuk nama-nama kelas mereka. Tetapi ada solusi yang lebih baik. Setelah bertanya-tanya, Anda bisa sampai pada kesimpulan bahwa aspek utama dari logika bisnis aplikasi adalah terjemahannya sendiri. Artinya, jika tidak ada, tidak akan ada yang lain. Jadi mengapa tidak mengikat semuanya? Cukup untuk sedikit memodifikasi kelas kami. Dan lagi obat generik datang untuk menyelamatkan.
Kelas Permintaan:
public class BaseRequest<T extends Transfer> { ... } public class InternalRequest extends BaseRequest<InternalTransfer> { ... }
Dan antarmuka pembangun:
public interface RequestBuilder<T extends Transfer> { BaseRequest<T> createRequest(T transfer); }
Dan di sini menjadi lebih menarik. Kami dihadapkan dengan fitur generik yang hampir tidak pernah disebutkan di mana pun dan digunakan terutama dalam kerangka kerja dan perpustakaan. Memang, sebagai BaseRequest kita bisa mengganti pewarisnya, yang sesuai dengan tipe T, yaitu:
public class InternalRequestBuilder implements RequestBuilder<InternalTransfer> { @Override public InternalRequest createRequest(InternalTransfer transfer) { return InternalRequest.builder() ... .build(); } }
Saat ini, kami telah mencapai peningkatan yang baik dalam arsitektur aplikasi kami. Tetapi masalah sakelar / kasus belum terselesaikan. Atau ...?
Episode 5:
Di sinilah keajaiban musim semi ikut bermain.
Faktanya adalah bahwa kita memiliki kesempatan untuk mendapatkan array nama bin yang sesuai dengan tipe yang diinginkan menggunakan metode
getBeanNamesForType (tipe ResolvableType) . Dan di kelas ResolvableType ada metode statis
untukClassWithGenerics (Kelas <?> Clazz, Kelas <?> ... generik) , di mana Anda harus lulus kelas (antarmuka) sebagai parameter pertama, yang menggunakan parameter kedua sebagai generik dan mengembalikan tipe yang sesuai. T e:
ResolvableType type = ResolvableType.forClassWithGenerics(RequestBuilder.class, transfer.getClass());
Mengembalikan yang berikut:
RequestBuilder<InternalTransfer>
Dan sekarang sedikit lebih ajaib - faktanya adalah bahwa jika Anda autowire sheet dengan antarmuka sebagai generik, maka semua implementasinya akan terkandung di dalamnya:
private final List<RequestBuilder<T>> builders;
Kami hanya harus menjalaninya dan menemukan yang sesuai menggunakan contoh instans:
builders.stream() .filter(b -> type.isInstance(b)) .findFirst() .get();
Demikian pula dengan opsi ini, masih ada kesempatan untuk autowire ApplicationContext atau BeanFactory, dan panggil metode getBeanNamesForType () pada mereka di mana harus mengirimkan tipe kami sebagai parameter. Tapi ini dianggap sebagai tanda selera buruk dan arsitektur ini tidak perlu (terima kasih khusus kepada
zolt85 untuk komentarnya).
Akibatnya, metode pabrik kami mengambil bentuk berikut:
@Component @AllArgsConstructor public class RequestBuildersFactory<T extends Transfer> { private final List<RequestBuilder<T>> builders; public BaseRequest<T> transferToRequest(T transfer) { ResolvableType type = ResolvableType.forClassWithGenerics(RequestBuilder.class, transfer.getClass()); RequestBuilder<T> builder = builders.stream() .filter(b -> type.isInstance(b)) .findFirst() .get(); return builder.createRequest(transfer, stage); } }
Episode 6: Kesimpulan
Dengan demikian, kami memiliki kerangka kerja mini dengan arsitektur yang dipikirkan dengan matang, yang mewajibkan semua pengembang untuk mematuhinya. Dan yang penting, kami menyingkirkan sakelar yang rumit dan menambahkan terjemahan baru tidak akan memengaruhi kelas yang ada dengan cara apa pun, yang merupakan kabar baik.
PS:Artikel ini tidak menganjurkan penggunaan obat generik sedapat mungkin dan tidak mungkin, tetapi dengan bantuannya saya ingin membagikan mekanisme dan arsitektur kuat apa yang mereka izinkan Anda buat.
Ucapan Terima Kasih:Terima kasih khusus kepada
Sultansoy , yang tanpanya arsitektur ini tidak akan diingat dan, kemungkinan besar, artikel ini tidak akan terlintas.
Referensi:Kode sumber Github