Subclases solo para constructores: ¿es este un antipatrón?


37

Estaba teniendo una discusión con un compañero de trabajo, y terminamos teniendo intuiciones conflictivas sobre el propósito de la subclase. Mi intuición es que si una función principal de una subclase es expresar un rango limitado de valores posibles de su padre, entonces probablemente no debería ser una subclase. Argumentó la intuición opuesta: que la subclase representa que un objeto es más "específico" y, por lo tanto, una relación de subclase es más apropiada.

Para poner mi intuición más concretamente, creo que si tengo una subclase que extiende una clase padre pero el único código que la subclase anula es un constructor (sí, sé que los constructores generalmente no "anulan", tengan paciencia conmigo), entonces lo que realmente se necesitaba era un método auxiliar.

Por ejemplo, considere esta clase de la vida real:

public class DataHelperBuilder
{
    public string DatabaseEngine { get; set; }
    public string ConnectionString { get; set; }

    public DataHelperBuilder(string databaseEngine, string connectionString)
    {
        DatabaseEngine = databaseEngine;
        ConnectionString = connectionString;
    }

    // Other optional "DataHelper" configuration settings omitted

    public DataHelper CreateDataHelper()
    {
        Type dataHelperType = DatabaseEngineTypeHelper.GetType(DatabaseEngine);
        DataHelper dh = (DataHelper)Activator.CreateInstance(dataHelperType);
        dh.SetConnectionString(ConnectionString);

        // Omitted some code that applies decorators to the returned object
        // based on omitted configuration settings

        return dh;
    }
}

Su afirmación es que sería completamente apropiado tener una subclase como esta:

public class SystemDataHelperBuilder
{
    public SystemDataHelperBuilder()
        : base(Configuration.GetSystemDatabaseEngine(),
               Configuration.GetSystemConnectionString())
    {
    }
 }

Entonces, la pregunta:

  1. Entre las personas que hablan sobre patrones de diseño, ¿cuál de estas intuiciones es correcta? ¿La subclase como se describió anteriormente es un antipatrón?
  2. Si es un antipatrón, ¿cómo se llama?

Pido disculpas si esto resultó ser una respuesta fácil de googleable; mis búsquedas en google en su mayoría arrojaron información sobre el antipatrón del constructor telescópico y no realmente lo que estaba buscando.


2
Considero que la palabra "Ayudante" en un nombre es un antipatrón. Es como leer un ensayo con referencias constantes a una cosita y un qué. Decir lo que es . ¿Es una fábrica? ¿Constructor? Para mí huele a un proceso de pensamiento perezoso, y alienta la violación del principio de responsabilidad única, ya que un nombre semántico apropiado lo dirigiría. Estoy de acuerdo con los otros votantes, y usted debe aceptar la respuesta de @ lxrec. Crear una subclase para un método de fábrica es completamente inútil, a menos que no pueda confiar en que los usuarios pasen los argumentos correctos como se especifica en la documentación. En cuyo caso, obtenga nuevos usuarios.
Aaron Hall

Estoy totalmente de acuerdo con la respuesta de @ Ixrec. Después de todo, su respuesta dice que siempre tuve razón y que mi compañero de trabajo estaba equivocado. ;-) Pero en serio, es exactamente por eso que me sentí raro al aceptarlo; Pensé que podría ser acusado de parcialidad. Pero soy nuevo en los programadores StackExchange; Estaría dispuesto a aceptarlo si me aseguraran que no sería una violación de la etiqueta.
HardlyKnowEm

1
No, se espera que acepte la respuesta que considera más valiosa. El que la comunidad cree que es más valioso hasta el momento es el de Ixrec por mucho más de 3 a 1. Por lo tanto, esperarían que lo acepte. En esta comunidad, se expresa muy poca frustración en un interrogador que acepta la respuesta incorrecta, pero hay una gran frustración expresada en los interrogatorios que nunca aceptan una respuesta. Tenga en cuenta que nadie se queja de la respuesta aceptada aquí, sino que se queja de la votación, que parece enderezarse: stackoverflow.com/q/2052390/541136
Aaron Hall

Muy bien, lo aceptaré. Gracias por tomarse el tiempo para educarme sobre los estándares de la comunidad aquí.
HardlyKnowEm

