Melhor ter 2 métodos com significado claro ou apenas 1 método de uso duplo?

30

Para simplificar a interface, é melhor simplesmente não ter o getBalance()método? Passar 0para o charge(float c);dará o mesmo resultado:

public class Client {
    private float bal;
    float getBalance() { return bal; }
    float charge(float c) {
        bal -= c;
        return bal;
    }
}

Talvez faça uma anotação javadoc? Ou deixe o usuário da classe descobrir como obter o equilíbrio?

david
fonte
26
Estou assumindo generosamente que este é um exemplo, código ilustrativo e que você não está usando matemática de ponto flutuante para armazenar valores monetários. Caso contrário, eu teria que ficar todo pregado. :-)
corsiKa
11
@corsiKa nah. Isso é apenas um exemplo. Mas, sim, eu teria usado float para representar dinheiro em uma classe real ... Obrigado por me lembrar sobre os perigos dos números de ponto flutuante.
David #
10
Alguns idiomas distinguem entre mutadores e acessadores com bons resultados. Seria muito chato não conseguir o equilíbrio de uma instância que só é acessível como uma constante!
JDługosz 06/02

Respostas:

90

Você parece sugerir que a complexidade de uma interface é medida pelo número de elementos que ela possui (métodos, neste caso). Muitos argumentariam que ter que lembrar que o chargemétodo pode ser usado para retornar o equilíbrio de a Clientadiciona muito mais complexidade do que ter o elemento extra do getBalancemétodo. Tornar as coisas mais explícitas é muito mais simples, especialmente ao ponto em que não deixa ambiguidade, independentemente do maior número de elementos na interface.

Além disso, a chamada charge(0)viola o princípio de menos espanto , também conhecido como métrica WTFs por minuto (do Código Limpo, imagem abaixo), dificultando a entrada de novos membros da equipe (ou atuais, após algum tempo longe do código) até eles entendem que a chamada é realmente usada para obter o equilíbrio. Pense em como outros leitores reagiriam:

insira a descrição da imagem aqui

Além disso, a assinatura do chargemétodo contraria as diretrizes de fazer uma e apenas uma coisa e separar a consulta de comando , porque faz com que o objeto altere seu estado e também retorna um novo valor.

Em suma, acredito que a interface mais simples nesse caso seria:

public class Client {
  private float bal;
  float getBalance() { return bal; }
  void charge(float c) { bal -= c; }
}
MichelHenrich
fonte
25
O ponto sobre a separação de consulta de comando é extremamente importante, IMO. Imagine que você é um novo desenvolvedor neste projeto. Examinando o código, fica imediatamente claro que getBalance()retorna algum valor e não modifica nada. Por outro lado, charge(0)parece que provavelmente modifica algo ... talvez ele envie um evento PropertyChanged? E o que está retornando, o valor anterior ou o novo valor? Agora você deve procurar nos documentos e desperdiçar recursos intelectuais, o que poderia ter sido evitado usando um método mais claro.
Mage Xy
19
Além disso, usar charge(0)como a maneira de obter o equilíbrio, porque agora não tem efeito, ajuda você a esse detalhe da implementação; Posso facilmente imaginar um requisito futuro em que o rastreamento do número de cobranças se torna relevante; nesse momento, ter sua base de códigos repleta de cobranças que não são realmente cobranças se torna um problema. Você deve fornecer elementos de interface que significam as coisas que seus clientes precisam fazer, não aqueles que simplesmente fazem o que seus clientes precisam fazer.
Ben
12
A advertência do CQS não é necessariamente apropriada. Para um charge()método, se esse método for atômico em um sistema simultâneo, pode ser apropriado retornar o valor novo ou antigo.
Dietrich Epp 06/02
3
Eu achei suas duas citações para o CQRS extremamente convincentes. Por exemplo, eu geralmente gosto das idéias de Meyer, mas olhando para o tipo de retorno de uma função para saber se ela sofre alguma mutação é arbitrária e não-robusta. Muitas estruturas de dados concorrentes ou imutáveis ​​requerem mutação + retorno. Como você acrescentaria a uma String sem violar o CQRS? Você tem citações melhores?
user949300
6
Quase nenhum código está em conformidade com a Separação de Consulta de Comando. Não é idiomático em nenhuma linguagem orientada a objetos popular e é violado pelas APIs internas de todas as linguagens que já usei. Descrevê-lo como uma "melhor prática", portanto, parece bizarro; você pode nomear pelo menos um projeto de software que realmente siga esse padrão?
Mark Amery
28

IMO, substituindo getBalance()com charge(0)toda a sua aplicação não é uma simplificação. Sim, são menos linhas, mas ofusca o significado do charge()método, o que pode causar dores de cabeça na linha quando você ou outra pessoa precisa revisitar esse código.

