Nunca torne os membros públicos virtuais / abstratos - realmente?


20

Nos anos 2000, um colega meu me disse que é um anti-padrão tornar os métodos públicos virtuais ou abstratos.

Por exemplo, ele considerou uma classe como esta não bem projetada:

public abstract class PublicAbstractOrVirtual
{
  public abstract void Method1(string argument);

  public virtual void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    // default implementation
  }
}

Ele afirmou que

  • o desenvolvedor de uma classe derivada que implementa Method1e substitui Method2deve repetir a validação do argumento.
  • caso o desenvolvedor da classe base decida adicionar algo à parte personalizável Method1ou Method2posterior, ele não poderá fazê-lo.

Em vez disso, meu colega propôs essa abordagem:

public abstract class ProtectedAbstractOrVirtual
{
  public void Method1(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method1Core(argument);
  }

  public void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method2Core(argument);
  }

  protected abstract void Method1Core(string argument);

  protected virtual void Method2Core(string argument)
  {
    // default implementation
  }
}

Ele me disse que tornar os métodos públicos (ou propriedades) virtuais ou abstratos é tão ruim quanto tornar os campos públicos. Ao agrupar campos em propriedades, é possível interceptar qualquer acesso a esses campos posteriormente, se necessário. O mesmo se aplica aos membros públicos / virtuais abstratos: agrupá-los da maneira mostrada na ProtectedAbstractOrVirtualclasse permite que o desenvolvedor da classe base intercepte todas as chamadas que vão para os métodos virtual / abstrato.

Mas não vejo isso como uma diretriz de design. Até a Microsoft não segue: basta dar uma olhada na Streamclasse para verificar isso.

O que você acha dessa linha de orientação? Isso faz algum sentido ou você acha que está complicando demais a API?


5
A criação de métodos virtual permite a substituição opcional. Seu método provavelmente deve ser público, pois pode não ser substituído. Criar métodos abstract obriga a substituí-los; provavelmente deveriam ser protected, porque não são particularmente úteis em um publiccontexto.
Robert Harvey

4
Na verdade, protectedé mais útil quando você deseja expor membros particulares da classe abstrata a classes derivadas. De qualquer forma, não estou particularmente preocupado com a opinião de seu amigo; escolha o modificador de acesso que faz mais sentido para sua situação específica.
Robert Harvey

4
Seu colega estava defendendo o Modelo de Método Padrão . Pode haver casos de uso para os dois lados, dependendo da interdependência dos dois métodos.
Greg Burghardt

8
@ GregBurghardt: parece que o colega do OP sugere sempre o uso do padrão de método de modelo, independentemente de ser necessário ou não. Esse é um uso excessivo típico de padrões - se alguém tem um martelo, mais cedo ou mais tarde todo problema começa a parecer um prego ;-)
Doc Brown

3
@PeterPerot: nunca tive problema em começar com DTOs simples com apenas campos públicos e, quando esse DTO acabou exigindo membros com lógica de negócios, refatore-os para classes com propriedades. Certamente, as coisas são diferentes quando alguém trabalha como fornecedor de bibliotecas e precisa cuidar de não alterar APIs públicas, e até mesmo transformar um campo público em uma propriedade pública com o mesmo nome pode causar problemas.
Doc Brown

Respostas:


30

Dizendo

que é um anti-padrão tornar os métodos públicos virtuais ou abstratos, devido ao desenvolvedor de uma classe derivada que implementa o Method1 e substitui o Method2, que deve repetir a validação do argumento

está misturando causa e efeito. Ele assume que todo método substituível requer uma validação de argumento não personalizável. Mas é o contrário:

Se alguém quiser criar um método em uma maneira que fornece algumas validações argumento fixas em todas as derivações da classe (ou - mais gerais - um personalizáveis e uma parte não personalizável), então faz sentido para fazer o ponto de entrada não-virtual e, em vez disso, forneça um método virtual ou abstrato para a parte personalizável chamada internamente.

Mas há muitos exemplos em que faz todo sentido ter um método virtual público, uma vez que não existe uma parte fixa não personalizável: observe métodos padrão como ToStringou Equalsou GetHashCode- melhoraria o design da objectclasse para que eles não fossem públicos e virtual ao mesmo tempo? Acho que não.

Ou, em termos de seu próprio código: quando o código na classe base finalmente e intencionalmente se parecer com isso

 public void Method1(string argument)
 {
    // nothing to validate here, all strings including null allowed
    this.Method1Core(argument);
 }

ter essa separação entre Method1e Method1Coreapenas complica as coisas sem motivo aparente.


1
No caso do ToString()método, a Microsoft tornou-o não virtual e introduziu um método de modelo virtual ToStringCore(). Por que: por causa disso: ToString()- note para os herdeiros . Eles afirmam que ToString()não devem retornar nulos. Eles poderiam ter coagido essa demanda implementando ToString() => ToStringCore() ?? string.Empty.
Peter Perot

9
@ PeterPerot: essa diretiva à qual você vinculou também recomenda não retornar string.Empty, você notou? E recomenda muitas outras coisas que não podem ser aplicadas no código, introduzindo algo como um ToStringCoremétodo. Portanto, essa técnica provavelmente não é a ferramenta certa para ToString.
Doc Brown