Respuestas:


54

Si todo lo que quiere hacer es crear la clase X con ciertos argumentos, la subclasificación es una forma extraña de expresar esa intención, porque no está utilizando ninguna de las características que le dan las clases y la herencia. No es realmente un antipatrón, es extraño y un poco inútil (a menos que tenga otras razones). Una forma más natural de expresar esta intención sería un Método Factory , que en este caso es un nombre elegante para su "método auxiliar".

Con respecto a la intuición general, tanto "más específico" como "un rango limitado" son formas potencialmente perjudiciales de pensar sobre las subclases, porque ambas implican que hacer de Square una subclase de rectángulo es una buena idea. Sin depender de algo formal como LSP, diría que una mejor intuición es que una subclase proporciona una implementación de la interfaz base o extiende la interfaz para agregar alguna funcionalidad nueva.


2
Sin embargo, los métodos de fábrica no le permiten aplicar ciertos comportamientos (gobernados por argumentos) en su base de código. Y a veces es útil anotar explícitamente que esta clase / método / campo toma SystemDataHelperBuilderespecíficamente.
Telastyn

3
Ah, creo que el argumento cuadrado versus rectángulo era exactamente lo que estaba buscando. ¡Gracias!
HardlyKnowEm

77
Por supuesto, tener un cuadrado como una subclase de rectángulo puede estar bien si son inmutables. Realmente tienes que mirar al LSP.
David Conrad

10
El problema del cuadrado / rectángulo es que las formas geométricas son inmutables. Siempre puede usar un cuadrado donde un rectángulo tenga sentido, pero cambiar el tamaño no es algo que hagan los cuadrados o los rectángulos.
Doval

3
@nha LSP = Principio de sustitución de Liskov
Ixrec

16

¿Cuál de estas intuiciones es correcta?

Su compañero de trabajo es correcto (suponiendo sistemas de tipo estándar).

Piénselo, las clases representan posibles valores legales. Si la clase Atiene un campo de byte F, es probable que piense que Atiene 256 valores legales, pero que la intuición es incorrecta. Aestá restringiendo "cada permutación de valores" a "debe tener un campo de F0-255".

Si extiende Acon el Bque tiene otro campo de byte FF, eso está agregando restricciones . En lugar de "valor donde Festá byte", tiene "valor donde Festá byte && FFtambién es byte". Dado que mantiene las viejas reglas, todo lo que funcionó para el tipo de base todavía funciona para su subtipo. Pero como está agregando reglas, se está especializando más lejos de "podría ser cualquier cosa".

O pensar en otra forma, asumir que Atenían un montón de subtipos: B, C, y D. Una variable de tipo Apodría ser cualquiera de estos subtipos, pero una variable de tipo Bes más específica. (En la mayoría de los sistemas de tipos) no puede ser a Co a D.

¿Es esto un antipatrón?

Enh? Tener objetos especializados es útil y no claramente perjudicial para el código. Lograr ese resultado mediante el uso de subtipos es quizás un poco cuestionable, pero depende de las herramientas que tenga a su disposición. Incluso con mejores herramientas, esta implementación es simple, directa y robusta.

No lo consideraría un antipatrón ya que no es claramente incorrecto ni malo en ninguna situación.


55
"Más reglas significan menos valores posibles, significa más específicos". Realmente no sigo esto. El tipo byte and bytedefinitivamente tiene más valores que solo el tipo byte; 256 veces más. Aparte de eso, no creo que puedas combinar reglas con varios elementos (o cardinalidad si quieres ser preciso). Se descompone cuando consideras conjuntos infinitos. ¡Hay tantos números pares como números enteros, pero saber que un número es par sigue siendo una restricción / me da más información que solo saber que es un número entero! Lo mismo podría decirse de los números naturales.
Doval

66
@Telastyn Nos estamos desviando del tema, pero es comprobable que la cardinalidad (libremente, "tamaño") del conjunto de enteros pares es la misma que el conjunto de todos los enteros, o incluso todos los números racionales. Es algo realmente genial. Ver math.grinnell.edu/~miletijo/museum/infinite.html
HardlyKnowEm

