Uma interface com apenas getters é um cheiro de código?

10

(Eu já vi essa pergunta , mas a primeira resposta é mais sobre propriedades automáticas do que sobre design, e a segunda diz ocultar o código de armazenamento de dados do consumidor , o que não tenho certeza é o que eu quero / meu código, então eu gostaria de ouvir outra opinião)

Eu tenho duas entidades muito semelhantes HolidayDiscounte RentalDiscount, que representam descontos de comprimento como 'se durar pelo menos numberOfDays, um percentdesconto é aplicável'. As tabelas possuem fks para diferentes entidades pai e são usadas em locais diferentes, mas onde são usadas, existe uma lógica comum para obter o desconto máximo aplicável. Por exemplo, a HolidayOfferpossui vários HolidayDiscountse, ao calcular seu custo, precisamos descobrir o desconto aplicável. Mesmo para aluguel e RentalDiscounts.

Como a lógica é a mesma, quero mantê-la em um único local. É isso que o seguinte método, predicado e comparador fazem:

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

Como as únicas informações necessárias são o número de dias, termino com uma interface como

public interface LengthDiscount {

    Integer getNumberOfDays();
}

E as duas entidades

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

A interface possui um método getter único que as duas entidades implementam, o que obviamente funciona, mas duvido que seja um bom design. Não representa nenhum comportamento, dado que manter um valor não é uma propriedade. Este é um caso bastante simples, tenho mais alguns casos semelhantes e mais complexos (com 3-4 getters).

Minha pergunta é: esse é um design ruim? Qual é uma abordagem melhor?

user3748908
fonte
4
O objetivo de uma interface é estabelecer um padrão de conexão, não representar comportamento. Esse dever recai sobre os métodos na implementação.
Robert Harvey
Quanto à sua implementação, há uma enorme quantidade de clichê (código que realmente não faz nada, além de fornecer estrutura). Tem certeza de que precisa de tudo isso?
Robert Harvey
Métodos Mesmo de interface são apenas 'getters' não significa que você não pode 'setters' implementa sobre a implementação (se todos têm setter, s você ainda pode usar um resumo)
рüффп

Respostas:

5

Eu diria que seu design é um pouco equivocado.

Uma solução potencial seria criar uma interface chamada IDiscountCalculator.

decimal CalculateDiscount();

Agora qualquer classe que precise fornecer um desconto implementaria essa interface. Se o desconto usar o número de dias, a interface não se importa, pois são os detalhes da implementação.

O número de dias provavelmente pertence a algum tipo de classe base abstrata se todas as classes de desconto precisarem desse membro de dados. Se for específico da classe, basta declarar uma propriedade que possa ser acessada publicamente ou em particular, dependendo da necessidade.

Jon Raynor
fonte
11
Não tenho certeza se concordo totalmente em ter HolidayDiscounte RentalDiscountimplementar IDiscountCalculator, porque não são calculadoras de desconto. O cálculo é feito nesse outro método getMaxApplicableLengthDiscount. HolidayDiscounte RentalDiscountsão descontos. Um desconto não é calculado, é um tipo de desconto aplicável calculado ou apenas selecionado entre vários descontos.
precisa saber é o seguinte
11
Talvez a interface deva se chamar IDiscountable. Qualquer classe que precise implementar pode. Se as classes de desconto para férias / aluguel são apenas DTO / POCO e armazena o resultado em vez de calcular que está correto.
21416 Jon Jonnor
3

Para responder à sua pergunta: você não pode concluir um cheiro de código se uma interface tiver apenas getters.

Um exemplo de métrica difusa para odores de código são interfaces inchadas com muitos métodos (não se getters ou não). Eles tendem a violar o princípio de segmentação de interface.

No nível semântico, o princípio da responsabilidade única também não deve ser violado. Costumo ser violado se você ver vários problemas diferentes tratados no contrato de interface que não são um assunto. Às vezes, isso é difícil de identificar e você precisa de alguma experiência para identificá-lo.

Por outro lado, você deve estar ciente se sua interface define setters. Isso ocorre porque os setters provavelmente mudarão de estado, esse é o trabalho deles. A pergunta a ser feita aqui: O objeto por trás da interface faz uma transição de um estado válido para outro estado válido. Mas isso não é principalmente um problema da interface, mas a implementação do contrato de interface do objeto de implementação.

A única preocupação que tenho com sua abordagem é que você opera em mapeamentos OR. Nesse pequeno caso, isso não é um problema. Tecnicamente está tudo bem. Mas eu não operaria em objetos mapeados em OR. Eles podem ter muitos estados diferentes que você certamente não considera nas operações realizadas neles ...

Os objetos mapeados em OR podem ser (pressupondo JPA) transitórios, desanexados persistentes, inalterados, anexados persistentes inalterados, alterados persistentes desanexados, alterados persistentes alterados ... Afinal, você pode identificar mais de 10 estados diferentes que o objeto OR-Mapped pode ter. Todas as suas operações lidam com esses estados corretamente?

Certamente isso não é problema se sua interface define o contrato e a implementação o segue. Mas, para objetos mapeados em OR, tenho uma dúvida razoável de que o contrato pode ser cumprido.

oopexpert
fonte