Utilice la clase abstracta en C # como definición

21

Como desarrollador de C ++, estoy bastante acostumbrado a los archivos de encabezado de C ++, y encuentro beneficioso tener algún tipo de "documentación" forzada dentro del código. Por lo general, tengo un mal momento cuando tengo que leer un código C # debido a eso: no tengo ese tipo de mapa mental de la clase con la que estoy trabajando.

Asumamos que como ingeniero de software estoy diseñando el marco de un programa. ¿Sería una locura definir cada clase como una clase abstracta no implementada, de manera similar a lo que haríamos con los encabezados de C ++, y dejar que los desarrolladores la implementen?

Supongo que puede haber algunas razones por las cuales alguien podría encontrar que esta es una solución terrible, pero no estoy seguro de por qué. ¿Qué debería uno considerar para una solución como esta?

Dani Barca Casafont
fuente
13
C # es un lenguaje de programación totalmente diferente que C ++. Alguna sintaxis es similar, pero debe comprender C # para hacer un buen trabajo en ella. Y debe hacer algo sobre el mal hábito que tiene al confiar en los archivos de encabezado. He trabajado con C ++ durante décadas, nunca he leído los archivos de encabezado, solo los he escrito.
Doblado
17
¡Una de las mejores cosas de C # y Java es la libertad de los archivos de encabezado! Solo usa un buen IDE.
Erik Eidt
99
Las declaraciones en los archivos de encabezado de C ++ no terminan siendo parte del binario generado. Están ahí para el compilador y el enlazador. Estas clases abstractas de C # serían parte del código generado, sin beneficio alguno.
Mark Benningfield el
16
La construcción equivalente en C # es una interfaz, no una clase abstracta.
Robert Harvey
66
@DaniBarcaCasafont "Veo los encabezados como una forma rápida y confiable de ver qué son los métodos y atributos de una clase, qué tipos de parámetros espera y qué devuelve". Cuando uso Visual Studio, mi alternativa a eso es el atajo Ctrl-MO.
wha7ever - Restablecer Monica

Respuestas:

44

La razón por la que esto se hizo en C ++ tuvo que ver con hacer que los compiladores sean más rápidos y fáciles de implementar. Este no fue un diseño para facilitar la programación .

El propósito de los archivos de encabezado era permitir al compilador hacer un primer paso súper rápido para conocer todos los nombres de funciones esperados y asignarles ubicaciones de memoria para que puedan ser referenciados cuando se invoquen en archivos cp, incluso si la clase que los definió tenía No se ha analizado todavía.

¡No se recomienda intentar replicar una consecuencia de las antiguas limitaciones de hardware en un entorno de desarrollo moderno!

Definir una interfaz o clase abstracta para cada clase reducirá su productividad; ¿Qué más podrías haber hecho con ese tiempo ? Además, otros desarrolladores no seguirán esta convención.

De hecho, otros desarrolladores pueden eliminar sus clases abstractas. Si encuentro una interfaz en el código que cumple estos dos criterios, la elimino y la refactorizo ​​fuera del código:1. Does not conform to the interface segregation principle 2. Only has one class that inherits from it

La otra cosa es que hay herramientas incluidas con Visual Studio que hacen lo que pretendes lograr automáticamente:

  1. los Class View
  2. los Object Browser
  3. En Solution Explorerpuede hacer clic en triángulos para gastar clases para ver sus funciones, parámetros y tipos de retorno.
  4. Hay tres pequeños menús desplegables debajo de las pestañas de archivo, el más a la derecha enumera todos los miembros de la clase actual.

Pruebe uno de los anteriores antes de dedicar tiempo a replicar archivos de encabezado de C ++ en C #.


Además, hay razones técnicas para no hacer esto ... hará que su binario final sea más grande de lo necesario. Repetiré este comentario de Mark Benningfield:

Las declaraciones en los archivos de encabezado de C ++ no terminan siendo parte del binario generado. Están ahí para el compilador y el enlazador. Estas clases abstractas de C # serían parte del código generado, sin beneficio alguno.


Además, mencionado por Robert Harvey, técnicamente, el equivalente más cercano de un encabezado en C # sería una interfaz, no una clase abstracta.

