En la década de 2000, un colega mío me dijo que es un antipatrón hacer que los métodos públicos sean virtuales o abstractos.
Por ejemplo, consideró que una clase como esta no está bien diseñada:
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
}
}
Él dijo que
- El desarrollador de una clase derivada que implementa
Method1
y anulaMethod2
tiene que repetir la validación del argumento. - en caso de que el desarrollador de la clase base decida agregar algo alrededor de la parte personalizable
Method1
oMethod2
posterior, no puede hacerlo.
En cambio, mi colega propuso este enfoque:
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
}
}
Me dijo que hacer que los métodos públicos (o propiedades) sean virtuales o abstractos es tan malo como hacer públicos los campos. Al envolver los campos en propiedades, uno puede interceptar cualquier acceso a esos campos más adelante, si es necesario. Lo mismo se aplica a los miembros públicos virtuales / abstractos: envolverlos de la manera que se muestra en la ProtectedAbstractOrVirtual
clase permite al desarrollador de la clase base interceptar cualquier llamada que vaya a los métodos virtuales / abstractos.
Pero no veo esto como una guía de diseño. Incluso Microsoft no lo sigue: solo eche un vistazo a la Stream
clase para verificar esto.
¿Qué opinas de esa guía? ¿Tiene algún sentido, o crees que está complicando demasiado la API?
fuente
virtual
permite la anulación opcional. Su método probablemente debería ser público, ya que podría no ser anulado. Hacer métodosabstract
te obliga a anularlos; probablemente deberían serloprotected
, porque no son particularmente útiles en unpublic
contexto.protected
es más útil cuando desea exponer miembros privados de la clase abstracta a clases derivadas. En cualquier caso, no estoy particularmente preocupado por la opinión de tu amigo; elija el modificador de acceso que tenga más sentido para su situación particular.Respuestas:
Diciendo
está mezclando causa y efecto. Supone que cada método reemplazable requiere una validación de argumento no personalizable. Pero es todo lo contrario:
Si se quiere diseñar un método de manera que proporcione algunas validaciones de argumentos fijos en todas las derivaciones de la clase (o, más en general, una parte personalizable y una parte no personalizable), entonces tiene sentido hacer que el punto de entrada no sea virtual y, en su lugar, proporcione un método virtual o abstracto para la parte personalizable que se llama internamente.
Pero hay un montón de ejemplos en los que tiene sentido perfecto para tener un método virtual pública, ya que no se fija no personalizable parte: vistazo a los métodos estándar como
ToString
oEquals
oGetHashCode
- habría que mejorar el diseño de laobject
clase para tener estos no pública y virtual al mismo tiempo? No lo creo.O, en términos de su propio código: cuando el código en la clase base finalmente e intencionalmente se ve así
tener esta separación entre
Method1
yMethod1Core
solo complica las cosas sin razón aparente.fuente
ToString()
método, Microsoft mejor lo hizo no virtual e introdujo un método de plantilla virtualToStringCore()
. Por qué: debido a esto:ToString()
- nota para los herederos . Afirman queToString()
no debe devolver nulo. Podrían haber coaccionado esta demanda mediante la implementaciónToString() => ToStringCore() ?? string.Empty
.string.Empty
, ¿lo notó? Y recomienda muchas otras cosas que no pueden aplicarse en el código introduciendo algo así como unToStringCore
método. Entonces, esta técnica probablemente no sea la herramienta adecuada paraToString
.anyObjectIGot.ToString()
lugar deanyObjectIGot?.ToString()
": ¿cómo es esto relevante? SuToStringCore()
enfoque evitaría que se devuelva una cadena nula, pero aún arrojaría unNullReferenceException
si el objeto es nulo.public virtual
, sino que hay casos en los quepublic virtual
está bien. Podríamos argumentar a favor de una parte vacía no personalizable como una prueba futura del código ... Pero eso no funciona. Retroceder y cambiarlo podría romper los tipos derivados. Por lo tanto, no gana nada.Hacerlo de la manera que sugiere su colega proporciona más flexibilidad al implementador de la clase base. Pero con esto también viene una mayor complejidad que generalmente no se justifica por los supuestos beneficios.
Tenga en cuenta que la mayor flexibilidad para el implementador de la clase base se produce a expensas de una menor flexibilidad para la parte primordial. Obtienen un comportamiento impuesto que quizás no les interese particularmente. Para ellos, las cosas se han vuelto más rígidas. Esto puede ser justificado y útil, pero todo depende del escenario.
La convención de nomenclatura para implementar esto (que yo sepa) es reservar el buen nombre para la interfaz pública y prefijar el nombre del método interno con "Do".
Un caso útil es cuando la acción realizada necesita algo de configuración y algo de cierre. Al igual que abrir una secuencia y cerrarla después de que la anulación haya terminado. En general, el mismo tipo de inicialización y finalización. Es un patrón válido para usar, pero sería inútil ordenar su uso en todos los escenarios abstractos y virtuales.
fuente
BindingList
. Además, encontré la recomendación en alguna parte, tal vez sus Pautas de diseño del marco o algo similar. Y: es un postfix . ;-)En C ++, esto se denomina patrón de interfaz no virtual (NVI). (Érase una vez, se llamaba el Método de la Plantilla. Eso era confuso, pero algunos de los artículos más antiguos tienen esa terminología). Herb Sutter promueve el NVI, quien ha escrito sobre él al menos algunas veces. Creo que uno de los primeros está aquí .
Si recuerdo correctamente, la premisa es que una clase derivada no debería cambiar lo que hace la clase base, sino cómo lo hace.
Por ejemplo, una Forma podría tener un método Mover para reubicar la forma. Las implementaciones concretas (p. Ej., Como el cuadrado y los círculos) no deberían anular directamente el movimiento, porque Shape define lo que significa mover (a nivel conceptual). Un cuadrado puede tener detalles de implementación diferentes que un círculo en términos de cómo se representa internamente la posición, por lo que tendrá que anular algún método para proporcionar la funcionalidad Mover.
En ejemplos simples, esto a menudo se reduce a un Move público que simplemente delega todo el trabajo en un ReallyDoTheMove virtual privado, por lo que parece una gran sobrecarga sin ningún beneficio.
Pero esta correspondencia uno a uno no es un requisito. Por ejemplo, podría agregar un método Animate a la API pública de Shape, y podría implementarlo llamando a ReallyDoTheMove en un bucle. Termina con dos API de métodos públicos no virtuales que dependen del método abstracto privado. Sus círculos y cuadrados no necesitan hacer ningún trabajo adicional, ni tampoco puede anular Animate .
La clase base define la interfaz pública utilizada por los consumidores, y define una interfaz de operaciones primitivas que necesita para implementar esos métodos públicos. Los tipos derivados son responsables de proporcionar implementaciones de esas operaciones primitivas.
No conozco ninguna diferencia entre C # y C ++ que pueda cambiar este aspecto del diseño de clase.
fuente