Interface e herança: o melhor dos dois mundos?

10

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?

MustafaM
fonte
2
... você está reinventando classes abstratas ou o quê?
21412 ZJR

Respostas:

10

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()no Employeedeveria estar virtualem seu primeiro exemplo.

#include <string>

using namespace std;

class Employee
{
    int money_earned;
    string name;
    virtual void on_do_work() {}

    public:
        void do_work() { money_earned++; on_do_work(); }
        string get_name() { return name; }
};

class Nurse : public Employee
{
    void on_do_work() { /* do more work. Oh, and I don't have to call do_work()! */ }
};

void HireNurse(Nurse* nurse)
{
    nurse->do_work();
}

Agora do_work()não pode ser substituído. Se você quiser estendê-lo, precisará fazê-lo através do on_do_work()qual do_work()tenha controle.

Isso, é claro, também pode ser usado com a interface do seu segundo exemplo, se a Employeeestender. 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.

Gyan tcp Gary Buyn
fonte
3
E esse é o padrão de design conhecido como "método de modelo" ( en.wikipedia.org/wiki/Template_method_pattern ).
Joris Timmermans
Sim, isso é compatível com o caso 3. Isso parece promissor. Examinará em detalhes. Além disso, este é algum tipo de sistema de eventos. Existe um nome para esse 'padrão'?
18712 MustafaM
@MadKeithV tem certeza de que este é o 'método de modelo'?
MustafaM
@illmath - sim, é um método público não virtual que delega partes de seus detalhes de implementação a métodos privados / protegidos virtuais.
Joris Timmermans
@illmath Eu não tinha pensado nisso como um método de modelo antes, mas acredito que é um exemplo básico de um. Acabei de encontrar este artigo que você pode ler onde o autor acredita que merece seu próprio nome: Idioma não virtual da interface
Gyan aka Gary Buyn
1

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.

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.

O caso 3 é superior ao caso 1? É por causa de 'melhor encapsulamento' ou 'mais frouxamente acoplado', ou algum outro motivo?

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.

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 ()?

Joe Coder deve obter o que merece por ignorar os testes de unidade que falharam. Ele testou essa classe, não foi? :)

Qual Case é o melhor para um projeto de software que pode ter 40.000 linhas de código?

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.

BЈовић
fonte
Obrigado pelo comentário. Atualizei o Caso 3 para deixar minha intenção mais clara.
MustafaM
11
Eu vou ter que -1 você aqui. Não há nenhuma razão para dizer que todas as interfaces devem ser puras ou que todas as classes devem herdar de uma interface.
DeadMG
@DeadMG ISP
BЈовић
@VJovic: Há uma grande diferença entre o SOLID e "Tudo deve herdar de uma interface".
DeadMG
"Um tamanho não serve para todos" e "aprenda alguns padrões de design" estão corretos - o restante da sua resposta viola sua própria sugestão de que um tamanho não serve para todos.
Joris Timmermans
0

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 IEmployeeefetivamente a mesma vantagem. Existem inúmeras maneiras de lidar com essas coisas.

Case 3: (best of both worlds?)

Similar to Case 1. However, imagine that (hypothetically)

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.

DeadMG
fonte
Você diz "você não tem implementações diferentes". Mas eu sim. Eu tenho a implementação de Employee por Enfermeira e posso ter outras implementações mais tarde (um médico, um zelador, etc.). Atualizei o Caso 3 para deixar mais claro o que eu quis dizer.
18712 MustafaM
@illmath: Mas você não tem outras implementações de get_name. Todas as suas implementações propostas compartilhariam a mesma implementação de get_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.
DeadMG
As interfaces não apenas podem ter implementações padrão em C ++, mas também podem ter implementações padrão e ainda serem abstratas! ou seja, virtual void IMethod () = 0 {std :: cout << "Ni!" << std :: endl; }
Joris Timmermans
@ MadKeithV: Eu não acredito que você possa defini-los em linha, mas o ponto ainda é o mesmo.
23712 DeadMG
@ MadKeith: Como se o Visual Studio tivesse sido uma representação particularmente precisa do Standard C ++.
DeadMG
0

O caso 3 é superior ao caso 1? É por causa de 'melhor encapsulamento' ou 'mais frouxamente acoplado', ou algum outro motivo?

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.

Qual caso é o melhor para um projeto de software que pode ter 40.000 linhas de código.

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

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 ()?

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.

Karthik Sreenivasan
fonte
0

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.

S.Robins
fonte