Usar classe abstrata em C # como definição

21

Como desenvolvedor de C ++, estou acostumado a arquivos de cabeçalho em C ++ e considero benéfico ter algum tipo de "documentação" forçada dentro do código. Normalmente, me divirto muito quando preciso ler um código C # por causa disso: não tenho esse tipo de mapa mental da classe com a qual estou trabalhando.

Vamos supor que, como engenheiro de software, estou projetando a estrutura de um programa. Seria muito louco definir cada classe como uma classe abstrata não implementada, de forma semelhante ao que faríamos com cabeçalhos de C ++, e permitir que os desenvolvedores a implementassem?

Acho que pode haver algumas razões pelas quais alguém pode achar que essa é uma solução terrível, mas não sei por que. O que alguém teria que considerar para uma solução como essa?

Dani Barca Casafont
fonte
13
C # é uma linguagem de programação totalmente diferente do C ++. Alguma sintaxe é semelhante, mas você precisa entender C # para fazer um bom trabalho nela. E você deve fazer algo sobre o mau hábito de contar com os arquivos de cabeçalho. Trabalho com C ++ há décadas, nunca li os arquivos de cabeçalho, apenas os escrevi.
Bent
17
Uma das melhores coisas de C # e Java é a liberdade dos arquivos de cabeçalho! Basta usar um bom IDE.
Erik Eidt
9
As declarações nos arquivos de cabeçalho C ++ não acabam fazendo parte do binário gerado. Eles estão lá para o compilador e o vinculador. Essas classes abstratas de C # seriam parte do código gerado, sem nenhum benefício.
precisa saber é o seguinte
16
A construção equivalente em C # é uma interface, não uma classe abstrata.
Robert Harvey
6
@DaniBarcaCasafont "Eu vejo os cabeçalhos como uma maneira rápida e confiável de ver quais são os métodos e atributos de uma classe, quais tipos de parâmetros ele espera e o que retorna". Ao usar o Visual Studio, minha alternativa é o atalho Ctrl-MO.
wha7ever - Restabelece Monica

Respostas:

44

A razão pela qual isso foi feito em C ++ tinha a ver com tornar os compiladores mais rápidos e fáceis de implementar. Este não foi um projeto para facilitar a programação .

O objetivo dos arquivos de cabeçalho era permitir que o compilador fizesse uma primeira passagem super rápida para conhecer todos os nomes de funções esperados e alocar os locais de memória para eles, para que pudessem ser referenciados quando chamados em arquivos cp, mesmo que a classe que os definisse tivesse ainda não foi analisado.

Não é recomendável tentar replicar uma consequência de limitações de hardware antigas em um ambiente de desenvolvimento moderno!

Definir uma interface ou classe abstrata para cada classe reduzirá sua produtividade; o que mais você poderia ter feito com esse tempo ? Além disso, outros desenvolvedores não seguirão esta convenção.

De fato, outros desenvolvedores podem excluir suas classes abstratas. Se eu encontrar uma interface no código que atenda a esses dois critérios, eu a apago e refatoro do código:1. Does not conform to the interface segregation principle 2. Only has one class that inherits from it

A outra coisa é que existem ferramentas incluídas no Visual Studio que fazem o que você deseja realizar automaticamente:

  1. o Class View
  2. o Object Browser
  3. Em Solution Explorervocê pode clicar em triângulos para gastar classes para ver suas funções, parâmetros e tipos de retorno.
  4. Existem três pequenos menus suspensos abaixo das guias do arquivo, o mais à direita lista todos os membros da classe atual.

Experimente uma das opções acima antes de dedicar tempo à replicação dos arquivos de cabeçalho C ++ em C #.


Além disso, existem razões técnicas para não fazer isso ... ele tornará seu binário final maior do que o necessário. Vou repetir este comentário de Mark Benningfield:

As declarações nos arquivos de cabeçalho C ++ não acabam fazendo parte do binário gerado. Eles estão lá para o compilador e o vinculador. Essas classes abstratas de C # seriam parte do código gerado, sem nenhum benefício.


Além disso, mencionado por Robert Harvey, tecnicamente, o equivalente mais próximo de um cabeçalho em C # seria uma interface, não uma classe abstrata.

