Czy to zły nawyk (nad) używanie refleksji?

16

Czy dobrą praktyką jest stosowanie odbicia, jeśli znacznie zmniejsza ilość kodu płyty grzewczej?

Zasadniczo istnieje kompromis między wydajnością i być może czytelnością z jednej strony a abstrakcją / automatyzacją / redukcją kodu płyty kotłowej z drugiej strony.

Edycja: Oto przykład zalecanego zastosowania odbicia .

Dla przykładu załóżmy, że istnieje klasa abstrakcyjna, Basektóra ma 10 pól i 3 podklasy SubclassA, SubclassBa SubclassCkażda z 10 różnymi polami; wszystkie są prostymi fasolami. Problem polega na tym, że otrzymujesz dwa Baseodwołania do typów i chcesz sprawdzić, czy odpowiadające im obiekty są tego samego (pod) typu i są równe.

Jako rozwiązania istnieje surowe rozwiązanie, w którym najpierw sprawdza się, czy typy są równe, a następnie sprawdza się wszystkie pola lub można użyć odbicia i dynamicznie sprawdzać, czy są one tego samego typu i iterować wszystkie metody zaczynające się od „get” (konwencja ponad konfigurację), wywołaj je na obu obiektach i wywołaj równe na wynikach.

boolean compare(Base base1, Base, base2) {
    if (base1 instanceof SubclassA && base2 instanceof SubclassA) { 
         SubclassA subclassA1 = (SubclassA) base1;
         SubclassA subclassA2 = (SubclassA) base2;
         compare(subclassA1, subclassA2);
    } else if (base1 instanceof SubclassB && base2 instanceof SubclassB) {
         //the same
    }
    //boilerplate
}

boolean compare(SubclassA subA1, SubclassA subA2) {
    if (!subA1.getField1().equals(subA2.getField1)) {
         return false;
    }
    if (!subA1.getField2().equals(subA2.getField2)) {
         return false;
    }
    //boilerplate
}

boolean compare(SubclassB subB1, SubclassB subB2) {
    //boilerplate
}

//boilerplate

//alternative with reflection 
boolean compare(Base base1, Base base2) {
        if (!base1.getClass().isAssignableFrom(base2.getClass())) {
            System.out.println("not same");
            System.exit(1);
        }
        Method[] methods = base1.getClass().getMethods();
        boolean isOk = true;
        for (Method method : methods) {
            final String methodName = method.getName();
            if (methodName.startsWith("get")) {
                Object object1 = method.invoke(base1);
                Object object2 = method.invoke(base2);
                if(object1 == null || object2 == null)  {
                    continue;
                }
                if (!object1.equals(object2)) {
                    System.out.println("not equals because " + object1 + " not equal with " + object2);
                    isOk = false;
                }
            }
        }

        if (isOk) {
            System.out.println("is OK");
        }
}
m3th0dman
źródło
20
Nadużywanie czegokolwiek jest złym nawykiem.
Tulains Córdova
1
@ user61852 Racja, zbyt duża wolność prowadzi do dyktatury. Niektóre stare Greki już o tym wiedziały.
ott--
6
„Zbyt dużo wody byłoby dla ciebie złe. Oczywiście, zbyt duża jest dokładnie ta ilość, która jest nadmierna - to właśnie oznacza! ”- Stephen Fry
Jon Purdy
4
„Za każdym razem, gdy piszesz kod formularza”, jeśli obiekt jest typu T1, zrób coś, ale jeśli jest typu T2, zrób coś innego, ” uderz
rm5248

Odpowiedzi:

25

Odbicie został stworzony do celów konkretnej, aby odkryć funkcjonalność klasy, która była nieznana w czasie kompilacji, podobny do tego, co dlopeni dlsymfunkcje zrobić w C. Jakiekolwiek użycie poza które powinny być mocno analizowane.

Czy zdarzyło Ci się kiedyś, że sami projektanci Java napotkali ten problem? Dlatego praktycznie każda klasa ma equalsmetodę. Różne klasy mają różne definicje równości. W niektórych okolicznościach obiekt pochodny może być równy obiektowi podstawowemu. W niektórych okolicznościach równość można ustalić na podstawie prywatnych pól bez osób pobierających. Nie wiesz

