(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 HolidayDiscount
e RentalDiscount
, que representam descontos de comprimento como 'se durar pelo menos numberOfDays
, um percent
desconto é 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 HolidayOffer
possui vários HolidayDiscounts
e, 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?
fonte
Respostas:
Eu diria que seu design é um pouco equivocado.
Uma solução potencial seria criar uma interface chamada
IDiscountCalculator
.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.
fonte
HolidayDiscount
eRentalDiscount
implementarIDiscountCalculator
, porque não são calculadoras de desconto. O cálculo é feito nesse outro métodogetMaxApplicableLengthDiscount
.HolidayDiscount
eRentalDiscount
são descontos. Um desconto não é calculado, é um tipo de desconto aplicável calculado ou apenas selecionado entre vários descontos.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.
fonte