2
La teoría de conjuntos es bastante poco intuitiva. Puede poner los enteros y los pares en correspondencia uno a uno (por ejemplo, usando la función n -> 2n). Esto no siempre es posible; no puede asignar los enteros a los reales, por lo que el conjunto de reales es "más grande" que los enteros. Pero puede asignar el intervalo (real) [0, 1] al conjunto de todos los reales, para que tengan el mismo "tamaño". Los subconjuntos se definen como "cada elemento de Aes también un elemento de B" precisamente porque una vez que sus conjuntos se vuelven infinitos, puede darse el caso de que sean la misma cardinalidad pero tengan elementos diferentes.
Doval

1
Más relacionado con el tema en cuestión, el ejemplo que da es una restricción que, en términos de programación orientada a objetos, en realidad extiende -funcionalidad- en términos de un campo adicional. Estoy hablando de subclases que no extienden la funcionalidad, como el problema círculo-elipse.
HardlyKnowEm

1
@Doval: Hay más de un posible significado de "menos", es decir, más de una relación de ordenación en conjuntos. La relación "es un subconjunto de" proporciona un orden bien definido (parcial) en los conjuntos. Además, se puede considerar que "más reglas" significa "todos los elementos del conjunto satisfacen más (es decir, un superconjunto de) proposiciones (de un determinado conjunto)". Con estas definiciones, "más reglas" significa "menos valores". Este es, en términos generales, el enfoque adoptado por varios modelos semánticos de programas de computadora, por ejemplo, dominios Scott.
Salidas del

12

No, no es un antipatrón. Puedo pensar en varios casos de uso prácticos para esto:

  1. Si desea utilizar la comprobación del tiempo de compilación para asegurarse de que las colecciones de objetos solo se ajusten a una subclase particular. Por ejemplo, si tiene MySQLDaoy SqliteDaoen su sistema, pero por alguna razón desea asegurarse de que una colección solo contenga datos de una fuente, si usa subclases como lo describe, puede hacer que el compilador verifique esta corrección particular. Por otro lado, si almacena la fuente de datos como un campo, esto se convertiría en una verificación en tiempo de ejecución.
  2. Mi aplicación actual tiene una relación uno a uno entre AlgorithmConfiguration+ ProductClassy AlgorithmInstance. En otras palabras, si configura FooAlgorithmpara ProductClassen el archivo de propiedades, solo puede obtener uno FooAlgorithmpara esa clase de producto. La única forma de obtener dos FooAlgorithms para un dado ProductClasses crear una subclase FooBarAlgorithm. Esto está bien, porque hace que el archivo de propiedades sea fácil de usar (no hay necesidad de sintaxis para muchas configuraciones diferentes en una sección en el archivo de propiedades), y es muy raro que haya más de una instancia para una clase de producto determinada.
  3. La respuesta de @ Telastyn da otro buen ejemplo.

En pocas palabras: no hay realmente ningún daño en hacer esto. Un antipatrón se define como:

Un antipatrón (o antipatrón) es una respuesta común a un problema recurrente que generalmente es ineficaz y corre el riesgo de ser altamente contraproducente.

No hay riesgo de ser altamente contraproducente aquí, por lo que no es un antipatrón.


2
Sí, creo que si tuviera que formular la pregunta de nuevo, diría "código de olor". No es lo suficientemente malo como para justificar la advertencia de la próxima generación de desarrolladores jóvenes para evitarlo, pero es algo que si lo ve, puede indicar que hay un problema en el diseño general, pero también puede ser correcto.
HardlyKnowEm

El punto 1 está justo en el blanco. Realmente no entendí el punto 2, pero estoy seguro de que es igual de bueno ... :)
Phil

9

Si algo es un patrón o un antipatrón depende significativamente del idioma y el entorno en el que está escribiendo. Por ejemplo, los forbucles son un patrón en ensamblador, C y lenguajes similares y un antipatrón en lisp.

Déjame contarte una historia de algún código que escribí hace mucho tiempo ...

Estaba en un lenguaje llamado LPC e implementó un marco para lanzar hechizos en un juego. Tenías una superclase de hechizos, que tenía una subclase de hechizos de combate que manejaba algunos controles, y luego una subclase para hechizos de daño directo de un solo objetivo, que luego se subclasificó a los hechizos individuales: misil mágico, rayo y similares.