Dlatego każdy obiekt, który chce niestandardowej równości, powinien wdrożyć equalsmetodę. W końcu będziesz chciał umieścić obiekty w zestawie lub użyć ich jako indeksu skrótu, a następnie będziesz musiał zaimplementować equals. Inne języki robią to inaczej, ale Java używa equals. Powinieneś trzymać się konwencji swojego języka.

Również kod „płyty grzewczej”, jeśli zostanie umieszczony w odpowiedniej klasie, jest dość trudny do zepsucia. Odbicie dodaje dodatkową złożoność, co oznacza dodatkowe szanse na błędy. Na przykład w Twojej metodzie dwa obiekty są uważane za równe, jeśli jeden zwraca nullokreślone pole, a drugi nie. Co się stanie, jeśli jeden z twoich pobierających zwróci jeden z twoich przedmiotów bez odpowiedniego equals? Twoja if (!object1.equals(object2))wola zawiedzie. Również podatność na błędy polega na tym, że odbicie jest rzadko używane, więc programiści nie są tak dobrze zaznajomieni z jego błędami.

Karl Bielefeldt
źródło
12

Nadużywanie refleksji prawdopodobnie zależy od używanego języka. Tutaj używasz Java. W takim przypadku refleksję należy stosować ostrożnie, ponieważ często jest to tylko obejście złego projektu.

Porównując różne klasy, jest to idealny problem do zastępowania metod . Zauważ, że wystąpienia dwóch różnych klas nigdy nie powinny być uważane za równe. Możesz porównać dla równości tylko wtedy, gdy masz instancje tej samej klasy. Zobacz /programming/27581/overriding-equals-and-hashcode-in-java, aby uzyskać przykład prawidłowego wdrożenia porównania równości.

Sułtan
źródło
16
+1 - Niektórzy z nas uważają, ŻADNE użycie odbicia jest czerwoną flagą wskazującą zły projekt.
Ross Patterson
1
Najprawdopodobniej w tym przypadku pożądane jest rozwiązanie oparte na równości, ale jako ogólna koncepcja, co jest nie tak z rozwiązaniem odbicia? W rzeczywistości jest to bardzo ogólne i metody równości nie muszą być wyraźnie zapisane w każdej klasie i podklasie (chociaż można je łatwo wygenerować przy pomocy dobrego IDE).
m3th0dman
2
@RossPatterson Dlaczego?
m3th0dman
2
@ m3th0dman Ponieważ prowadzi to do rzeczy takich jak twoja compare()metoda, zakładając, że każda metoda zaczynająca się od „get” jest geterem i jest bezpieczna i odpowiednia do wywołania w ramach operacji porównania. Jest to naruszenie definicji interfejsu obiektu i chociaż może to być celowe, prawie zawsze jest błędne.
Ross Patterson
4
@ m3th0dman Narusza to enkapsulację - klasa podstawowa musi mieć dostęp do atrybutów w swoich podklasach. Co jeśli są prywatne (lub getter prywatne)? Co z polimorfizmem? Czy jeśli zdecyduję się dodać kolejną podklasę i chcę zrobić w niej inne porównanie? Cóż, klasa podstawowa już to robi dla mnie i nie mogę tego zmienić. Co jeśli pobierający leniwie coś ładują? Czy chcę to zrobić za pomocą metody porównawczej? Skąd mam wiedzieć, że metoda zaczynająca się getod gettera, a nie niestandardowa metoda zwraca coś?
Sulthan
1

Myślę, że masz tutaj dwa problemy.

  1. Ile powinienem mieć kod dynamiczny vs. statyczny?
  2. Jak wyrazić niestandardową wersję równości?

Kod dynamiczny a statyczny

To odwieczne pytanie, a odpowiedź jest bardzo pozytywna.

