(Widziałem to pytanie , ale pierwsza odpowiedź dotyczy bardziej właściwości automatycznych niż projektowania, a druga mówi, że ukryj kod przechowywania danych przed konsumentem , czego nie jestem pewien, czy tego chcę / mój kod robi, więc chciałbym usłyszeć inną opinię)
Mam dwóch bardzo podobnych podmiotów, HolidayDiscount
oraz RentalDiscount
, że stanowią rabaty długość jak „jeśli to trwa co najmniej numberOfDays
, o percent
zniżka ma zastosowanie”. Tabele mają fks do różnych podmiotów nadrzędnych i są używane w różnych miejscach, ale tam, gdzie są używane, istnieje wspólna logika, aby uzyskać maksymalną obowiązującą zniżkę. Na przykład a HolidayOffer
ma pewną liczbę HolidayDiscounts
i przy obliczaniu jej kosztu musimy obliczyć odpowiednią zniżkę. To samo dotyczy wynajmu i RentalDiscounts
.
Ponieważ logika jest taka sama, chcę ją zachować w jednym miejscu. To właśnie robią następująca metoda, predykat i komparator:
Optional<LengthDiscount> getMaxApplicableLengthDiscount(List<LengthDiscount> discounts, int daysToStay) {
if (discounts.isEmpty()) {
return Optional.empty();
}
return discounts.stream()
.filter(new DiscountIsApplicablePredicate(daysToStay))
.max(new DiscountMinDaysComparator());
}
public class DiscountIsApplicablePredicate implements Predicate<LengthDiscount> {
private final long daysToStay;
public DiscountIsApplicablePredicate(long daysToStay) {
this.daysToStay = daysToStay;
}
@Override
public boolean test(LengthDiscount discount) {
return daysToStay >= discount.getNumberOfDays();
}
}
public class DiscountMinDaysComparator implements Comparator<LengthDiscount> {
@Override
public int compare(LengthDiscount d1, LengthDiscount d2) {
return d1.getNumberOfDays().compareTo(d2.getNumberOfDays());
}
}
Ponieważ jedyną potrzebną informacją jest liczba dni, kończę na interfejsie jako
public interface LengthDiscount {
Integer getNumberOfDays();
}
I dwa byty
@Entity
@Table(name = "holidayDiscounts")
@Setter
public class HolidayDiscount implements LengthDiscount {
private BigInteger percent;
private Integer numberOfDays;
public BigInteger getPercent() {
return percent;
}
@Override
public Integer getNumberOfDays() {
return numberOfDays;
}
}
@Entity
@Table(name = "rentalDiscounts")
@Setter
public class RentalDiscount implements LengthDiscount {
private BigInteger percent;
private Integer numberOfDays;
public BigInteger getPercent() {
return percent;
}
@Override
public Integer getNumberOfDays() {
return numberOfDays;
}
}
Interfejs ma metodę pojedynczego gettera, którą implementują dwa byty, co oczywiście działa, ale wątpię, czy to dobry projekt. Nie reprezentuje żadnego zachowania, ponieważ utrzymanie wartości nie jest własnością. To dość prosty przypadek, mam kilka podobnych, bardziej złożonych przypadków (z 3-4 pobierającymi).
Moje pytanie brzmi: czy to zły projekt? Jakie jest lepsze podejście?
źródło
Odpowiedzi:
Powiedziałbym, że twój projekt jest trochę mylny.
Jednym potencjalnym rozwiązaniem byłoby stworzenie interfejsu o nazwie
IDiscountCalculator
.Teraz każda klasa, która musi zapewnić zniżkę, wdroży ten interfejs. Jeśli zniżka wykorzystuje liczbę dni, niech tak będzie, interfejs tak naprawdę nie obchodzi, ponieważ są to szczegóły implementacji.
Liczba dni prawdopodobnie należy do pewnego rodzaju abstrakcyjnej klasy bazowej, jeśli wszystkie klasy zniżek potrzebują tego elementu danych. Jeśli jest to specyficzne dla klasy, po prostu zadeklaruj właściwość, do której można uzyskać dostęp publiczny lub prywatny w zależności od potrzeby.
źródło
HolidayDiscount
iRentalDiscount
wdrażaniemIDiscountCalculator
, ponieważ nie są to kalkulatory rabatów. Obliczenia wykonuje się w tej drugiej metodziegetMaxApplicableLengthDiscount
.HolidayDiscount
iRentalDiscount
są rabatami. Rabat nie jest obliczany, jest rodzajem stosownego rabatu, który jest obliczany, lub po prostu wybrany spośród wielu rabatów.Aby odpowiedzieć na twoje pytanie: nie możesz wywnioskować zapachu kodu, jeśli interfejs ma tylko moduły pobierające.
Przykładem rozmytej metryki zapachów kodu są rozlane interfejsy z wieloma metodami (nie robi to, jeśli gettery lub nie). Zwykle naruszają zasadę segregacji interfejsów.
Na poziomie semantycznym nie należy również naruszać zasady pojedynczej odpowiedzialności. Mam tendencję do naruszania, jeśli widzisz kilka różnych problemów rozwiązanych w umowie interfejsu, które nie są jednym przedmiotem. Czasami jest to trudne do zidentyfikowania i potrzebujesz trochę doświadczenia, aby je zidentyfikować.
Z drugiej strony powinieneś wiedzieć, czy twój interfejs definiuje setery. To dlatego, że setery mogą zmienić stan, to jest ich praca. Pytanie, które należy zadać tutaj: Czy obiekt za interfejsem dokonuje przejścia ze stanu poprawnego do innego prawidłowego. Ale to nie jest głównie problem interfejsu, ale implementacja umowy interfejsu obiektu implementującego.
Jedyne moje obawy związane z twoim podejściem to to, że operujesz na mapowaniach OR. W tym małym przypadku nie stanowi to problemu. Technicznie jest w porządku. Ale nie działałbym na obiektach z mapowaniem OR. Mogą mieć zbyt wiele różnych stanów, których z pewnością nie bierzesz pod uwagę podczas wykonywanych na nich operacji ...
Obiekty odwzorowane na OR mogą być przejściowe (założono JPA), trwałe odłączone niezmienione, trwałe dołączone niezmienione, trwałe odłączone zmienione, trwałe dołączone zmienione ... jeśli używasz sprawdzania poprawności komponentu bean na tych obiektach, nie będziesz mógł zobaczyć, czy obiekt został sprawdzony, czy nie. W końcu możesz zidentyfikować więcej niż 10 różnych stanów, które może mieć obiekt OR-Mapped. Czy wszystkie operacje poprawnie obsługują te stany?
Z pewnością nie stanowi to problemu, jeśli interfejs definiuje umowę, a implementacja następuje po niej. Ale w przypadku obiektów odwzorowanych na OR mam uzasadnione wątpliwości, czy uda mi się wypełnić taki kontrakt.
źródło