La naturaleza de cómo funcionaba LPC era que tenías un objeto maestro que era Singleton. antes de ir a 'eww Singleton' - era y no era "eww" en absoluto. Uno no hizo copias de los hechizos, sino que invocó los métodos estáticos (efectivamente) en los hechizos. El código para el misil mágico se veía así:

inherit "spells/direct";

void init() {
  ::init();
  damage = 10;
  cost = 3;
  delay = 1.0;
  caster_message = "You cast a magic missile at ${target}";
  target_message = "You are hit with a magic missile cast by ${caster}";
}

¿Y ves eso? Es una clase de constructor único. Estableció algunos valores que estaban en su clase abstracta principal (no, no debería usar setters, es inmutable) y eso fue todo. Funcionó bien . Fue fácil de codificar, fácil de extender y fácil de entender.

Este tipo de patrón se traduce en otras situaciones en las que tiene una subclase que extiende una clase abstracta y establece algunos valores propios, pero toda la funcionalidad está en la clase abstracta que hereda. Eche un vistazo a StringBuilder en Java para ver un ejemplo de esto, no solo del constructor, pero estoy presionado para encontrar mucha lógica en los métodos mismos (todo está en AbstractStringBuilder).

Esto no es algo malo, y ciertamente no es un antipatrón (y en algunos idiomas puede ser un patrón en sí mismo).


2

El uso más común de tales subclases en las que puedo pensar es en jerarquías de excepciones, aunque ese es un caso degenerado en el que normalmente no definiríamos nada en la clase, siempre que el lenguaje nos permita heredar constructores. Usamos la herencia para expresar que ReadErrores un caso especial de IOError, y así sucesivamente, pero ReadErrorno es necesario anular ningún método de IOError.

Hacemos esto porque se detectan excepciones al verificar su tipo. Por lo tanto, necesitamos codificar la especialización en el tipo para que alguien pueda atrapar solo ReadErrorsin atrapar todo IOError, si así lo desea.

Normalmente, verificar el tipo de cosas es terriblemente malo y tratamos de evitarlo. Si logramos evitarlo, no hay necesidad esencial de expresar especializaciones en el sistema de tipos.

Sin embargo, aún podría ser útil, por ejemplo, si el nombre del tipo aparece en la forma de cadena registrada del objeto: este es el lenguaje y posiblemente el marco específico. Se podría, en principio, sustituir el nombre del tipo en cualquier sistema con una llamada a un método de la clase, en cuyo caso estaríamos anular más que el constructor en SystemDataHelperBuilder, también nos sobrescribimos loggingname(). Entonces, si hay algún registro de este tipo en su sistema, podría imaginar que, aunque su clase literalmente solo anula al constructor, "moralmente" también está anulando el nombre de la clase, por lo que, a efectos prácticos, no es una subclase exclusiva del constructor. Tiene un comportamiento diferente deseable, '

Entonces, diría que su código puede ser una buena idea si hay algún código en otro lugar que de alguna manera verifica las instancias SystemDataHelperBuilder, ya sea explícitamente con una rama (como ramas de captura de excepción según el tipo) o tal vez porque hay algún código genérico que terminará usando el nombre SystemDataHelperBuilderde una manera útil (incluso si solo se trata de iniciar sesión).

Sin embargo, utilizando los tipos de casos especiales da lugar a cierta confusión, ya que si ImmutableRectangle(1,1)y ImmutableSquare(1)se comportan de manera diferente en modo alguno luego, eventualmente alguien va a extrañar por qué y tal vez desearía que no lo hizo. A diferencia de las jerarquías de excepción, verifica si un rectángulo es un cuadrado verificando si height == width, no con instanceof. En su ejemplo, hay una diferencia entre a SystemDataHelperBuildery a DataHelperBuildercreado con exactamente los mismos argumentos, y eso tiene el potencial de causar problemas. Por lo tanto, una función para devolver un objeto creado con los argumentos "correctos" me parece normalmente lo correcto: la subclase da como resultado dos representaciones diferentes de "lo mismo", y solo ayuda si está haciendo algo que usted normalmente trataría de no hacer.