3
@ Theraot: certamente é possível encontrar algumas razões ou argumentos pelos quais ToString e Equals ou GetHashcode poderiam ter sido projetados de maneira diferente, mas hoje eles são como são (pelo menos eu acho que o design deles é bom o suficiente para dar um bom exemplo).
Doc Brown

1
@PeterPerot "Vi muitos códigos como em anyObjectIGot.ToString()vez de anyObjectIGot?.ToString()" - como isso é relevante? Sua ToStringCore()abordagem impediria que uma seqüência nula fosse retornada, mas ainda geraria um NullReferenceExceptionse o objeto for nulo.
IMil 04/09

1
@ PeterPerot Eu não estava tentando transmitir um argumento da autoridade. Não é tanto que a Microsoft usa public virtual, mas há casos em que public virtualestá tudo bem. Poderíamos argumentar por uma parte não personalizável vazia como tornar o código à prova do futuro ... Mas isso não funciona. Voltar e alterá-lo pode quebrar tipos derivados. Assim, ele não ganha nada.
Theraot

6

Fazer da maneira sugerida pelo seu colega fornece mais flexibilidade ao implementador da classe base. Mas com isso também vem mais complexidade, que normalmente não é justificada pelos benefícios presumidos.

Lembre-se de que o aumento da flexibilidade do implementador da classe base custa menos flexibilidade para a parte que substitui. Eles têm algum comportamento imposto que eles podem não cuidar particularmente. Para eles, as coisas ficaram mais rígidas. Isso pode ser justificado e útil, mas tudo depende do cenário.

A convenção de nomenclatura para implementar isso (que eu saiba) é reservar o bom nome para a interface pública e prefixar o nome do método interno com "Do".

Um caso útil é quando a ação executada precisa de alguma configuração e fechamento. Como abrir um fluxo e fechá-lo após a substituição. Em geral, o mesmo tipo de inicialização e finalização. É um padrão válido para usar, mas não faria sentido exigir seu uso em todos os cenários abstratos e virtuais.


1
O prefixo do método Do é uma opção. A Microsoft geralmente usa o postfix do método Core .
Peter Perot

@Peter Perot. Eu nunca vi o prefixo Core em nenhum material da Microsoft, mas isso pode ser porque eu não tenho prestado muita atenção ultimamente. Eu suspeito que eles começaram a fazer isso recentemente apenas para promover o apelido de Core, para criar um nome para o .NET Core.
Martin Maat

Não, é um velho chapéu: BindingList. Além disso, encontrei a recomendação em algum lugar, talvez suas Diretrizes de Design da Estrutura ou algo semelhante. E: é um postfix . ;-)
Peter Perot

Menos flexibilidade para a classe derivada é o ponto. A classe base é um limite de abstração. A classe base informa ao consumidor o que sua API pública faz e define a API necessária para atingir esses objetivos. Se uma classe derivada puder substituir um método público na classe base, você corre um risco maior de violar o Princípio de Substituição de Liskov.
Adrian McCarthy

2

No C ++, isso é chamado de padrão de interface não virtual (NVI). (Era uma vez chamado de Método Modelo. Isso era confuso, mas alguns dos artigos mais antigos têm essa terminologia.) A NVI é promovida por Herb Sutter, que já escreveu sobre isso pelo menos algumas vezes. Eu acho que um dos primeiros está aqui .

Se bem me lembro, a premissa é que uma classe derivada não deve mudar o que a classe base faz, mas como faz.

Por exemplo, um Shape pode ter um método Move para realocar a forma. Umas implementações concretas (por exemplo, como Quadrado e Círculos) não devem substituir diretamente o Move, porque Shape define o que Moving significa (em um nível conceitual). Um quadrado pode ter detalhes de implementação diferentes do que um círculo em termos de como a posição é representada internamente; portanto, eles terão que substituir algum método para fornecer a funcionalidade Mover.

Em exemplos simples, isso geralmente se resume a uma mudança pública que apenas delega todo o trabalho em um ReallyDoTheMove virtual virtual, de modo que parece haver muita sobrecarga sem nenhum benefício.

Mas essa correspondência individual não é um requisito. Por exemplo, você pode adicionar um método Animate à API pública do Shape e pode implementá-lo chamando ReallyDoTheMove em um loop. Você acaba com duas APIs públicas de métodos não virtuais que dependem do único método abstrato privado. Seus círculos e quadrados não precisam fazer nenhum trabalho extra, nem a substituição do Animate .

A classe base define a interface pública usada pelos consumidores e define uma interface de operações primitivas necessárias para implementar esses métodos públicos. Os tipos derivados são responsáveis ​​por fornecer implementações dessas operações primitivas.

Não estou ciente de nenhuma diferença entre C # e C ++ que alteraria esse aspecto do design de classe.


Boa descoberta! Agora, lembro-me de que encontrei exatamente esse post do seu segundo link para (ou uma cópia dele), nos anos 2000. Lembro que estava procurando mais evidências da afirmação de meu colega e que não encontrei nada no contexto de C #, mas C ++. Este. É. Isto! :-) Mas, em C # land, parece que esse padrão não é muito usado. Talvez as pessoas tenham percebido que adicionar funcionalidades básicas mais tarde também pode interromper as classes derivadas, e que o uso estrito do TMP ou NVIP em vez de métodos virtuais públicos nem sempre faz sentido.
Peter Perot

Nota para self: é bom saber que esse padrão tem um nome: NVIP.
Peter Perot
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.