تأملات في OOP وحالة الكائنات

رمز البرنامج مكتوب من أعلى إلى أسفل. تتم قراءة التعليمات وتنفيذها بنفس الترتيب. هذا منطقي وقد اعتاد الجميع عليه. في بعض الحالات ، يمكنك تغيير ترتيب العمليات. لكن في بعض الأحيان يكون تسلسل استدعاءات الوظائف أمرًا مهمًا ، على الرغم من أن هذا غير واضح بناءً. اتضح أن الرمز يبدو فعالاً ، حتى بعد إعادة ترتيب المكالمات ، والنتيجة غير متوقعة.


مرة واحدة ، اشتعلت رمز مماثل عيني.


المشكلة


ذات مرة ، اكتشفت رمز شخص آخر في مشروع مشترك ، اكتشفت وظيفة مثل:


public Product fill(Product product, Images images, Prices prices, Availabilities availabilities){ priceFiller.fill(product, prices); //do not move this line below availabilityFiller call, availabilities require prices availabilityFiller.fill(product, availabilities); imageFiller.fill(product, images); return product; } 

بالطبع ، ليس النمط الأكثر أناقة يلفت انتباهك: بيانات تخزين الفصول (POJO) ، وظائف تغيير الكائنات الواردة ...


بشكل عام ، يبدو أن لا شيء. لدينا كائن لا توجد فيه بيانات كافية ، وهناك بيانات في حد ذاتها تأتي من مصادر (خدمات) أخرى ، والتي سنضعها الآن في هذا الكائن لإكمالها.


ولكن هناك بعض الفروق الدقيقة.


  1. لا أحب حقيقة أنه لا توجد وظيفة واحدة مكتوبة بأسلوب FP وتعديل الكائن الذي تم تمريره كوسيطة.
    • ولكن دعنا نقول أن هذا قد تم لتقليل وقت المعالجة وعدد الكائنات التي تم إنشاؤها.
  2. فقط تعليق في الكود يوضح أن تسلسل المكالمات أمر مهم ، ويجب أن تكون حذرًا عند تضمين Filler الجديد
    • لكن عدد الأشخاص الذين يعملون في المشروع هو أكثر من شخص واحد وليس كل شخص يعرف عن هذه الخدعة. أشخاص جدد خصوصًا في الفريق (ليس بالضرورة في المؤسسة).

النقطة الأخيرة حراسة خاصة لي. بعد كل شيء ، تم تصميم API بطريقة لا نعرف ما الذي تغير في الكائن بعد استدعاء هذه الوظيفة. على سبيل المثال ، قبل وبعد أن لدينا طريقة Product::getImages ، والتي ، قبل استدعاء وظيفة fill ، ستنتج قائمة فارغة ، ثم قائمة تحتوي على صور لمنتجنا.


مع Filler الأمور أسوأ. لا يوضح AvailableFiller أنه يتوقع أن تكون المعلومات حول سعر البضاعة مضمنة بالفعل في الكائن المنقول.


وهكذا فكرت في الكيفية التي يمكنني بها حماية زملائي من الاستخدام الخاطئ للوظائف.


الحلول المقترحة


أولاً ، قررت مناقشة هذه القضية مع زملائي. لسوء الحظ ، فإن كل الحلول التي اقترحوها لا تبدو لي النهج الصحيح.


RuntimeException


كان أحد الخيارات المقترحة: وأنت تكتب في AvailabilityFiller في بداية الدالة Objects.requireNonNull(product.getPrices) وبعد ذلك سيتلقى أي مبرمج خطأ بالفعل أثناء الاختبارات المحلية.


  • ولكن قد لا يكون السعر موجودًا بالفعل ، إذا كانت الخدمة غير متوفرة أو حدث خطأ آخر ، فيجب أن يحصل المنتج على حالة "نفاد المخزون". سيتعين علينا أن نعزو جميع أنواع الأعلام أو أي شيء لتمييز "لا توجد بيانات" عن "لا يطلب حتى".
    • إذا قمت بطرح استثناء في getPrices نفسها ، getPrices نفس المشكلات مثل Java الحديثة مع القوائم
      • افترض أن يتم تمرير قائمة إلى وظيفة توفر طريقة get في API الخاصة بها ... أعرف أنك لست بحاجة إلى تغيير الكائنات المنقولة ، ولكن إنشاء كائنات جديدة. لكن خلاصة القول هي أن API تقدم لنا مثل هذه الطريقة ، ولكن في وقت التشغيل قد يحدث خطأ إذا كانت قائمة ثابتة ، مثل تلك التي تم الحصول عليها من Collectors.toList ()
  • إذا تم استخدام AvailableFiller من قبل شخص آخر ، فلن يفهم المبرمج الذي كتب المكالمة على الفور المشكلة. فقط بعد الاطلاق والتصحيح. ثم لا يزال يتعين عليه فهم الكود لمعرفة مكان الحصول على البيانات.

اختبار


"وستكتب اختبارًا سينتهي إذا غيرت ترتيب المكالمات." أي إذا قامت جميع شركات Filler بإرجاع منتج "جديد" ، فسيظهر شيء مثل هذا:


 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)); 

  • أنا لا أحب الاختبارات التي هي "White-Box"
  • فواصل مع كل Filler جديد
  • ينهار عند تغيير تسلسل المكالمات المستقلة
  • مرة أخرى ، لا يحل مشكلة إعادة استخدام AvailableFiller نفسها