Tenga en cuenta que estoy no hablar en términos de patrones de diseño y anti-patrones, porque yo no creo que es posible proporcionar una taxonomía de todas las buenas y malas ideas que un programador ha pensado nunca. Por lo tanto, no todas las ideas son un patrón o antipatrón, solo se convierte en eso cuando alguien identifica que ocurre repetidamente en diferentes contextos y lo nombra ;-) No conozco ningún nombre en particular para este tipo de subclases.


Pude ver usos para ImmutableSquareMatrix:ImmutableMatrix, siempre que hubiera un medio eficiente de pedirle a un ImmutableMatrixque presente su contenido como ImmutableSquareMatrixsi fuera posible. Incluso si ImmutableSquareMatrixera básicamente un ImmutableMatrixconstructor cuyo requería que la altura y el ancho coincidieran, y cuyo AsSquareMatrixmétodo simplemente se devolvería a sí mismo (en lugar de, por ejemplo, construir uno ImmutableSquareMatrixque envuelva la misma matriz de respaldo), dicho diseño permitiría la validación de parámetros en tiempo de compilación para métodos que requieren un cuadrado matrices
supercat

@supercat: buen punto, y en el ejemplo del interrogador si hay funciones que deben pasarse a SystemDataHelperBuilder, y no cualquiera DataHelperBuilderlo hará, entonces tiene sentido definir un tipo para eso (y una interfaz, suponiendo que maneje DI de esa manera) . En su ejemplo, hay algunas operaciones que una matriz cuadrada podría tener que una no cuadrada no tendría, como determinante, pero su punto es bueno que incluso si el sistema no necesita las que aún querría verificar por tipo .
Steve Jessop

... así que supongo que no es del todo cierto lo que dije, que "compruebas si un rectángulo es un cuadrado comprobando si height == width, no con instanceof". Es posible que prefiera algo que pueda afirmar estáticamente.
Steve Jessop

Desearía que hubiera un mecanismo que permitiera que algo como la sintaxis del constructor invoque métodos de fábrica estáticos. El tipo de expresión foo=new ImmutableMatrix(someReadableMatrix)es más claro que el de someReadableMatrix.AsImmutable()o incluso ImmutableMatrix.Create(someReadableMatrix), pero las últimas formas ofrecen posibilidades semánticas útiles que la primera carece; si el constructor puede decir que la matriz es cuadrada, ser capaz de reflejar su tipo que podría ser más limpio que requerir un código posterior que necesita una matriz cuadrada para crear una nueva instancia de objeto.
supercat

@supercat: Una pequeña cosa sobre Python que me gusta es la ausencia de un newoperador. Una clase es simplemente invocable, que cuando se llama devuelve (por defecto) una instancia de sí misma. Entonces, si desea preservar la consistencia de la interfaz, es perfectamente posible escribir una función de fábrica cuyo nombre comience con una letra mayúscula, por lo tanto, se parece a una llamada de constructor. No es que haga esto rutinariamente con las funciones de fábrica, pero está ahí cuando lo necesitas.
Steve Jessop

2

La definición más estricta orientada a objetos de una relación de subclase se conoce como «es-a». Usando el ejemplo tradicional de un cuadrado y un rectángulo, un rectángulo cuadrado «es-a».

Con esto, debe usar subclases para cualquier situación en la que sienta que un «es-a» es una relación significativa para su código.

En su caso específico de una subclase con nada más que un constructor, debe analizarlo desde una perspectiva «es-a». Está claro que un SystemDataHelperBuilder «es-a» DataHelperBuilder, por lo que el primer paso sugiere que es un uso válido.

La pregunta que debe responder es si alguno de los otros códigos aprovecha esta relación «es-a». ¿Algún otro código intenta distinguir SystemDataHelperBuilders de la población general de DataHelperBuilders? Si es así, la relación es bastante razonable, y debe mantener la subclase.

Sin embargo, si ningún otro código hace esto, entonces lo que tiene es una relación que podría ser una relación «es-a», o podría implementarse con una fábrica. En este caso, puede ir en cualquier dirección, pero recomendaría una Fábrica en lugar de una relación «es-a». Al analizar el código orientado a objetos escrito por otro individuo, la jerarquía de clases es una fuente importante de información. Proporciona las relaciones «es-a» que necesitarán para dar sentido al código. En consecuencia, poner este comportamiento en una subclase promueve el comportamiento de "algo que alguien encontrará si profundiza en los métodos de la superclase" a "algo que todos mirarán antes de comenzar a sumergirse en el código". Citando a Guido van Rossum, "el código se lee con más frecuencia de lo que se escribe" Por lo tanto, reducir la legibilidad de su código para los futuros desarrolladores es un precio muy alto. Yo elegiría no pagarlo, si pudiera usar una fábrica en su lugar.


