Eu 'descobri' interfaces e comecei a amá-las. A beleza de uma interface é que ela é um contrato, e qualquer objeto que cumpra esse contrato pode ser usado sempre que a interface for necessária.
O problema com uma interface é que ela não pode ter uma implementação padrão, o que é um problema para propriedades comuns e derrota o DRY. Isso também é bom, porque mantém a implementação e o sistema dissociados. A herança, por outro lado, mantém um acoplamento mais rígido e tem o potencial de quebrar o encapsulamento.
Caso 1 (herança com membros privados, bom encapsulamento, fortemente acoplado)
class Employee
{
int money_earned;
string name;
public:
void do_work(){money_earned++;};
string get_name(return name;);
};
class Nurse : public Employee:
{
public:
void do_work(/*do work. Oops, can't update money_earned. Unaware I have to call superclass' do_work()*/);
};
void HireNurse(Nurse *n)
{
nurse->do_work();
)
Caso 2 (apenas uma interface)
class IEmployee
{
virtual void do_work()=0;
virtual string get_name()=0;
};
//class Nurse implements IEmployee.
//But now, for each employee, must repeat the get_name() implementation,
//and add a name member string, which breaks DRY.
Caso 3: (melhor dos dois mundos?)
Semelhante ao Caso 1 . No entanto, imagine que (hipoteticamente) o C ++ não permitisse métodos de substituição, exceto aqueles que são puramente virtuais .
Portanto, no Caso 1 , substituir do_work () causaria um erro em tempo de compilação. Para corrigir isso, configuramos do_work () como virtual puro e adicionamos um método separado increment_money_earned (). Como um exemplo:
class Employee
{
int money_earned;
string name;
public:
virtual void do_work()=0;
void increment_money_earned(money_earned++;);
string get_name(return name;);
};
class Nurse : public Employee:
{
public:
void do_work(/*do work*/ increment_money_earned(); ); .
};
Mas mesmo isso tem problemas. E se daqui a três meses, Joe Coder criar um Médico Funcionário, mas ele esquecer de chamar increment_money_earned () em do_work ()?
A questão:
O caso 3 é superior ao caso 1 ? É por causa de 'melhor encapsulamento' ou 'mais frouxamente acoplado', ou algum outro motivo?
O Caso 3 é superior ao Caso 2 porque está em conformidade com o DRY?
fonte
Respostas:
Uma maneira de resolver o problema de esquecer a chamada da superclasse é devolver o controle à superclasse! Repensei seu primeiro exemplo para mostrar como (e o fiz compilar;)). Oh, eu também assumir que
do_work()
noEmployee
deveria estarvirtual
em seu primeiro exemplo.Agora
do_work()
não pode ser substituído. Se você quiser estendê-lo, precisará fazê-lo através doon_do_work()
qualdo_work()
tenha controle.Isso, é claro, também pode ser usado com a interface do seu segundo exemplo, se a
Employee
estender. Então, se eu entendi direito, acho que é o caso 3, mas sem ter que usar C ++ hipotético! É SECO e tem um forte encapsulamento.fonte
Na minha opinião, as interfaces devem ter apenas métodos puros - sem uma implementação padrão. Ele não quebra o princípio DRY de nenhuma maneira, porque as interfaces mostram como acessar alguma entidade. Apenas para referências, estou analisando a explicação DRY aqui :
"Todo conhecimento deve ter uma representação única, inequívoca e autorizada dentro de um sistema".
Por outro lado, o SOLID informa que todas as classes devem ter uma interface.
Não, o caso 3 não é superior ao caso 1. Você precisa se decidir. Se você deseja ter uma implementação padrão, faça-o. Se você quer um método puro, vá em frente.
Joe Coder deve obter o que merece por ignorar os testes de unidade que falharam. Ele testou essa classe, não foi? :)
Um tamanho não serve para todos. É impossível dizer qual é o melhor. Existem alguns casos em que um se encaixaria melhor que o outro.
Talvez você deva aprender alguns padrões de design em vez de tentar inventar alguns dos seus.
Acabei de perceber que você está procurando um padrão de design de interface não virtual , porque é assim que a sua classe de caso 3 se parece.
fonte
As interfaces podem ter implementações padrão em C ++. Não há nada dizendo que uma implementação padrão de uma função não dependa apenas de outros membros virtuais (e argumentos), portanto, não aumenta nenhum tipo de acoplamento.
No caso 2, DRY está substituindo aqui. Existe um encapsulamento para proteger seu programa de alterações, de diferentes implementações, mas nesse caso, você não tem implementações diferentes. Então encapsulamento YAGNI.
De fato, as interfaces de tempo de execução são geralmente consideradas inferiores aos seus equivalentes em tempo de compilação. No caso de tempo de compilação, você pode ter os casos 1 e 2 no mesmo pacote, sem mencionar as inúmeras outras vantagens. Ou mesmo em tempo de execução, você pode simplesmente fazer
Employee : public IEmployee
efetivamente a mesma vantagem. Existem inúmeras maneiras de lidar com essas coisas.Eu parei de ler. YAGNI. C ++ é o que C ++ é, e o comitê de Padrões nunca, jamais, implementará essa alteração, por excelentes razões.
fonte
get_name
. Todas as suas implementações propostas compartilhariam a mesma implementação deget_name
. Além disso, como eu disse, não há razão para escolher, você pode ter os dois. Além disso, o Caso 3 é totalmente inútil. Você pode substituir os virtuais não puros, então esqueça um design em que não pode.Pelo que vejo na sua implementação, sua implementação do Caso 3 requer uma classe abstrata que pode implementar métodos virtuais puros que podem ser alterados posteriormente na classe derivada. O caso 3 seria melhor, pois a classe derivada pode alterar a implementação de do_work como e quando necessário e todas as instâncias derivadas basicamente pertenceriam ao tipo abstrato base.
Eu diria que depende apenas do seu design de implementação e do objetivo que você deseja alcançar. A classe abstrata e as interfaces são implementadas com base no problema que precisa ser resolvido.
Editar na pergunta
Testes de unidade podem ser realizados para verificar se cada classe confirma o comportamento esperado. Portanto, se testes de unidade adequados forem aplicados, os erros poderão ser evitados quando Joe Coder implementar a nova classe.
fonte
O uso de interfaces interrompe o DRY apenas se cada implementação for uma duplicata. Você pode resolver esse dilema aplicando a interface e a herança. No entanto, há alguns casos em que você pode implementar a mesma interface em várias classes, mas varia o comportamento de cada uma das classes, e isso ainda mantém o princípio de SECO. Se você optar por usar qualquer uma das três abordagens descritas, se resume às escolhas que você precisa fazer para aplicar a melhor técnica para corresponder a uma determinada situação. Por outro lado, você provavelmente descobrirá que, com o tempo, usa mais as interfaces e aplica a herança apenas onde deseja remover a repetição. Isso não quer dizer que este seja o único razão da herança, mas é melhor minimizar o uso da herança para permitir que você mantenha suas opções em aberto se achar que seu design precisa mudar mais tarde e se deseja minimizar o impacto nas classes descendentes dos efeitos que uma mudança introduziria em uma classe pai.
fonte