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:
- 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?
- 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.
fuente
Respuestas:
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.
fuente
SystemDataHelperBuilder
específicamente.Su compañero de trabajo es correcto (suponiendo sistemas de tipo estándar).
Piénselo, las clases representan posibles valores legales. Si la clase
A
tiene un campo de byteF
, es probable que piense queA
tiene 256 valores legales, pero que la intuición es incorrecta.A
está restringiendo "cada permutación de valores" a "debe tener un campo deF
0-255".Si extiende
A
con elB
que tiene otro campo de byteFF
, eso está agregando restricciones . En lugar de "valor dondeF
está byte", tiene "valor dondeF
está byte &&FF
tambié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
A
tenían un montón de subtipos:B
,C
, yD
. Una variable de tipoA
podría ser cualquiera de estos subtipos, pero una variable de tipoB
es más específica. (En la mayoría de los sistemas de tipos) no puede ser aC
o aD
.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.
fuente
byte and byte
definitivamente tiene más valores que solo el tipobyte
; 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.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 deA
es también un elemento deB
" precisamente porque una vez que sus conjuntos se vuelven infinitos, puede darse el caso de que sean la misma cardinalidad pero tengan elementos diferentes.No, no es un antipatrón. Puedo pensar en varios casos de uso prácticos para esto:
MySQLDao
ySqliteDao
en 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.AlgorithmConfiguration
+ProductClass
yAlgorithmInstance
. En otras palabras, si configuraFooAlgorithm
paraProductClass
en el archivo de propiedades, solo puede obtener unoFooAlgorithm
para esa clase de producto. La única forma de obtener dosFooAlgorithm
s para un dadoProductClass
es crear una subclaseFooBarAlgorithm
. 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.En pocas palabras: no hay realmente ningún daño en hacer esto. Un antipatrón se define como:
No hay riesgo de ser altamente contraproducente aquí, por lo que no es un antipatrón.
fuente
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
for
bucles 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í:
¿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).
fuente
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
ReadError
es un caso especial deIOError
, y así sucesivamente, peroReadError
no es necesario anular ningún método deIOError
.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
ReadError
sin atrapar todoIOError
, 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 sobrescribimosloggingname()
. 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 nombreSystemDataHelperBuilder
de 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)
yImmutableSquare(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 siheight == width
, no coninstanceof
. En su ejemplo, hay una diferencia entre aSystemDataHelperBuilder
y aDataHelperBuilder
creado 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.
fuente
ImmutableSquareMatrix:ImmutableMatrix
, siempre que hubiera un medio eficiente de pedirle a unImmutableMatrix
que presente su contenido comoImmutableSquareMatrix
si fuera posible. Incluso siImmutableSquareMatrix
era básicamente unImmutableMatrix
constructor cuyo requería que la altura y el ancho coincidieran, y cuyoAsSquareMatrix
método simplemente se devolvería a sí mismo (en lugar de, por ejemplo, construir unoImmutableSquareMatrix
que 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 matricesSystemDataHelperBuilder
, y no cualquieraDataHelperBuilder
lo 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 .height == width
, no coninstanceof
". Es posible que prefiera algo que pueda afirmar estáticamente.foo=new ImmutableMatrix(someReadableMatrix)
es más claro que el desomeReadableMatrix.AsImmutable()
o inclusoImmutableMatrix.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.new
operador. 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.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.
fuente
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
interface
le beneficiaría, pero eso es cierto para todos los usos de la herencia.fuente
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.
fuente
Utilizo subclases de constructor solo con bastante frecuencia para expresar diferentes conceptos.
Por ejemplo, considere la siguiente clase:
Entonces podemos crear una subclase:
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.
fuente
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
DatabaseEngine
yConnectionString
fueron reimplementadas por la subclase, que accedióConfiguration
a 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í.
fuente
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.
fuente