Hay dos formas en que se usan las clases base abstractas.
Está especializando su objeto abstracto, pero todos los clientes utilizarán la clase derivada a través de su interfaz base.
Está utilizando una clase base abstracta para factorizar la duplicación dentro de los objetos en su diseño, y los clientes usan las implementaciones concretas a través de sus propias interfaces.
Solución para 1 - Patrón de estrategia
Si tiene la primera situación, entonces tiene una interfaz definida por los métodos virtuales en la clase abstracta que están implementando sus clases derivadas.
Debería considerar hacer de esto una interfaz real, cambiar su clase abstracta para que sea concreta, y tomar una instancia de esta interfaz en su constructor. Sus clases derivadas se convierten en implementaciones de esta nueva interfaz.
Esto significa que ahora puede probar su clase abstracta anterior utilizando una instancia simulada de la nueva interfaz y cada nueva implementación a través de la interfaz ahora pública. Todo es simple y comprobable.
Solución para 2
Si tiene la segunda situación, su clase abstracta funciona como una clase auxiliar.
Eche un vistazo a la funcionalidad que contiene. Vea si algo de esto puede ser empujado sobre los objetos que están siendo manipulados para minimizar esta duplicación. Si aún le queda algo, considere convertirlo en una clase auxiliar que su implementación concreta tome en su constructor y elimine su clase base.
Esto nuevamente conduce a clases concretas que son simples y fácilmente comprobables.
Como una regla
Favorezca la red compleja de objetos simples sobre una red simple de objetos complejos.
La clave del código comprobable extensible son los pequeños bloques de construcción y el cableado independiente.
Actualizado: ¿Cómo manejar mezclas de ambos?
Es posible tener una clase base que realice ambos roles ... es decir, tiene una interfaz pública y tiene métodos auxiliares protegidos. Si este es el caso, puede factorizar los métodos auxiliares en una clase (escenario2) y convertir el árbol de herencia en un patrón de estrategia.
Si encuentra que tiene algunos métodos que su clase base implementa directamente y otros son virtuales, aún puede convertir el árbol de herencia en un patrón de estrategia, pero también lo tomaría como un buen indicador de que las responsabilidades no están correctamente alineadas, y puede necesita refactorización.
Actualización 2: Clases abstractas como un trampolín (12/06/2014)
El otro día tuve una situación en la que utilicé el resumen, así que me gustaría explorar por qué.
Tenemos un formato estándar para nuestros archivos de configuración. Esta herramienta en particular tiene 3 archivos de configuración, todos en ese formato. Quería una clase fuertemente tipada para cada archivo de configuración, por lo que, a través de la inyección de dependencia, una clase podría pedir la configuración que le interesaba.
Implementé esto al tener una clase base abstracta que sabe cómo analizar los formatos de los archivos de configuración y las clases derivadas que expusieron esos mismos métodos, pero encapsularon la ubicación del archivo de configuración.
Podría haber escrito un "SettingsFileParser" que envuelvan las 3 clases, y luego delegarlo en la clase base para exponer los métodos de acceso a datos. Elegí no hacer esto todavía, ya que conduciría a 3 clases derivadas con más código de delegación en ellas que cualquier otra cosa.
Sin embargo ... a medida que este código evoluciona y los consumidores de cada una de estas clases de configuración se vuelven más claros. Los usuarios de cada configuración solicitarán algunas configuraciones y las transformarán de alguna manera (como las configuraciones son texto, pueden envolverlas en objetos para convertirlas en números, etc.). A medida que esto suceda, comenzaré a extraer esta lógica en los métodos de manipulación de datos y los empujaré nuevamente a las clases de configuración fuertemente tipadas. Esto conducirá a una interfaz de nivel superior para cada conjunto de configuraciones, que eventualmente ya no es consciente de que se trata de 'configuraciones'.
En este punto, las clases de configuración fuertemente tipadas ya no necesitarán los métodos "getter" que exponen la implementación subyacente de 'configuración'.
En ese punto ya no querría que su interfaz pública incluyera los métodos de acceso a la configuración; así que cambiaré esta clase para encapsular una clase de analizador de configuraciones en lugar de derivarla.
Por lo tanto, la clase Resumen es: una forma de evitar el código de delegación en este momento y un marcador en el código para recordarme que cambie el diseño más adelante. Es posible que nunca llegue a él, por lo que puede vivir un buen rato ... solo el código puede decirlo.
Creo que esto es cierto con cualquier regla ... como "sin métodos estáticos" o "sin métodos privados". Indican un olor en el código ... y eso es bueno. Lo mantiene buscando la abstracción que se ha perdido ... y le permite seguir proporcionando valor a su cliente mientras tanto.
Me imagino reglas como esta que definen un paisaje, donde el código mantenible vive en los valles. A medida que agrega un nuevo comportamiento, es como que llueva en su código. Inicialmente lo pones donde aterriza ... luego refactorizas para permitir que las fuerzas del buen diseño impulsen el comportamiento hasta que todo termine en los valles.