Czy interfejs tylko pobierający ma zapach kodu?

10

(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, HolidayDiscountoraz RentalDiscount, że stanowią rabaty długość jak „jeśli to trwa co najmniej numberOfDays, o percentzniż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 HolidayOfferma pewną liczbę HolidayDiscountsi 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?

użytkownik3748908
źródło
4
Interfejs ma na celu ustanowienie wzorca połączenia, a nie reprezentowanie zachowania. Obowiązek ten należy do metod wdrażania.
Robert Harvey
Jeśli chodzi o twoją implementację, jest strasznie dużo bojlerów (kod, który tak naprawdę nic nie robi, poza strukturą). Czy na pewno potrzebujesz tego wszystkiego?
Robert Harvey
Nawet metody interfejsu są tylko „getterami”, nie oznacza to, że nie można zaimplementować „setterów” w implementacji (jeśli wszystkie mają setter, możesz nadal używać abstraktu)
рüффп

Odpowiedzi:

5

Powiedziałbym, że twój projekt jest trochę mylny.

Jednym potencjalnym rozwiązaniem byłoby stworzenie interfejsu o nazwie IDiscountCalculator.

decimal CalculateDiscount();

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.

Jon Raynor
źródło
1
Nie jestem pewien, czy całkowicie się zgadzam z posiadaniem HolidayDiscounti RentalDiscountwdrażaniem IDiscountCalculator, ponieważ nie są to kalkulatory rabatów. Obliczenia wykonuje się w tej drugiej metodzie getMaxApplicableLengthDiscount. HolidayDiscounti RentalDiscountsą rabatami. Rabat nie jest obliczany, jest rodzajem stosownego rabatu, który jest obliczany, lub po prostu wybrany spośród wielu rabatów.
user3748908
1
Może interfejs powinien się nazywać IDiscountable. Cokolwiek klasy muszą zaimplementować, może. Jeśli klasy zniżek na wakacje / wynajem są tylko DTO / POCO i przechowują wynik, a nie obliczają, to jest w porządku.
Jon Raynor
3

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.

oopexpert
źródło