Z jednej strony twój kompilator jest bardzo dobry w wyłapywaniu wszelkiego rodzaju złego kodu. Dokonuje tego poprzez różne formy analizy, przy czym analiza typów jest powszechna. Wie, że nie można użyć Bananaobiektu w kodzie, który oczekujeCog . Mówi to przez błąd kompilacji.

Teraz może to zrobić tylko wtedy, gdy może wywnioskować zarówno zaakceptowany Typ, jak i dany Typ z kontekstu. Ile można wywnioskować i jak ogólne jest to wnioskowanie, w dużej mierze zależy od używanego języka. Java może wnioskować o typach informacji za pomocą mechanizmów takich jak dziedziczenie, interfejsy i ogólne. Przebieg jest różny, niektóre inne języki zapewniają mniej mechanizmów, a niektóre zapewniają więcej. Nadal sprowadza się do tego, co kompilator może wiedzieć, że jest prawdziwy.

Z drugiej strony twój kompilator nie jest w stanie przewidzieć kształtu obcego kodu, a czasem ogólny algorytm może być wyrażony na wielu typach, których nie można łatwo wyrazić za pomocą systemu typów języka. W takich przypadkach kompilator nie zawsze może znać wynik z góry i może nawet nie być w stanie wiedzieć, jakie pytanie zadać. Odbicie, interfejsy i klasa Object są sposobem Java radzenia sobie z tymi problemami. Będziesz musiał zapewnić prawidłowe kontrole i obsługę, ale nie jest niezdrowe mieć tego rodzaju kod.

Czy sprecyzować kod, czy też ogólnie, sprowadza się do problemu, który próbujesz rozwiązać. Jeśli możesz to łatwo wyrazić za pomocą systemu typów, zrób to. Pozwól kompilatorowi wykorzystać jego mocne strony i pomóc. Jeśli system typów prawdopodobnie nie wiedziałby z góry (kod obcy) lub system typów jest nieodpowiedni do ogólnej implementacji algorytmu, wówczas właściwym narzędziem jest odbicie (i inne dynamiczne środki).

Pamiętaj tylko, że wyjście poza system pisma twojego języka jest zaskakujące. Wyobraź sobie, że podchodzisz do przyjaciela i zaczynasz rozmowę po angielsku. Nagle dodaj kilka słów z hiszpańskiego, francuskiego i kantońskiego, które dokładnie wyrażają twoje myśli. Kontekst wiele powie twojemu przyjacielowi, ale może również nie wiedzieć, jak radzić sobie z tymi słowami, co prowadzi do różnego rodzaju nieporozumień. Lepiej radzi sobie z tymi nieporozumieniami niż wyjaśnianie tych pomysłów w języku angielskim przy użyciu większej liczby słów?

Niestandardowa równość

Rozumiem, że Java w dużej mierze opiera się na equals metodzie ogólnego porównywania dwóch obiektów, ale nie zawsze jest odpowiednia w danym kontekście.

Jest inny sposób, a także standard Java. Nazywa się to Komparator .

To, jak zaimplementujesz swój komparator, będzie zależeć od tego, co porównasz i od tego, jak.

  • Można go zastosować do dowolnych dwóch obiektów, niezależnie od ich konkretnej equalsimplementacji.
  • Może zaimplementować ogólną (opartą na odbiciach) metodę porównania do obsługi dowolnych dwóch obiektów.
  • Funkcje porównawcze płyty kotłowej można dodać do często porównywanych typów obiektów, zapewniając bezpieczeństwo typu i optymalizację.
Kain0_0
źródło
1

Wolę unikać programowania refleksyjnego, ponieważ jest to możliwe

  • utrudnia statyczne sprawdzenie kodu przez kompilator
  • sprawia, że ​​kod jest trudniejszy do uzasadnienia
  • utrudnia refaktoryzację kodu

Jest również znacznie mniej wydajny niż proste wywołania metod; kiedyś był wolniejszy o rząd wielkości lub więcej.

Kod kontroli statycznej

