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
Method1
e substituiMethod2
deve repetir a validação do argumento. - caso o desenvolvedor da classe base decida adicionar algo à parte personalizável
Method1
ouMethod2
posterior, 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 ProtectedAbstractOrVirtual
classe 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 Stream
classe 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?
fonte
virtual
permite a substituição opcional. Seu método provavelmente deve ser público, pois pode não ser substituído. Criar métodosabstract
obriga a substituí-los; provavelmente deveriam serprotected
, porque não são particularmente úteis em umpublic
contexto.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.Respostas:
Dizendo
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
ToString
ouEquals
ouGetHashCode
- melhoraria o design daobject
classe 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
ter essa separação entre
Method1
eMethod1Core
apenas complica as coisas sem motivo aparente.fonte
ToString()
método, a Microsoft tornou-o não virtual e introduziu um método de modelo virtualToStringCore()
. Por que: por causa disso:ToString()
- note para os herdeiros . Eles afirmam queToString()
não devem retornar nulos. Eles poderiam ter coagido essa demanda implementandoToString() => ToStringCore() ?? string.Empty
.string.Empty
, você notou? E recomenda muitas outras coisas que não podem ser aplicadas no código, introduzindo algo como umToStringCore
método. Portanto, essa técnica provavelmente não é a ferramenta certa paraToString
.anyObjectIGot.ToString()
vez deanyObjectIGot?.ToString()
" - como isso é relevante? SuaToStringCore()
abordagem impediria que uma seqüência nula fosse retornada, mas ainda geraria umNullReferenceException
se o objeto for nulo.public virtual
, mas há casos em quepublic virtual
está 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.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.
fonte
BindingList
. Além disso, encontrei a recomendação em algum lugar, talvez suas Diretrizes de Design da Estrutura ou algo semelhante. E: é um postfix . ;-)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.
fonte