1

Un subtipo nunca es "incorrecto" siempre que pueda reemplazar una instancia de su supertipo con una instancia del subtipo y que todo siga funcionando correctamente. Esto se verifica siempre que la subclase nunca intente debilitar ninguna de las garantías que ofrece el supertipo. Puede hacer garantías más fuertes (más específicas), por lo que en ese sentido la intuición de su compañero de trabajo es correcta.

Dado eso, la subclase que creó probablemente no esté equivocada (ya que no sé exactamente qué hacen, no puedo decirlo con certeza). Debe tener cuidado con el frágil problema de la clase base, y también debe considere si interfacele beneficiaría, pero eso es cierto para todos los usos de la herencia.


1

Creo que la clave para responder a esta pregunta es mirar este, un escenario de uso particular.

La herencia se usa para imponer las opciones de configuración de la base de datos, como la cadena de conexión.

Este es un anti patrón porque la clase está violando el Principio de Responsabilidad Única --- Se está configurando a sí mismo con una fuente específica de esa configuración y no "haciendo una cosa y haciéndolo bien". La configuración de una clase no debe integrarse en la clase misma. Para configurar las cadenas de conexión de la base de datos, la mayoría de las veces he visto que esto sucede a nivel de la aplicación durante algún tipo de evento que ocurre cuando se inicia la aplicación.


1

Utilizo subclases de constructor solo con bastante frecuencia para expresar diferentes conceptos.

Por ejemplo, considere la siguiente clase:

public class Outputter
{
    private readonly IOutputMethod outputMethod;
    private readonly IDataMassager dataMassager;

    public Outputter(IOutputMethod outputMethod, IDataMassager dataMassager)
    {
        this.outputMethod = outputMethod;
        this.dataMassager = dataMassager;
    }

    public MassageDataAndOutput(string data)
    {
        this.outputMethod.Output(this.dataMassager.Massage(data));
    }
}

Entonces podemos crear una subclase:

public class CapitalizeAndPrintOutputter : Outputter
{
    public CapitalizeAndPrintOutputter()
        : base(new PrintOutputMethod(), new Capitalizer())
    {
    }
}

Luego puede usar un CapitalizeAndPrintOutputter en cualquier parte de su código y sabe exactamente qué tipo de salida es. Además, este patrón hace que sea muy fácil usar un contenedor DI para crear esta clase. Si el contenedor DI conoce PrintOutputMethod y Capitalizer, incluso puede crear automáticamente CapitalizeAndPrintOutputter por usted.

Usar este patrón es realmente bastante flexible y útil. Sí, puede usar métodos de fábrica para hacer lo mismo, pero luego (al menos en C #) pierde parte de la potencia del sistema de tipos y, ciertamente, el uso de contenedores DI se vuelve más engorroso.


1

Permítanme señalar primero que a medida que se escribe el código, la subclase no es en realidad más específica que el padre, ya que los dos campos inicializados son configurables por el código del cliente.

Una instancia de la subclase solo se puede distinguir usando un downcast.

Ahora, si tiene un diseño en el que las propiedades DatabaseEnginey ConnectionStringfueron reimplementadas por la subclase, que accedió Configurationa hacerlo, entonces tiene una restricción que tiene más sentido tener la subclase.

Dicho esto, "antipatrón" es demasiado fuerte para mi gusto; No hay nada realmente malo aquí.


0

En el ejemplo que diste, tu pareja tiene razón. Los dos creadores de ayuda de datos son estrategias. Uno es más específico que el otro, pero ambos son intercambiables una vez inicializados.

Básicamente, si un cliente de datahelperbuilder recibiera el systemdatahelperbuilder, nada se rompería. Otra opción hubiera sido hacer que systemdatahelperbuilder sea una instancia estática de la clase datahelperbuilder.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.