محاولات خاصة لحل المشكلة


فكرة


أعتقد أنك خمنت بالفعل أنني أود حل المشكلة على مستوى الترجمة. حسنًا ، لماذا أحتاج إلى لغة مترجمة مع كتابة قوية إذا لم أتمكن من منع الخطأ.


وتساءلت عما إذا كان كائن بدون بيانات إضافية وكائن "ممتد" ينتمي إلى نفس الفئة؟


ألن يكون من الصواب وصف الحالات المختلفة الممكنة للكائن بفئات أو واجهات منفصلة؟


لذلك كانت فكرتي هذا:


 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) 

أي المنتج المحدد أصلاً هو مثيل لفئة أخرى غير الفئة التي يتم إرجاعها ، والتي تعرض بالفعل تغييرات في البيانات.


تحدد Filler ، في واجهات برمجة التطبيقات الخاصة بها ، البيانات التي يحتاجون إليها بالضبط وما يعيدونها.


وبهذه الطريقة يمكنك منع تسلسل الاتصال الخاطئ.


تطبيق


كيف تترجم هذا إلى واقع في جافا؟ (تذكر أن الوراثة من فئات متعددة في Java غير ممكن.)


يضاف التعقيد من خلال عمليات مستقلة. على سبيل المثال ، يمكن إضافة الصور قبل وبعد إضافة الأسعار ، وكذلك في نهاية الوظيفة.
ثم ربما


 class ProductWithImages extends BaseProduct implements ImageAware{} class ProductWithImagesAndPrices extends BaseProduct implements ImageAware, PriceAware{} class Product extends BaseProduct implements ImageAware, PriceAware, AvailabilityAware{} 

كيف تصف كل شيء؟


إنشاء محولات؟


 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; } 

نسخ البيانات / الروابط؟


 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; } 

كما هو ملحوظ بالفعل ، كل هذا يأتي فقط إلى كمية هائلة من الكود. وهذا على الرغم من حقيقة أنني في المثال تركت فقط 3 أنواع من بيانات الإدخال. في العالم الحقيقي ، يمكن أن يكون هناك الكثير.


اتضح أن تكاليف الكتابة والحفاظ على مثل هذا الرمز لا تبرر نفسها ، على الرغم من أن فكرة تقسيم الدولة إلى فئات منفصلة تبدو جذابة للغاية بالنسبة لي.


