Fala André, tudo bem ?
Bom, a gente pode levar esse pensamento mais pro lado do conceito do negócio do que pro conceito técnico (que traz a vontade de reutilizar o código).
Pensando no conceito, a transferência é pensada inicialmente realmente como uma operação constituída (de forma integral) por duas sub-operações que são saque e depósito (com suas regras originais garantidas). A partir desse momento é que se deve concluir (do ponto de vista técnico) que a melhor prática de implementação é reaproveitar os códigos invocando-os dentro do transfere.
class Conta {
private double saldo;
public boolean saca(double valor) {
if(this.saldo >= valor) {
this.saldo -= valor;
return true;
}
return false;
}
public void deposita(double valor) {
this.saldo += valor;
}
public boolean transferePara(Conta destino, double valor) {
if( this.saca(valor) ) {
destino.deposita(valor);
return true;
}
return false;
}
}
class ContaCorrente extends Conta {
@Override
public boolean saca(double valor) {
return super.saca(valor+0.2);
}
}
Essa prática, além de deixar as coisas bem alinhadas conceitualmente, vem na verdade resolver um possível problema de repetição de código (e não necessariamente de muitos métodos acessarem o mesmo atributo diretamente).
Se não aplicássemos a técnica teríamos o seguinte possível trecho de implementação..
public boolean saca(double valor) {
if(this.saldo >= valor) {
this.saldo -= valor;
return true;
}
return false;
}
public boolean transferePara(Conta destino, double valor) {
if(this.saldo >= valor) {
this.saldo -= valor;
destino.deposita(valor); // ou mesmo destino.saldo += valor =/
return true;
}
return false;
}
O real problema é a repetição do código. Se consideramos conceitualmente a tranferência como uma saque + deposito toda vez que o código do saque mudar o código do transfere fica desatualizado, e precisaremos dar manutenção em dois pontos.
Agora uma coisa é importante discutirmos. Quando começamos a mudar o pensamento conceitual dizendo que a taxa deveria ser cobrada apenas para o saque e não para a transferência estamos chegando a conclusão que a regra de negócio da transferência não é mais satisfeita por um saque e depois um deposito. Portanto, não faz sentido fazer mais o código do transfere chamar o saca. Se a ideia é geral pra todas as contas, se aplicaria a alteração nas implementações da classe mais generica Conta, já se a ideia é aplicar esse cenário apenas para a ContaCorrente as alterações poderiam vir na própria ContaCorrente através de mais um override dessa vez para o transfere.
class ContaCorrente extends Conta {
@Override
public boolean saca(double valor) {
return super.saca(valor + 0.2);
}
// não chama o super pois se ele for chamado o saca vai ser usado cobrando taxa dessa operação
// manipula o atributo de acordo com sua própria regra de negócio que (nesse novo pensamento) não tem mais relação com a regra do saque
@Override
public boolean transferePara(Conta destino, double valor) {
if(this.saldo >= valor) {
this.saldo -= valor;
destino.deposita(valor);
return true;
}
return false;
}
Dessa forma a ideia seria justamente o transfere ter todo o código necessário para garantir sua responsabilidade. Não existe problema em mais de um método manipular o mesmo atributo, nenhuma regra está sendo afetada ao fazermos isso. Por exemplo, tanto saca
como deposita
manipulam o saldo
diretamente, e não há nenhum problema com isso, muito pelo contrário, alguns métodos no geral vão manipular o(s) mesmo(s) atributos de uma classe. O problema é deixá-los descrevendo as mesmas tarefas sobre esse atributo e tendo repetição como citado antes.
Ainda é discutível se não poderíamos tentar extrair a pequena repetição gerada na implementação do override do transferePara
(if verificando se pode fazer o saque, com a condição => se a condição mudar, temos que mudar no saca da Conta e no transfere da conta corrente). Poderíamos ter um método que isola essa responsabilidade com os métodos saca e transfere reaproveitando quando for necessário. Algo como um conta.podeSacar(valor)
(ou algum outro nome que descreva bem a responsabilidade).
Aí o saca do Conta faz:
public boolean saca(double valor) {
if (this.podeSacar(valor)) {
this.saldo -= valor;
return true;
}
return false;
}
e o transfere especifico da ContaCorrente:
@Override
public boolean transferePara(Conta destino, double valor) {
if(this.podeSacar(valor)) {
this.saldo -= valor;
destino.deposita(valor);
return true;
}
return false;
}
Aqui vai ser inevitável essa similaridade.
PS: Deve-se usar exceptions ao invés de retorno pra controlar pontos de falha'
Espero ter ajudado no pensamento. Abraço!