TheCatWhisperer
fuente
2
También terminaría con muchas llamadas de despacho virtuales innecesarias (no es bueno si su código es sensible al rendimiento).
Lucas Trzesniewski
55
aquí está el Principio de segregación de interfaz al que se hace referencia
NH.
2
También podría simplemente colapsar toda la clase (clic derecho -> Esquema -> Contraer a definiciones) para obtener una vista panorámica de la misma.
jpmc26
"¿Qué más podría haber hecho con ese tiempo" -> Uhm, copiar y pegar y muy, muy poco, postwork? Me lleva unos 3 segundos, en todo caso. Por supuesto, podría haber usado ese tiempo para sacar una punta de filtro en preparación para rodar un cigarrillo. No es realmente un punto relevante. [Descargo de responsabilidad: me encantan ambos, C # idiomático, así como C ++ idiomático y algunos idiomas más]
phresnel
1
@phresnel: Me gustaría verte intentar repetir las definiciones de una clase y heredar la clase original de la abstracta en 3s, confirmada sin errores, para cualquier clase que sea lo suficientemente grande como para no tener una vista panorámica fácil (ya que ese es el caso de uso propuesto por el OP: clases supuestamente complicadas de otro modo complicadas). Casi inherentemente, la naturaleza complicada de la clase original significa que no puedes escribir la definición de clase abstracta de memoria (porque eso significaría que ya la sabes de memoria) Y luego considerar el tiempo necesario para hacer esto para una base de código completa .
Flater
14

Primero, comprenda que una clase puramente abstracta es realmente solo una interfaz que no puede hacer herencia múltiple.

Escribir clase, extraer interfaz, es una actividad con muerte cerebral. Tanto es así que tenemos una refactorización para ello. Lo cual es una pena. Seguir este patrón de "cada clase tiene una interfaz" no solo produce desorden, sino que pierde el punto por completo.

Una interfaz no debe considerarse simplemente como una reformulación formal de lo que la clase pueda hacer. Una interfaz debe considerarse como un contrato impuesto por el uso del código del cliente que detalla sus necesidades.

No tengo ningún problema para escribir una interfaz que actualmente solo tiene una clase que la implementa. De hecho, no me importa si ninguna clase lo implementa todavía. Porque estoy pensando en lo que necesita mi código de uso. La interfaz expresa lo que exige el código de uso. Lo que venga después puede hacer lo que quiera siempre y cuando satisfaga estas expectativas.

Ahora no hago esto cada vez que un objeto usa otro. Hago esto cuando cruzo un límite. Lo hago cuando no quiero que un objeto sepa exactamente con qué otro objeto está hablando. Cuál es la única forma en que funcionará el polimorfismo. Lo hago cuando espero que el objeto del código de mi cliente pueda cambiar. Ciertamente no hago esto cuando lo que estoy usando es la clase String. La clase String es agradable y estable y no siento la necesidad de evitar que me cambie.

Cuando decide interactuar directamente con una implementación concreta en lugar de hacerlo a través de una abstracción, predice que la implementación es lo suficientemente estable como para confiar en no cambiar.

Ahí está la forma en que atenúo el Principio de Inversión de Dependencia . No deberías aplicar esto ciegamente a todo fanáticamente. Cuando agrega una abstracción, realmente está diciendo que no confía en que la elección de implementar la clase sea estable durante la vida del proyecto.

Todo esto supone que está tratando de seguir el Principio Abierto Cerrado . Este principio es importante solo cuando los costos asociados con la realización de cambios directos al código establecido son significativos. Una de las principales razones por las que las personas no están de acuerdo sobre la importancia de los objetos de desacoplamiento es porque no todos experimentan los mismos costos al realizar cambios directos. Si volver a probar, volver a compilar y redistribuir su base de código completa es trivial para usted, entonces resolver una necesidad de cambio con modificación directa es probablemente una simplificación muy atractiva de este problema.

Simplemente no hay una respuesta cerebral a esta pregunta. Una interfaz o clase abstracta no es algo que debe agregar a cada clase y no puede simplemente contar el número de clases de implementación y decidir que no es necesario. Se trata de lidiar con el cambio. Lo que significa que estás anticipando el futuro. No te sorprendas si te equivocas. Hazlo lo más simple posible sin retroceder en una esquina.

Entonces, por favor, no escriba abstracciones solo para ayudarnos a leer el código. Tenemos herramientas para eso. Use abstracciones para desacoplar lo que necesita desacoplamiento.

naranja confitada
fuente
5

Sí, eso sería terrible porque (1) introduce código innecesario (2) confundirá al lector.

Si desea programar en C #, debería acostumbrarse a leer C #. El código escrito por otras personas no seguirá este patrón de todos modos.

JacquesB
fuente
1

Pruebas unitarias

Sugeriría encarecidamente que escriba pruebas unitarias en lugar de saturar el código con interfaces o clases abstractas (excepto donde esté justificado por otros motivos).

Una prueba unitaria bien escrita no solo describe la interfaz de su clase (como lo haría un archivo de encabezado, clase abstracta o interfaz) sino que también describe, por ejemplo, la funcionalidad deseada.

Ejemplo: donde podría haber escrito un archivo de encabezado myclass.h como este:

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

En cambio, en C # escriba una prueba como esta:

[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");
    }
}

Esta prueba muy simple transmite la misma información que el archivo de encabezado. (Deberíamos tener una clase llamada "MyClass" con una función sin parámetros "Foo") Si bien un archivo de encabezado es más compacto, la prueba contiene mucha más información.

Una advertencia: tal proceso de tener un ingeniero de software senior que proporcione pruebas (fallidas) para que otros desarrolladores resuelvan choques violentamente con metodologías como TDD, pero en su caso sería una gran mejora.

Guran
fuente
¿Cómo se hacen las pruebas unitarias (pruebas aisladas) = ​​se necesitan simulacros, sin interfaces? Qué marco admite la burla de una implementación real, la mayoría que he visto utiliza una interfaz para intercambiar la implementación con la implementación simulada.
Bulan
No creo que se necesiten simulacros de nessecarily para los propósitos del OP. Recuerde, esto no son pruebas unitarias como herramientas para TDD, sino pruebas unitarias como reemplazo de los archivos de encabezado. (Por supuesto, podrían evolucionar en pruebas unitarias "habituales" con simulacros, etc.)
Guran
Ok, si solo considero el lado de los OP, lo entiendo mejor. Lea su respuesta como una respuesta de aplicación más general. Gracias por aclarar!
Bulan
@Bulan, la necesidad de burlarse a menudo es indicativa de un mal diseño
TheCatWhisperer
@TheCatWhisperer Sí, soy muy consciente de eso, así que no hay discusión allí, tampoco puedo ver que hice esa declaración en ninguna parte: DI estaba hablando de pruebas en general, que todo el Mock-framework que he usado usa Interfaces para intercambiar fuera de la implementación real, y si hubo alguna otra técnica sobre cómo se burló si no tiene interfaces.
Bulan