¿En qué punto debería YAGNI tener prioridad sobre las buenas prácticas de codificación y viceversa? Estoy trabajando en un proyecto en el trabajo y quiero introducir lentamente buenos estándares de código para mis compañeros de trabajo (actualmente no hay ninguno y todo está pirateado sin ton ni son), pero después de crear una serie de clases (nosotros no haga TDD, o lamentablemente ningún tipo de prueba de unidad) Di un paso atrás y pensé que está violando YAGNI porque sé con certeza que no necesitamos la necesidad de extender algunas de estas clases.
Aquí hay un ejemplo concreto de lo que quiero decir: tengo una capa de acceso a datos que envuelve un conjunto de procedimientos almacenados, que utiliza un patrón de estilo de repositorio rudimentario con funciones CRUD básicas. Como hay un puñado de métodos que necesitan todas mis clases de repositorio, creé una interfaz genérica para mis repositorios, llamada IRepository
. Sin embargo, luego creé una interfaz "marcador" (es decir, una interfaz que no agrega ninguna funcionalidad nueva) para cada tipo de repositorio (por ejemplo ICustomerRepository
) y la clase concreta lo implementa. He hecho lo mismo con una implementación de Fábrica para construir los objetos comerciales de DataReaders / DataSets devueltos por el Procedimiento almacenado; la firma de mi clase de repositorio tiende a parecerse a esto:
public class CustomerRepository : ICustomerRepository
{
ICustomerFactory factory = null;
public CustomerRepository() : this(new CustomerFactory() { }
public CustomerRepository(ICustomerFactory factory) {
this.factory = factory;
}
public Customer Find(int customerID)
{
// data access stuff here
return factory.Build(ds.Tables[0].Rows[0]);
}
}
Mi preocupación aquí es que yo estoy violando YAGNI porque sé con 99% de certeza de que nunca va a ser una razón para dar más que un algo concreto CustomerFactory
a este repositorio; Como no tenemos pruebas unitarias, no necesito una MockCustomerFactory
o cosas similares, y tener tantas interfaces podría confundir a mis compañeros de trabajo. Por otro lado, usar una implementación concreta de la fábrica parece un olor a diseño.
¿Hay una buena manera de llegar a un compromiso entre el diseño de software adecuado y no sobreasignar la solución? Me pregunto si necesito tener todas las "interfaces de implementación única" o si podría sacrificar un poco de buen diseño y solo tener, por ejemplo, la interfaz base y luego el concreto único, y no preocuparme por programar interfaz si la implementación es que alguna vez se utilizará.
fuente
Respuestas:
YAGNI
Suposición falsa.
Eso no es un "sacrificio". Ese es un buen diseño.
fuente
En la mayoría de los casos, evitar el código que no va a necesitar conduce a un mejor diseño. El diseño más fácil de mantener y a prueba de futuro es el que utiliza la menor cantidad de código simple y bien nombrado que satisface los requisitos.
Los diseños más simples son los más fáciles de evolucionar. Nada mata la mantenibilidad como las capas de abstracción inutilizadas y sobredimensionadas.
fuente
YAGNI y SOLID (o cualquier otra metodología de diseño) no son mutuamente excluyentes. Sin embargo, son opuestos casi polares. No tiene que adherirse al 100% a ninguno de los dos, pero habrá algo de toma y daca; cuanto más miras un patrón altamente abstraído utilizado por una clase en un lugar, y dices YAGNI y lo simplificas, menos SÓLIDO se vuelve el diseño. Lo contrario también puede ser cierto; muchas veces en desarrollo, un diseño se implementa SOLIDAMENTE "por fe"; no ves cómo lo necesitarás, pero solo tienes una corazonada. Esto podría ser cierto (y es cada vez más probable que sea cierto cuanto más experiencia gane), pero también podría ponerle tanta deuda técnica como un enfoque de "hazlo ligero"; en lugar de una base de código de "código de espagueti" DIL, puede terminar con "código de lasaña", tener tantas capas que simplemente agregar un método o un nuevo campo de datos se convierte en un proceso de varios días de vadeo a través de servidores proxy de servicio y dependencias poco acopladas con una sola implementación. O podría terminar con el "código de ravioles", que viene en trozos tan pequeños que al moverse hacia arriba, hacia abajo, hacia la izquierda o hacia la derecha en la arquitectura lo lleva a través de 50 métodos con 3 líneas cada uno.
Lo he dicho en otras respuestas, pero aquí está: en la primera pasada, haz que funcione. En el segundo pase, que sea elegante. En el tercer pase, hazlo SÓLIDO.
Desglosando eso:
Cuando escribe una línea de código por primera vez, simplemente tiene que funcionar. En este punto, por lo que sabes, es único. Por lo tanto, no obtienes puntos de estilo para construir una arquitectura de "torre de marfil" para agregar 2 y 2. Haz lo que tienes que hacer y asume que nunca lo volverás a ver.
La próxima vez que su cursor vaya en esa línea de código, ahora ha refutado su hipótesis desde la primera vez que la escribió. Está volviendo a visitar ese código, probablemente ya sea para extenderlo o para usarlo en otro lugar, por lo que no es único. Ahora, se deben implementar algunos principios básicos como DRY (no se repita) y otras reglas simples para el diseño de código; extraiga métodos y / o bucles de forma para código repetido, extraiga variables para literales o expresiones comunes, tal vez agregue algunos comentarios, pero en general su código debe autodocumentarse. Ahora, su código está bien organizado, aunque todavía esté estrechamente acoplado, y cualquiera que lo vea puede aprender fácilmente lo que está haciendo leyendo el código, en lugar de rastrearlo línea por línea.
La tercera vez que el cursor ingresa ese código, probablemente sea un gran problema; lo está ampliando una vez más o se ha vuelto útil en al menos otros tres lugares diferentes en la base de código. En este punto, es un elemento clave, si no el núcleo, de su sistema, y debe ser diseñado como tal. En este punto, generalmente también tiene el conocimiento de cómo se ha utilizado hasta ahora, lo que le permitirá tomar buenas decisiones de diseño con respecto a cómo diseñar el diseño para simplificar esos usos y los nuevos. Ahora las reglas SÓLIDAS deberían entrar en la ecuación; extraiga clases que contengan código con propósitos específicos, defina interfaces comunes para cualquier clase que tenga propósitos o funcionalidades similares, configure dependencias sueltas entre clases y diseñe las dependencias de manera que pueda agregarlas, eliminarlas o intercambiarlas fácilmente.
A partir de este momento, si necesita ampliar, volver a implementar o reutilizar este código, todo está muy bien empaquetado y resumido en el formato de "recuadro negro" que todos conocemos y amamos; conéctelo donde más lo necesite o agregue una nueva variación en el tema como una nueva implementación de la interfaz sin tener que cambiar el uso de dicha interfaz.
fuente
En lugar de cualquiera de esos, prefiero WTSTWCDTUAWCROT?
(¿Qué es lo más simple que podemos hacer que es útil y podemos lanzar el jueves?)
Acrónimos más simples están en mi lista de cosas que hacer, pero no son una prioridad.
fuente
YAGNI y el buen diseño no son contradictorios. YAGNI trata sobre (no) apoyar necesidades futuras. Un buen diseño consiste en hacer transparente lo que hace su software en este momento y cómo lo hace.
¿Introducir una fábrica simplificará su código existente? Si no, no lo agregue. Si lo hace, por ejemplo, cuando agrega pruebas (¡lo que debe hacer!), Agréguelo.
YAGNI se trata de no agregar complejidad para admitir funciones futuras.
Un buen diseño consiste en eliminar la complejidad mientras se admiten todas las funciones actuales.
fuente
No están en conflicto, tus objetivos están equivocados.
¿Qué intentas lograr?
Desea escribir software de calidad y, para hacerlo, desea mantener su base de código pequeña y no tener problemas.
Ahora que llegamos a un conflicto, ¿cómo cubrimos todos los casos si no escribimos casos que no vamos a utilizar?
Así es como se ve tu problema.
(cualquiera interesado, esto se llama nubes de evaporación )
Entonces, ¿qué está impulsando esto?
¿Cuál de estos podemos resolver? Bueno, parece que no quiere perder el tiempo y el código hinchado es un gran objetivo, y tiene sentido. ¿Qué hay de ese primero? ¿Podemos averiguar qué vamos a necesitar para codificar?
Reformulemos todo eso
No necesita un compromiso, necesita a alguien para administrar el equipo que sea competente y tenga una visión de todo el proyecto. Necesitas a alguien que pueda planificar lo que vas a necesitar, en lugar de que cada uno de ustedes arroje cosas que NO vas a necesitar porque no estás seguro del futuro porque ... ¿por qué? Te diré por qué, es porque nadie tiene un maldito plan entre todos ustedes. Estás intentando incorporar estándares de código para solucionar un problema completamente diferente. Su problema PRIMARIO que necesita resolver es una hoja de ruta y un proyecto claros. Una vez que tenga eso, puede decir "los estándares de código nos ayudan a alcanzar este objetivo de manera más efectiva como equipo", lo cual es la verdad absoluta pero está fuera del alcance de esta pregunta.
Obtenga un gerente de proyecto / equipo que pueda hacer estas cosas. Si tiene uno, debe pedirles un mapa y explicarles el problema de YAGNI que no presenta un mapa. Si son extremadamente incompetentes, escriba el plan usted mismo y diga "aquí está mi informe para usted sobre las cosas que necesitamos, revíselo y háganos saber su decisión".
fuente
Permitir que su código para ser extendido con pruebas de unidad no va a ser cubiertos por YAGNI, porque usted va a necesitar. Sin embargo, no estoy convencido de que sus cambios de diseño en una interfaz de implementación única realmente estén aumentando la capacidad de prueba del código porque CustomerFactory ya hereda de una interfaz y de todos modos puede cambiarse por una MockCustomerFactory en cualquier momento.
fuente
La pregunta presenta un falso dilema. La aplicación adecuada del principio YAGNI no es algo sin relación. Es un aspecto del buen diseño. Cada uno de los principios SÓLIDOS también son aspectos del buen diseño. No siempre se puede aplicar completamente todos los principios en cualquier disciplina. Los problemas del mundo real ejercen muchas fuerzas en su código, y algunos de ellos empujan en direcciones opuestas. Los principios de diseño tienen que dar cuenta de todos ellos, pero ningún puñado de principios puede adaptarse a todas las situaciones.
Ahora, echemos un vistazo a cada principio con el entendimiento de que, si bien a veces pueden tomar diferentes direcciones, de ninguna manera están inherentemente en conflicto.
YAGNI fue concebido para ayudar a los desarrolladores a evitar un tipo particular de retrabajo: lo que viene de construir algo incorrecto. Lo hace al guiarnos para evitar tomar decisiones errantes demasiado pronto en base a suposiciones o predicciones sobre lo que creemos que cambiará o será necesario en el futuro. La experiencia colectiva nos dice que cuando hacemos esto, generalmente estamos equivocados. Por ejemplo, YAGNI le diría que no cree una interfaz con el propósito de reutilización , a menos que sepa en este momento que necesita múltiples implementadores. De manera similar, YAGNI diría que no cree un "ScreenManager" para administrar el formulario único en una aplicación a menos que sepa en este momento que tendrá más de una pantalla.
Al contrario de lo que mucha gente piensa, SOLID no se trata de reutilización, genérico o incluso abstracción. SOLID tiene la intención de ayudarlo a escribir código que esté preparado para el cambio , sin decir nada sobre cuál podría ser ese cambio específico. Los cinco principios de SOLID crean una estrategia para construir código que es flexible sin ser demasiado genérico y simple sin ser ingenuo. La aplicación adecuada del código SOLID produce clases pequeñas y enfocadas con roles y límites bien definidos. El resultado práctico es que para cualquier cambio de requisitos necesarios, se debe tocar un número mínimo de clases. Y de manera similar, para cualquier cambio de código, hay una cantidad minimizada de "ondulación" a través de otras clases.
Mirando la situación de ejemplo que tiene, veamos qué podrían decir YAGNI y SOLID. Está considerando una interfaz de repositorio común debido al hecho de que todos los repositorios se ven iguales desde el exterior. Pero el valor de una interfaz genérica común es la capacidad de usar cualquiera de los implementadores sin necesidad de saber cuál es en particular. A menos que haya algún lugar en su aplicación donde esto sea necesario o útil, YAGNI dice que no lo haga.
Hay 5 principios SÓLIDOS para ver. S es responsabilidad única. Esto no dice nada sobre la interfaz, pero podría decir algo sobre sus clases concretas. Se podría argumentar que manejar el acceso a los datos en sí mismo podría ser una responsabilidad de una o más clases, mientras que la responsabilidad de los repositorios es traducir de un contexto implícito (CustomerRepository es un repositorio implícito para las entidades del Cliente) en llamadas explícitas a la API de acceso a datos generalizada que especifica el tipo de entidad del Cliente.
O es abierto-cerrado. Esto se trata principalmente de herencia. Se aplicaría si intentara derivar sus repositorios de una base común que implementa una funcionalidad común, o si espera derivar más de los diferentes repositorios. Pero no lo eres, así que no.
L es la sustituibilidad de Liskov. Esto se aplica si tenía la intención de utilizar los repositorios a través de la interfaz de repositorio común. Establece restricciones en la interfaz y las implementaciones para garantizar la coherencia y evitar un manejo especial para diferentes impelementers. La razón de esto es que tal manejo especial socava el propósito de una interfaz. Puede ser útil considerar este principio, porque puede advertirle que no use la interfaz de repositorio común. Esto coincide con la guía de YAGNI.
I es la segregación de interfaz. Esto puede aplicarse si comienza a agregar diferentes operaciones de consulta a sus repositorios. La segregación de interfaz se aplica cuando puede dividir a los miembros de una clase en dos subconjuntos donde uno será utilizado por ciertos consumidores y el otro por otros, pero es probable que ningún consumidor use ambos subconjuntos. La guía es crear dos interfaces separadas, en lugar de una común. En su caso, es poco probable que la búsqueda y el almacenamiento de instancias individuales se consuman con el mismo código que haría consultas generales, por lo que podría ser útil separarlos en dos interfaces.
D es inyección de dependencia. Aquí volvemos al mismo punto que la S. Si separó su consumo de la API de acceso a datos en un objeto separado, este principio dice que, en lugar de simplemente actualizar una instancia de ese objeto, debe pasarlo cuando cree Un repositorio. Esto facilita el control de la vida útil del componente de acceso a datos, abriendo la posibilidad de compartir referencias a él entre sus repositorios, sin tener que seguir la ruta de convertirlo en un singleton.
Es importante tener en cuenta que la mayoría de los principios SOLID no se aplican necesariamente en esta etapa particular del desarrollo de su aplicación. Por ejemplo, si debe romper el acceso a los datos depende de lo complicado que sea, y si desea probar la lógica de su repositorio sin tocar la base de datos. Parece que esto es poco probable (desafortunadamente, en mi opinión), por lo que probablemente no sea necesario.
Entonces, después de toda esa consideración, descubrimos que YAGNI y SOLID realmente proporcionan una pieza común de consejos sólidos e inmediatamente relevantes: probablemente no sea necesario crear una interfaz de repositorio genérica común.
Todo este pensamiento cuidadoso es extremadamente útil como ejercicio de aprendizaje. A medida que aprende, lleva mucho tiempo, pero con el tiempo desarrolla la intuición y se vuelve muy rápido. Sabrá qué hacer, pero no necesita pensar todas estas palabras a menos que alguien le pida que explique por qué.
fuente
Parece creer que "buen diseño" significa seguir algún tipo de idealología y un conjunto formal de reglas que siempre deben aplicarse, incluso cuando no sirven.
OMI que es un mal diseño. YAGNI es un componente del buen diseño, nunca una contradicción.
fuente
En su ejemplo, diría que YAGNI debería prevalecer. No le costará tanto si necesita agregar interfaces más adelante. Por cierto, ¿es realmente un buen diseño tener una interfaz por clase si no sirve para nada?
Un pensamiento más, quizás a veces lo que necesita no es un buen diseño sino un diseño suficiente. Aquí hay una secuencia muy interesante de publicaciones sobre el tema:
fuente
Algunas personas argumentan que los nombres de las interfaces no deberían comenzar por I. Específicamente, una de las razones es que usted está filtrando la dependencia de si el tipo dado es una clase o una interfaz.
¿Qué le impide
CustomerFactory
ser una clase al principio y luego cambiarla a una interfaz, que será implementada porDefaultCustormerFactory
oUberMegaHappyCustomerPowerFactory3000
? Lo único que debería tener que cambiar es el lugar, donde se implementa la implementación. Y si tiene un diseño menos bueno, este es un puñado de lugares como máximo.La refactorización es parte del desarrollo. Es mejor tener un código pequeño, que sea fácil de refactorizar, que tener una interfaz y una clase declaradas para cada clase, lo que le obliga a cambiar cada nombre de método en al menos dos lugares al mismo tiempo.
El punto real en el uso de interfaces es lograr la modularidad, que es posiblemente el pilar más importante del buen diseño. Sin embargo, tenga en cuenta que un módulo no solo se define por su desacoplamiento del mundo exterior (aunque así es como lo percibimos desde la perspectiva exterior), sino también por su funcionamiento interno.
Lo que quiero decir es que las cosas desacopladas, que intrínsecamente van juntas, no tienen mucho sentido. En cierto modo, es como tener un armario con un estante individual por taza.
La importancia radica en idear un problema grande y complejo en subproblemas más pequeños y simples. Y debe detenerse en el punto, donde se vuelven lo suficientemente simples sin subdivisiones adicionales, de lo contrario, se volverán más complicados. Esto podría verse como un corolario de YAGNI. Y definitivamente significa buen diseño.
El objetivo no es resolver de alguna manera un problema local con un único repositorio y una sola fábrica. El objetivo es que esta decisión no tenga efecto en el resto de su aplicación. De eso se trata la modularidad.
Desea que sus compañeros de trabajo vean su módulo, vean una fachada con un puñado de llamadas autoexplicativas y se sientan seguros de que pueden usarlos, sin tener que preocuparse por todas las tuberías internas potencialmente sofisticadas.
fuente
Estás creando una
interface
anticipación de múltiples implementaciones en el futuro. También podría tener unI<class>
para cada clase en su código base. No lo hagasSolo use la clase de concreto individual de acuerdo con YAGNI. Si encuentra que necesita tener un objeto "simulado" para fines de prueba, convierta la clase original en una clase abstracta con dos implementaciones, una con la clase concreta original y la otra con la implementación simulada.
Obviamente, tendrá que actualizar todas las instancias de la clase original para crear una instancia de la nueva clase concreta. Puede evitar eso utilizando un constructor estático en la delantera.
YAGNI dice que no escriba código antes de que deba escribirse.
Un buen diseño dice que use la abstracción.
Puedes tener ambos. Una clase es una abstracción.
fuente
¿Por qué las interfaces del marcador? Me parece que no está haciendo nada más que "etiquetar". Con una "etiqueta" diferente para cada tipo de fábrica, ¿cuál es el punto?
El propósito de una interfaz es dar a una clase "actos como" comportamiento, para darles "capacidad de depósito", por así decirlo. Entonces, si todos sus tipos de repositorio concretos se comportan como el mismo IRepository (todos implementan IRepository), entonces todos pueden ser manejados de la misma manera por otro código, exactamente por el mismo código. En este punto, su diseño es extensible. Añadiendo más tipos de repositorios concretos, todos manejados como repositorios IR genéricos: el mismo código maneja todos los tipos concretos como repositorios "genéricos".
Las interfaces son para manejar cosas basadas en elementos comunes. Pero las interfaces de marcadores personalizados a) no agregan ningún comportamiento. yb) obligarlo a manejar su singularidad.
En la medida en que diseñe interfaces útiles, obtendrá la bondad OO de no tener que escribir código especializado para manejar clases especializadas, tipos o interfaces de marcador personalizadas que tengan una correlación 1: 1 con clases concretas. Eso es redundancia sin sentido.
De repente, puedo ver una interfaz de marcador si, por ejemplo, necesita una colección fuertemente tipada de muchas clases diferentes. En la colección, todos son "ImarkerInterface", pero a medida que los saca, tiene que convertirlos en sus tipos adecuados.
fuente
¿Puede, ahora mismo, escribir un ICustomerRepository vagamente razonable? Por ejemplo, (¿realmente llegando aquí, probablemente un mal ejemplo) en el pasado sus clientes siempre han usado PayPal? ¿O todos en la compañía están entusiasmados con la conexión con Alibaba? Si es así, es posible que desee utilizar el diseño más complejo ahora y que parezca clarividente a sus jefes. :-)
De lo contrario, espera. Adivinar en una interfaz antes de tener una o dos implementaciones reales generalmente falla. En otras palabras, no generalice / resuma / use un patrón de diseño elegante hasta que tenga un par de ejemplos para generalizar.
fuente