Każdy kod odblaskowy wyszukuje klasy i metody za pomocą ciągów; w oryginalnym przykładzie szuka dowolnej metody zaczynającej się od „get”; zwróci gettery, ale oprócz tego inne metody, takie jak „gettysburgAddress ()”. Reguły można zaostrzyć w kodzie, ale chodzi o to, że jest to kontrola czasu wykonywania ; IDE i kompilator nie mogą pomóc. Zasadniczo nie lubię kodu „ciągłego pisania” lub „prymitywnej obsesji”.

Trudniej jest uzasadnić

Kod odblaskowy jest bardziej szczegółowy niż proste wywołania metod. Więcej kodu = więcej błędów lub przynajmniej większy potencjał błędów, więcej kodu do czytania, testowania itp. Mniej to więcej.

Trudniej zrefaktoryzować

Ponieważ kod oparty jest na łańcuchach / dynamice; gdy tylko na scenie pojawi się refleksja, nie można refaktoryzować kodu ze 100% pewnością przy użyciu narzędzi refaktoryzacyjnych IDE, ponieważ IDE nie może rozpoznać zastosowań odblaskowych.

Zasadniczo unikaj refleksji w ogólnym kodzie, jeśli to w ogóle możliwe; szukaj ulepszonego projektu.

David Kerr
źródło
0

Nadużywanie czegokolwiek z definicji jest złe, prawda? Więc na razie pozbądźmy się (ponad).

Czy nazwałbyś niewiarygodnie ciężkie wewnętrzne użycie odbicia wiosny złym nawykiem?

Wiosenne oswajanie odbicia za pomocą adnotacji - podobnie Hibernacja (i prawdopodobnie dziesiątki / setki innych narzędzi).

Postępuj zgodnie z tymi wzorcami, jeśli używasz go we własnym kodzie. Użyj adnotacji, aby upewnić się, że IDE użytkownika nadal może im pomóc (nawet jeśli jesteś jedynym „użytkownikiem” swojego kodu, nieostrożne użycie refleksji prawdopodobnie wróci, by ostatecznie ugryźć cię w tyłek).

Jednak bez uwzględnienia sposobu, w jaki kod będzie wykorzystywany przez programistów, nawet najprostsze użycie refleksji jest prawdopodobnie nadmierne.

Bill K.
źródło
0

Myślę, że większość z tych odpowiedzi nie ma sensu.

  1. Tak, prawdopodobnie powinieneś napisać equals()i hashcode(), jak zauważył @KarlBielefeldt.

  2. Ale dla klasy z wieloma polami może to być nużąca płyta.

  3. To zależy .

    • Jeśli potrzebujesz tylko równości i kodu skrótu rzadko , pragmatyczne i prawdopodobnie w porządku jest użycie obliczenia odbicia ogólnego przeznaczenia. Przynajmniej jako szybkie i brudne pierwsze przejście.
    • ale jeśli potrzebujesz dużo, np. te obiekty zostaną umieszczone w HashTables, więc wydajność będzie problemem, zdecydowanie powinieneś napisać kod. będzie znacznie szybciej.
  4. Inna możliwość: jeśli twoje zajęcia naprawdę mają tyle pól, że pisanie równości jest uciążliwe, zastanów się

    • umieszczenie pól w mapie.
    • nadal możesz pisać niestandardowe programy pobierające i ustawiające
    • użyj Map.equals()do porównania. (lub Map.hashcode())

np. ( uwaga : ignorowanie czeków zerowych, prawdopodobnie powinno się używać Wyliczeń zamiast kluczy Łańcuchowych, wiele nie pokazano ...)

class TooManyFields {
  private HashMap<String, Object> map = new HashMap<String, Object>();

  public setFoo(int i) { map.put("Foo", Integer.valueOf(i)); }
  public int getFoo()  { return map.get("Foo").intValue(); }

  public setBar(Sttring s) { map.put("Bar", s); }
  public String getBar()  { return map.get("Bar").toString(); }

  ... more getters and setters ...

  public boolean equals(Object o) {
    return (o instanceof TooManyFields) &&
           this.map.equals( ((TooManyFields)o).map);
}
użytkownik949300
źródło