Rozważmy sytuację, w której klasa implementuje to samo podstawowe zachowanie, metody itd., Ale istnieje wiele różnych wersji tej klasy do różnych zastosowań. W moim szczególnym przypadku mam wektor (wektor geometryczny, a nie listę) i ten wektor może mieć zastosowanie do dowolnej N-wymiarowej przestrzeni euklidesowej (1 wymiar, 2 wymiar, ...). Jak można zdefiniować tę klasę / typ?
Byłoby to łatwe w C ++, gdzie szablony klas mogą mieć rzeczywiste wartości jako parametry, ale nie mamy tego luksusu w Javie.
Dwa podejścia, które mogę wymyślić, które można zastosować w celu rozwiązania tego problemu, to:
Posiadanie implementacji każdego możliwego przypadku w czasie kompilacji.
public interface Vector { public double magnitude(); } public class Vector1 implements Vector { public final double x; public Vector1(double x) { this.x = x; } @Override public double magnitude() { return x; } public double getX() { return x; } } public class Vector2 implements Vector { public final double x, y; public Vector2(double x, double y) { this.x = x; this.y = y; } @Override public double magnitude() { return Math.sqrt(x * x + y * y); } public double getX() { return x; } public double getY() { return y; } }
To rozwiązanie jest oczywiście bardzo czasochłonne i niezwykle uciążliwe dla kodu. W tym przykładzie nie wydaje się to takie złe, ale w moim rzeczywistym kodzie mam do czynienia z wektorami, które mają wiele implementacji, z maksymalnie czterema wymiarami (x, y, z i w). Obecnie mam ponad 2000 wierszy kodu, chociaż każdy wektor naprawdę potrzebuje tylko 500.
Określanie parametrów w czasie wykonywania.
public class Vector { private final double[] components; public Vector(double[] components) { this.components = components; } public int dimensions() { return components.length; } public double magnitude() { double sum = 0; for (double component : components) { sum += component * component; } return Math.sqrt(sum); } public double getComponent(int index) { return components[index]; } }
Niestety to rozwiązanie szkodzi wydajności kodu, powoduje powstanie bałaganu w porównaniu z poprzednim rozwiązaniem i nie jest tak bezpieczne w czasie kompilacji (nie można zagwarantować w czasie kompilacji, że wektor, z którym mamy do czynienia, jest w rzeczywistości dwuwymiarowy, na przykład).
Obecnie rozwijam się w Xtend, więc jeśli jakiekolwiek rozwiązania Xtend będą dostępne, będą one również do przyjęcia.
źródło
Odpowiedzi:
W takich przypadkach używam generowania kodu.
Piszę aplikację Java, która generuje rzeczywisty kod. W ten sposób możesz łatwo użyć pętli for, aby wygenerować kilka różnych wersji. Używam JavaPoet , co sprawia, że budowanie właściwego kodu jest dość proste. Następnie możesz zintegrować generowanie kodu z systemem kompilacji.
źródło
Mam bardzo podobny model w mojej aplikacji, a naszym rozwiązaniem było po prostu utrzymanie mapy o dynamicznym rozmiarze, podobnej do twojego rozwiązania 2.
Po prostu nie będziesz musiał się martwić wydajnością z takim rodzajem prymitywu java. Generujemy macierze o górnych granicach 100 kolumn (czytaj: 100 wektorów wymiarowych) na 10 000 wierszy i osiągnęliśmy dobrą wydajność przy znacznie bardziej złożonych typach wektorów niż Twoje rozwiązanie 2. Możesz spróbować uszczelnić klasę lub metody znakowania jako ostateczne aby przyspieszyć, ale myślę, że optymalizujesz przedwcześnie.
Możesz uzyskać oszczędności kodu (kosztem wydajności), tworząc klasę podstawową do udostępniania kodu:
Oczywiście, jeśli korzystasz z Java-8 +, możesz użyć domyślnych interfejsów, aby uczynić to jeszcze bardziej ścisłym:
Poza tym JVM nie ma możliwości. Możesz oczywiście napisać je w C ++ i użyć czegoś takiego jak JNA, aby je zmostkować - to nasze rozwiązanie dla niektórych szybkich operacji matrycowych, w których używamy MKL fortran i Intel - ale spowolni to tylko, jeśli po prostu piszesz swoją matrycę w C ++ i wywołujesz jej moduły pobierające / ustawiające z java.
źródło
data class
obiektów, aby łatwo stworzyć 10 podklas wektorowych. W Javie, zakładając, że możesz przenieść całą swoją funkcjonalność do klasy podstawowej, każda podklasa zajmie 1-10 linii. Dlaczego nie stworzyć klasy podstawowej?asArray
metody, te różne metody nie byłyby sprawdzane w czasie kompilacji (możesz wykonać iloczyn skalarny między skalarem a wektorem kartezjańskim i kompilowałby się dobrze, ale nie działałby w czasie wykonywania) .Rozważmy wyliczenie z każdym nazwanym Vectorem posiadającym konstruktor, który składa się z tablicy (zainicjowanej na liście parametrów z nazwami wymiarów lub podobnymi, a może po prostu liczbą całkowitą dla rozmiaru lub pustą tablicą komponentów - twoim projektem) i lambda dla metoda getMagnitude. Możesz mieć enum także zaimplementować interfejs dla setComponents / getComponent (s) i po prostu ustalić, który składnik był w użyciu, eliminując getX, i in. Przed użyciem należy zainicjować każdy obiekt z jego rzeczywistymi wartościami składników, prawdopodobnie sprawdzając, czy rozmiar tablicy wejściowej odpowiada nazwom lub rozmiarom wymiarów.
Następnie, jeśli rozszerzysz rozwiązanie na inny wymiar, po prostu zmodyfikujesz wyliczenie i lambda.
źródło
Opierając się na opcji 2, dlaczego po prostu tego nie zrobić? Jeśli chcesz uniemożliwić korzystanie z surowej bazy, możesz uczynić ją abstrakcyjną:
źródło
double[]
jest niepożądany w porównaniu z implementacją, która po prostu używa 2 prymitywówdouble
. W tak minimalnym przykładzie wydaje się to mikrooptymalizacją, ale rozważmy znacznie bardziej złożony przypadek, w którym zaangażowanych jest znacznie więcej metadanych, a dany typ ma krótki okres użytkowania.Vector
bardziej wyspecjalizowanym (np.Vector3
), Gdyby jego żywotność miała być stosunkowo długa.Jeden pomysł:
Zapewnia to dobrą wydajność w typowych przypadkach i pewne bezpieczeństwo podczas kompilacji (wciąż można je poprawić) bez poświęcania ogólnego przypadku.
Szkielet kodu:
źródło