Kode program ditulis dari atas ke bawah. Instruksi dibaca dan dieksekusi dalam urutan yang sama. Ini logis dan semua orang sudah lama terbiasa. Dalam beberapa kasus, Anda dapat mengubah urutan operasi. Tetapi kadang-kadang urutan pemanggilan fungsi penting, meskipun secara sintaksis hal ini tidak jelas. Ternyata kode itu terlihat berfungsi, bahkan setelah mengatur ulang panggilan, dan hasilnya tidak terduga.
Suatu kali, kode yang sama menarik perhatian saya.
Masalah
Suatu hari, ketika mencari-cari kode orang lain dalam proyek bersama, saya menemukan fungsi seperti:
public Product fill(Product product, Images images, Prices prices, Availabilities availabilities){ priceFiller.fill(product, prices);
Tentu saja, bukan gaya yang paling "elegan" yang menarik perhatian Anda: kelas menyimpan data (POJO), fungsi mengubah objek yang masuk ...
Secara umum, sepertinya tidak ada apa-apa. Kami memiliki objek di mana tidak ada cukup data, dan ada data itu sendiri yang berasal dari sumber lain (layanan), yang sekarang akan kita tempatkan di objek ini untuk membuatnya lengkap.
Namun ada beberapa nuansa.
- Saya tidak menyukai kenyataan bahwa tidak ada satu fungsi pun yang ditulis dalam gaya FP dan memodifikasi objek yang dilewatkan sebagai argumen.
- Tetapi katakanlah ini dilakukan untuk mengurangi waktu pemrosesan dan jumlah objek yang dibuat.
- Hanya komentar dalam kode yang mengatakan bahwa urutan panggilan itu penting, dan Anda harus berhati-hati saat menyematkan
Filler
baru
- Tetapi jumlah orang yang mengerjakan proyek ini lebih dari 1 dan tidak semua orang tahu tentang trik ini. Terutama orang-orang baru dalam tim (belum tentu di perusahaan).
Poin terakhir terutama melindungi saya. Lagipula, API dibangun sedemikian rupa sehingga kita tidak tahu apa yang telah berubah pada objek setelah memanggil fungsi ini. Misalnya, sebelum dan sesudah kami memiliki metode Product::getImages
, yang, sebelum memanggil fungsi fill
, akan menghasilkan daftar kosong, dan kemudian daftar dengan gambar ke produk kami.
Dengan Filler
segalanya menjadi lebih buruk. AvailabilityFiller
tidak memperjelas bahwa mereka mengharapkan informasi tentang harga barang sudah tertanam dalam objek yang ditransfer.
Jadi saya berpikir tentang bagaimana saya bisa melindungi kolega saya dari kesalahan penggunaan fungsi.
Solusi yang diajukan
Pertama, saya memutuskan untuk membahas kasus ini dengan kolega saya. Sayangnya, semua solusi yang mereka usulkan tampaknya tidak cocok untuk saya.
Runtimeexception
Salah satu opsi yang diusulkan adalah: dan Anda menulis di AvailabilityFiller
di awal fungsi Objects.requireNonNull(product.getPrices)
dan kemudian programmer mana pun akan menerima kesalahan selama tes lokal.
- tetapi harga mungkin benar-benar tidak ada di sana, jika layanan tidak tersedia atau kesalahan lain, maka produk tersebut harus menerima status "kehabisan stok". Kami harus mengaitkan semua jenis bendera atau apa pun untuk membedakan "tidak ada data" dari "bahkan tidak diminta".
- Jika Anda melempar pengecualian di
getPrices
sendiri, maka kami akan membuat masalah yang sama dengan Java modern dengan daftar
- Misalkan Daftar dilewatkan ke fungsi yang menawarkan metode get dalam API-nya ... Saya tahu Anda tidak perlu mengubah objek yang ditransfer, tetapi buat yang baru. Tetapi intinya adalah bahwa API menawarkan metode seperti itu kepada kami, tetapi dalam runtime kesalahan dapat terjadi jika itu adalah daftar yang tidak dapat diubah, seperti yang diperoleh dari Collectors.toList ()
- jika
AvailabilityFiller
digunakan oleh orang lain, programmer yang menulis panggilan tidak akan langsung mengerti apa masalahnya. Hanya setelah peluncuran dan Debug. Kemudian dia masih harus memahami kode untuk mencari tahu di mana mendapatkan data.
Tes
"Dan kamu menulis tes yang akan pecah jika kamu mengubah urutan panggilan." yaitu jika semua Filler
mengembalikan produk "baru", sesuatu seperti ini akan berubah:
given(priceFillerMock.fill(eq(productMock), any())).willReturn(productWithPricesMock); given(availabilityFillerMock.fill(eq(productMockWithPrices), any())).willReturn(productMockWithAvailabilities); given(imageFillerMock.fill(eq(productMockWithAvailabilities), any())).willReturn(productMockWithImages); var result = productFiller.fill(productMock, p1, p2, p3); assertThat("unexpected return value", result, is(productMockWithImages));
- Saya tidak suka tes yang begitu "Kotak Putih"
- Beristirahat dengan setiap
Filler
baru - Itu rusak ketika mengubah urutan panggilan independen
- Sekali lagi, itu tidak menyelesaikan masalah penggunaan kembali
AvailabilityFiller
itu sendiri
Upaya sendiri untuk menyelesaikan masalah
Ide
Saya pikir Anda sudah menebak bahwa saya ingin menyelesaikan masalah di tingkat kompilasi. Nah, mengapa saya perlu, seseorang bertanya, bahasa yang dikompilasi dengan pengetikan yang kuat jika saya tidak dapat mencegah kesalahan.
Dan saya bertanya-tanya apakah sebuah objek tanpa data tambahan dan objek "extended" milik kelas yang sama?
Bukankah benar untuk menggambarkan berbagai keadaan yang mungkin dari suatu objek sebagai kelas atau antarmuka yang terpisah?
Jadi ide saya adalah ini:
public Product fill(<? extends BaseProduct> product, Images images, Prices prices, Availabilities availabilities){ var p1 = priceFiller.fill(product, prices); var p2 = availabilityFiller.fill(p1, availabilities); return imageFiller.fill(p2, images); } PriceFiller public ProductWithPrices fill(<? extends BaseProduct> product, Prices prices) AvailabilityFiller public ProductWithAvailabilities fill(<? extends ProductWithPrices> product, Prices prices) public <BaseProduct & PriceAware & AvailabilityAware> fill(<? extends BaseProduct & PriceAware> product, Prices prices)
Yaitu produk yang awalnya didefinisikan adalah turunan dari kelas selain yang dikembalikan, yang sudah menunjukkan perubahan data.
Filler
, di API mereka, tentukan dengan tepat data apa yang mereka butuhkan dan apa yang mereka kembalikan.
Dengan cara ini Anda dapat mencegah urutan panggilan yang salah.
Implementasi
Bagaimana menerjemahkan ini menjadi kenyataan di Jawa? (Ingatlah bahwa mewarisi dari beberapa kelas di Jawa tidak dimungkinkan.)
Kompleksitas ditambahkan oleh operasi independen. Misalnya, gambar dapat ditambahkan sebelum dan sesudah menambahkan harga, serta di akhir fungsi.
Lalu mungkin
class ProductWithImages extends BaseProduct implements ImageAware{} class ProductWithImagesAndPrices extends BaseProduct implements ImageAware, PriceAware{} class Product extends BaseProduct implements ImageAware, PriceAware, AvailabilityAware{}
Bagaimana cara menggambarkan semuanya?
Buat adaptor?
public ProductWithImagesAndPrices(<? extends BaseProduct & PriceAware> base){ this.base = base; this.images = Collections.emptyList(); } public long getId(){ return this.base.getId(); } public Price getPrice(){ return this.base.getPrice(); } public List<Image> getImages(){ return this.images; }
Salin data / tautan?
public ProductWithImagesAndPrices(<? extends BaseProduct & PriceAware> base){ this.id = base.getId(); this.prices = base.getPrices(); this.images = Collections.emptyList(); } public List<Image> getImages(){ return this.images; }
Seperti yang sudah terlihat, semuanya hanya memiliki sejumlah besar kode. Dan ini terlepas dari kenyataan bahwa dalam contoh saya hanya menyisakan 3 jenis data input. Di dunia nyata, bisa ada banyak lagi.
Ternyata biaya penulisan dan pemeliharaan kode seperti itu tidak membenarkan diri mereka sendiri, walaupun gagasan untuk membagi negara menjadi kelas-kelas terpisah tampak sangat menarik bagi saya.
Mundur
Jika Anda melihat bahasa lain, maka di suatu tempat masalah ini lebih mudah untuk dipecahkan, tetapi di suatu tempat tidak.
Misalnya, di Go, Anda dapat menulis referensi ke kelas yang dapat dikembangkan tanpa metode "menyalin" atau "membebani". Tapi ini bukan tentang Go
Penyimpangan lain
Saat menulis artikel ini, solusi lain yang mungkin muncul dengan Proxy
, yang meminta penulisan hanya metode baru, tetapi membutuhkan hierarki antarmuka. Secara umum, menakutkan, marah dan tidak cocok. Jika seseorang tiba-tiba tertarik:
Sebelum tidur dan makan, tidak disarankan untuk melihat ini public class Application { public static void main(String[] args) { var baseProduct = new BaseProductProxy().create(new BaseProductImpl(100L)); var productWithPrices = fillPrices(baseProduct, BigDecimal.TEN); var productWithAvailabilities = fillAvailabilities(productWithPrices, "available"); var productWithImages = fillImages(productWithAvailabilities, List.of("url1, url2")); var product = productWithImages; System.out.println(product.getId()); System.out.println(product.getPrice()); System.out.println(product.getAvailability()); System.out.println(product.getImages()); } static <T extends BaseProduct> ImageAware fillImages(T base, List<String> images) { return (ImageAware) Proxy.newProxyInstance(base.getClass().getClassLoader(), new Class[]{ImageAware.class, BaseProduct.class}, new MyInvocationHandler<>(base, new ImageAware() { @Override public List<String> getImages() { return images; } })); } static <T extends BaseProduct> PriceAware fillPrices(T base, BigDecimal price) { return (PriceAware) Proxy.newProxyInstance(base.getClass().getClassLoader(), new Class[]{PriceAware.class}, new MyInvocationHandler<>(base, new PriceAware() { @Override public BigDecimal getPrice() { return price; } })); } static AvailabilityAware fillAvailabilities(PriceAware base, String availability) { return (AvailabilityAware) Proxy.newProxyInstance(base.getClass().getClassLoader(), new Class[]{AvailabilityAware.class}, new MyInvocationHandler<>(base, new AvailabilityAware() { @Override public String getAvailability() { return base.getPrice().intValue() > 0 ? availability : "sold out"; } })); } static class BaseProductImpl implements BaseProduct { private final long id; BaseProductImpl(long id) { this.id = id; } @Override public long getId() { return id; } } static class BaseProductProxy { BaseProduct create(BaseProduct base) { return (BaseProduct) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{BaseProduct.class}, new MyInvocationHandler<>(base, base)); } } public interface BaseProduct { default long getId() { return -1L; } } public interface PriceAware extends BaseProduct { default BigDecimal getPrice() { return BigDecimal.ZERO; } } public interface AvailabilityAware extends PriceAware { default String getAvailability() { return "sold out"; } } public interface ImageAware extends AvailabilityAware { default List<String> getImages() { return Collections.emptyList(); } } static class MyInvocationHandler<T extends BaseProduct, U extends BaseProduct> implements InvocationHandler { private final U additional; private final T base; MyInvocationHandler(T base, U additional) { this.additional = additional; this.base = base; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Arrays.stream(additional.getClass().getInterfaces()).anyMatch(i -> i == method.getDeclaringClass())) { return method.invoke(additional, args); } var baseMethod = Arrays.stream(base.getClass().getMethods()).filter(m -> m.getName().equals(method.getName())).findFirst(); if (baseMethod.isPresent()) { return baseMethod.get().invoke(base, args); } throw new NoSuchMethodException(method.getName()); } } }
Kesimpulan
Apa yang terjadi? Di satu sisi, ada pendekatan menarik yang menerapkan kelas yang terpisah untuk "objek" di berbagai negara dan menjamin pencegahan kesalahan yang disebabkan oleh urutan panggilan yang salah untuk metode yang memodifikasi objek ini.
Di sisi lain, pendekatan ini membuat Anda menulis begitu banyak kode sehingga Anda langsung ingin menolaknya. Banyaknya antarmuka dan kelas hanya membuat proyek sulit dipahami.
Dalam proyek saya yang lain, saya masih mencoba menggunakan pendekatan ini. Dan pada awalnya di tingkat antarmuka, semuanya baik-baik saja. Saya menulis fungsinya:
<T extends Foo> List<T> firstStep(List<T> ts){} <T extends Foo & Bar> List<T> nStep(List<T> ts){} <T extends Foo> List<T> finalStep(List<T> ts){}
Dengan demikian menunjukkan bahwa langkah pemrosesan data tertentu memerlukan informasi tambahan yang tidak diperlukan baik pada awal pemrosesan atau pada akhirnya.
Menggunakan mock
dan, saya berhasil menguji kode. Tetapi ketika sampai pada implementasi dan jumlah data dan berbagai sumber mulai bertambah, saya dengan cepat menyerah dan mengubah semuanya menjadi tampilan โnormalโ. Semuanya berfungsi dan tidak ada yang mengeluh. Ternyata efisiensi dan kesederhanaan kode menang atas "pencegahan" kesalahan, dan Anda dapat melacak urutan panggilan yang benar secara manual, bahkan jika kesalahan memanifestasikan dirinya hanya pada tahap pengujian manual.
Mungkin jika saya mengambil langkah mundur dan melihat kode dari sisi lain, saya akan memiliki solusi yang sama sekali berbeda. Tetapi kebetulan saya menjadi tertarik pada baris komentar khusus ini.
Sudah di akhir artikel, memikirkan fakta bahwa karena tidak bagus untuk menggambarkan setter di antarmuka, Anda dapat membayangkan perakitan data produk dalam bentuk Builder
, yang mengembalikan antarmuka yang berbeda setelah menambahkan data yang ditentukan. Sekali lagi, itu semua tergantung pada kompleksitas logika objek bangunan. Jika Anda bekerja dengan Spring Security, maka Anda terbiasa dengan solusi semacam ini.
Sebagai contoh saya, begini:
Solusi Berbasis Pola Pembangun public class Application_2 { public static void main(String[] args) { var product = new Product.Builder() .id(1000) .price(20) .availability("available") .images(List.of("url1, url2")) .build(); System.out.println(product.getId()); System.out.println(product.getAvailability()); System.out.println(product.getPrice()); System.out.println(product.getImages()); } static class Product { private final int price; private final long id; private final String availability; private final List<String> images; private Product(int price, long id, String availability, List<String> images) { this.price = price; this.id = id; this.availability = availability; this.images = images; } public int getPrice() { return price; } public long getId() { return id; } public String getAvailability() { return availability; } public List<String> getImages() { return images; } public static class Builder implements ProductBuilder, ProductWithPriceBuilder { private int price; private long id; private String availability; private List<String> images; @Override public ProductBuilder id(long id) { this.id = id; return this; } @Override public ProductWithPriceBuilder price(int price) { this.price = price; return this; } @Override public ProductBuilder availability(String availability) { this.availability = availability; return this; } @Override public ProductBuilder images(List<String> images) { this.images = images; return this; } public Product build(){ var av = price > 0 && availability != null ? availability : "sold out"; return new Product(price, id, av, images); } } public interface ProductBuilder { ProductBuilder id(long id); ProductBuilder images(List<String> images); ProductWithPriceBuilder price(int price); Product build(); } public interface ProductWithPriceBuilder{ ProductBuilder availability(String availability); } } }
Jadi itu:
- Tulis fitur bersih
- Tulis kode yang indah dan jelas
- Ingat bahwa singkatnya adalah saudara perempuan dan yang paling penting adalah kode bekerja
- Jangan ragu untuk bertanya hal-hal, mencari cara lain, dan bahkan memunculkan solusi alternatif, dalam hal ini
- Jangan diam! Ketika menjelaskan masalah kepada orang lain, solusi yang lebih baik dilahirkan (Duck Duck Driven Developent)
Terima kasih atas perhatian anda