TheCatWhisperer
fonte
2
Você também terminaria com muitas chamadas de despacho virtual desnecessárias (nada bom se o seu código for sensível ao desempenho).
Lucas Trzesniewski
5
Aqui está o Princípio de Segregação de Interface referido.
NH.
2
Pode-se também simplesmente recolher a classe inteira (clique com o botão direito do mouse -> Estrutura de tópicos -> Recolher as definições) para obter uma visão geral.
precisa saber é
"o que mais você poderia ter feito com esse tempo" -> Uhm, copie e cole e muito mais pequenas postagens? Demora cerca de 3 segundos, se for o caso. Claro, eu poderia ter usado esse tempo para tirar uma ponta de filtro em preparação para enrolar um cigarro. Não é realmente um ponto relevante. [Disclaimer: Eu amo tanto, idiomática C #, bem como idiomática C ++, e mais algumas línguas]
phresnel
1
@phresnel: Gostaria de ver você tentar repetir as definições de uma classe e herdar a classe original da abstrata em 3s, confirmada sem erros, para qualquer classe que seja suficientemente grande para não ter uma visão fácil sobre (como esse é o caso de uso proposto pelo OP: classes supostamente descomplicadas ou complicadas). Quase inerentemente, a natureza complicada da classe original significa que você não pode simplesmente escrever a definição de classe abstrata de cor (porque isso significa que você já a conhece de cor). E então considere o tempo necessário para fazer isso em toda uma base de código.
Flater
14

Primeiro, entenda que uma classe puramente abstrata é realmente apenas uma interface que não pode fazer herança múltipla.

Classe de gravação, interface de extração, é uma atividade com morte cerebral. Tanto é assim que temos uma refatoração para isso. O que é uma pena. Após esse padrão "toda classe obtém uma interface", não apenas produz desordem, que perde completamente o objetivo.

Uma interface não deve ser pensada como uma simples reformulação formal do que a classe pode fazer. Uma interface deve ser pensada como um contrato imposto pelo código do cliente usando detalhando suas necessidades.

Não tenho problemas em escrever uma interface que atualmente tenha apenas uma classe implementando-a. Na verdade, eu não me importo se nenhuma aula ainda a implementa. Porque estou pensando no que meu código de uso precisa. A interface expressa o que o código em uso exige. O que vier depois pode fazer o que quiser, desde que satisfaça essas expectativas.

Agora não faço isso toda vez que um objeto usa outro. Eu faço isso ao atravessar uma fronteira. Faço isso quando não quero que um objeto saiba exatamente com qual outro objeto está falando. Qual é a única maneira de o polimorfismo funcionar. Faço isso quando espero que o objeto que meu código de cliente está falando provavelmente mude. Certamente não faço isso quando estou usando a classe String. A classe String é agradável e estável e não sinto necessidade de me proteger contra isso.

Quando você decide interagir diretamente com uma implementação concreta, e não através de uma abstração, está prevendo que a implementação é estável o suficiente para confiar em não mudar.

É assim que tempero o Princípio da Inversão da Dependência . Você não deve aplicar cegamente fanaticamente isso a tudo. Quando você adiciona uma abstração, está realmente dizendo que não confia que a escolha da classe de implementação seja estável ao longo da vida do projeto.

Tudo isso pressupõe que você esteja tentando seguir o Princípio Aberto Fechado . Esse princípio é importante apenas quando os custos associados à realização de alterações diretas no código estabelecido são significativos. Uma das principais razões pelas quais as pessoas discordam sobre a importância da dissociação de objetos é porque nem todos experimentam os mesmos custos ao fazer mudanças diretas. Se retestar, recompilar e redistribuir toda a sua base de código for trivial para você, resolver a necessidade de alteração com modificação direta provavelmente será uma simplificação muito atraente desse problema.

Simplesmente não existe uma resposta de morte cerebral para essa pergunta. Uma interface ou classe abstrata não é algo que você deve adicionar a todas as classes e você não pode apenas contar o número de classes de implementação e decidir que não é necessário. É sobre lidar com mudanças. O que significa que você está antecipando o futuro. Não se surpreenda se você errar. Mantenha o mais simples possível, sem recuar no canto.

Portanto, não escreva abstrações apenas para nos ajudar a ler o código. Temos ferramentas para isso. Use abstrações para dissociar o que precisa ser dissociado.

candied_orange
fonte
5

Sim, isso seria terrível porque (1) introduz código desnecessário (2) confunde o leitor.

Se você deseja programar em C #, deve se acostumar a ler C #. O código escrito por outras pessoas não seguirá esse padrão de qualquer maneira.

JacquesB
fonte
1

Testes unitários

Eu sugeriria fortemente que você escrevesse testes de unidade em vez de sobrecarregar o código com interfaces ou classes abstratas (exceto quando necessário por outros motivos).

Um teste de unidade bem escrito não apenas descreve a interface da sua classe (como um arquivo de cabeçalho, classe abstrata ou interface faria), mas também descreve, por exemplo, a funcionalidade desejada.

Exemplo: Onde você pode ter escrito um arquivo de cabeçalho myclass.h assim:

class MyClass
{
public:
  void foo();
};

Em vez disso, em c #, escreva um teste como este:

[TestClass]
public class MyClassTests
{
    [TestMethod]
    public void MyClass_should_have_method_Foo()
    {
        //Arrange
        var myClass = new MyClass();
        //Act
        myClass.Foo();
        //Verify
        Assert.Inconclusive("TODO: Write a more detailed test");
    }
}

Este teste muito simples transmite as mesmas informações que o arquivo de cabeçalho. (Deveríamos ter uma classe chamada "MyClass" com uma função sem parâmetros "Foo"). Enquanto um arquivo de cabeçalho é mais compacto, o teste contém muito mais informações.

Uma ressalva: esse processo de ter um engenheiro de software sênior fornecendo (falhando) testes para outros desenvolvedores para resolver conflitos violentamente com metodologias como TDD, mas no seu caso, seria uma grande melhoria.

Guran
fonte
Como você faz o teste de unidade (testes isolados) = zombarias necessárias, sem interfaces? Qual estrutura suporta simulação de uma implementação real, a maioria que eu já vi usa uma interface para trocar a implementação pela implementação simulada.
Bulan
Não acho que as zombarias sejam necessárias para os propósitos do OP: s. Lembre-se, isso não é testes de unidade como ferramentas para TDD, mas teste de unidade como um substituto para arquivos de cabeçalho. (Eles podem, naturalmente evoluir para testes "habituais" da unidade com simulações etc)
Guran
Ok, se eu considerar apenas os OPs, entendo melhor. Leia sua resposta como uma resposta de aplicação mais geral. Obrigado por esclarecer!
Bulan
@Bulan, a necessidade de zombaria extensiva é frequentemente indicativa de um projeto ruim
TheCatWhisperer
@TheCatWhisperer Sim, eu estou bem ciente disso, então nenhum argumento aqui também não pode ver que eu fiz essa declaração em qualquer lugar: DI estava falando sobre testes em geral, que todo o framework Mock que eu usei usa interfaces para trocar a implementação real e se havia alguma outra técnica sobre como você zombaria se não tivesse interfaces.
Bulan