Embora eles possam dar o mesmo resultado, obter o saldo de uma conta não é o mesmo que cobrar zero, portanto, provavelmente seria melhor separar suas preocupações. Por exemplo, se você precisar modificar o charge()log sempre que houver uma transação na conta, agora terá um problema e precisará separar a funcionalidade de qualquer maneira.

Matt Dalzell
fonte
13

É importante lembrar que seu código deve ser auto-documentado. Quando ligo charge(x), espero xser cobrado. As informações sobre o equilíbrio são secundárias. Além do mais, talvez eu não saiba como charge()é implementado quando o chamo e definitivamente não saberei como ele será implementado amanhã. Por exemplo, considere esta atualização futura em potencial para charge():

float charge(float c) {
    lockDownUserAccountUntilChargeClears();
    bal -= c;
    Unlock();
    return bal;
}

De repente, usar charge()para obter o equilíbrio não parece tão bom.

David diz Reinstate Monica
fonte
Sim! Um bom ponto que chargeé muito mais pesado.
Deduplicator
2

Usar charge(0);para obter o saldo é uma péssima ideia: um dia, alguém pode adicionar algum código para registrar as cobranças feitas sem perceber o outro uso da função e, toda vez que alguém obtiver o saldo, ela será registrada como cobrança. (Existem maneiras de contornar isso, como uma declaração condicional que diz algo como:

if (c > 0) {
    // make and log charge
}
return bal;

mas isso depende do programador saber implementá-los, o que ele não fará se não for imediatamente óbvio que eles são necessários.

Resumindo: não confie nos usuários ou nos sucessores de programadores que percebem que charge(0);é a maneira correta de obter o equilíbrio, porque, a menos que exista documentação que garantam que eles não perderão, francamente, essa será a maneira mais assustadora de obter o equilíbrio. possível.

Micheal Johnson
fonte
0

Sei que há muitas respostas, mas outro motivo charge(0)é que um simples erro de digitação charge(9)fará com que o saldo de seu cliente diminua toda vez que você desejar obter o equilíbrio. Se você tiver um bom teste de unidade, poderá mitigar esse risco, mas, se não for diligente em todas as ligações, chargepoderá ter esse problema.

Shaz
fonte
-2

Eu gostaria de mencionar um caso específico em que faria sentido ter menos métodos, mais polivalentes: se houver muito polimorfismo, ou seja, muitas implementações dessa interface ; especialmente se essas implementações estiverem em código desenvolvido separadamente e não puderem ser atualizados em sincronia (a interface é definida por uma biblioteca).

Nesse caso, simplificar o trabalho de escrever cada implementação é muito mais valioso do que a clareza de uso, porque a primeira evita erros de violação de contrato (os dois métodos são inconsistentes entre si), enquanto a última apenas prejudica a legibilidade, o que pode ser recuperado por uma função auxiliar ou método de superclasse que defina getBalanceem termos de charge.

(Esse é um padrão de design, para o qual não recordo um nome específico: definição de uma interface complexa e amigável para quem chama em termos de uma interface mínima para o implementador. No Classic Mac OS, a interface mínima para operações de desenho era chamada de "gargalo" mas esse parece não ser um termo popular.)

Se esse não for o caso (existem poucas implementações ou exatamente uma) charge(), faz sentido separar os métodos para maior clareza e permitir a simples adição de comportamentos relevantes a cobranças diferentes de zero .

Kevin Reid
fonte
11
De fato, tive casos em que a interface mais mínima foi escolhida para facilitar o teste de cobertura completa. Mas isso não é necessariamente exposto ao usuário da classe. Por exemplo , inserir , acrescentar e excluir podem ser simples invólucros de uma substituição geral que possui todos os caminhos de controle bem estudados e testados.
JDługosz 06/02
Eu já vi essa API. O alocador Lua 5.1+ usa-o. Você fornece uma função e, com base nos parâmetros que ela passa, decide se está tentando alocar nova memória, desalocar memória ou realocar memória da memória antiga. É doloroso escrever essas funções. Eu preferiria que Lua tivesse lhe dado duas funções, uma para alocação e outra para desalocação.
Nicol Bolas 07/02
@NicolBolas: E nenhum para realocação? De qualquer forma, esse tem o "benefício" adicional de quase ser o mesmo que realloc, às vezes, em alguns sistemas.
Deduplicator
@ Reduplicador: alocação vs. realocação é ... OK. Não é ótimo colocá-los na mesma função, mas não é tão ruim quanto colocar a desalocação na mesma. Também é uma distinção fácil de fazer.
Nicol Bolas
Eu diria que a desalocação conflitante com a (re) alocação é um exemplo especialmente ruim desse tipo de interface. (Desalocação tem um contrato muito diferente: ele não resultar em um ponteiro válido.)
Kevin Reid