تراجع


إذا نظرت إلى لغات أخرى ، فمن السهل حل هذه المشكلة في مكان ما ، ولكن في مكان ما لا.
على سبيل المثال ، في Go ، يمكنك كتابة مرجع لفئة قابلة للمد دون طرق "نسخ" أو "التحميل الزائد". لكنها ليست عن الذهاب

استطرادا آخر


أثناء كتابة هذا المقال ، توصل حل آخر محتمل إلى Proxy ، والذي دعا إلى كتابة طرق جديدة فقط ، ولكن يتطلب تسلسل هرمي للواجهات. بشكل عام ، مخيف ، غاضب وغير مناسب. إذا كان شخص ما مهتمًا فجأة:

قبل الذهاب إلى السرير وتناول الطعام ، لا ينصح أن ننظر إلى هذا
 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()); } } } 

استنتاج


ما تبين؟ من ناحية ، هناك نهج مثير للاهتمام يطبق فئات منفصلة على "كائن" في حالات مختلفة ويضمن منع الأخطاء الناتجة عن تسلسل غير صحيح للمكالمات على الأساليب التي تعدل هذا الكائن.


من ناحية أخرى ، فإن هذا الأسلوب يجعلك تكتب رمزًا كبيرًا لدرجة أنك تريد رفضه على الفور. كومة من واجهات وفئات فقط يجعل من الصعب فهم المشروع.


في مشروعي الآخر ، ما زلت أحاول استخدام هذا النهج. وفي البداية على مستوى الواجهة ، كان كل شيء على ما يرام. كتبت الوظائف:


 <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){} 

بعد الإشارة إلى أن خطوة معينة لمعالجة البيانات تتطلب معلومات إضافية غير مطلوبة سواء في بداية المعالجة أو في نهايتها.


باستخدام mock و ، تمكنت من اختبار الرمز. ولكن عندما يتعلق الأمر بالتنفيذ وبدأت كمية البيانات والمصادر المختلفة في النمو ، فقد تخليت بسرعة وأعدت كل شيء في شكل "طبيعي". كل شيء يعمل ولا أحد يشكو. اتضح أن كفاءة وبساطة الرمز ينتصران على "منع" الأخطاء ، ويمكنك تتبع التسلسل الصحيح للمكالمات يدويًا ، حتى لو كان الخطأ يظهر في مرحلة الاختبار اليدوي فقط.


ربما إذا عدت خطوة إلى الوراء ونظرت إلى الكود من الجانب الآخر ، سيكون لدي حلول مختلفة تمامًا. لكن ما حدث هو أنني أصبحت مهتمًا بهذا السطر المعين.


في نهاية المقالة بالفعل ، بالتفكير في حقيقة أنه نظرًا لأنه ليس من الجيد وصف المستعمرات في الواجهات ، يمكنك أن تتخيل تجميع بيانات المنتج في شكل أداة إنشاء ، والتي تُرجع واجهة مختلفة بعد إضافة البيانات المحددة. مرة أخرى ، كل هذا يتوقف على تعقيد منطق بناء الكائنات. إذا كنت تعمل مع Spring Security ، فأنت على دراية بهذا النوع من الحلول.


على سبيل المثال ، يذهب:


الحل القائم على نمط البناء
 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); } } } 

بحيث:


  • اكتب ميزات نظيفة
  • اكتب رمزًا جميلًا وواضحًا
  • تذكر أن الإيجاز هو أخت والأهم من ذلك أن التعليمات البرمجية تعمل
  • لا تتردد في استجواب الأشياء ، والبحث عن طرق أخرى ، وحتى طرح حلول بديلة لهذه المسألة
  • لا تصمت! عند شرح مشكلة للآخرين ، تولد حلول أفضل (مطور يحركه البط المطاطي)

شكرا لاهتمامكم

Source: https://habr.com/ru/post/ar478826/


All Articles