¿Seguir SOLID conduce a escribir un marco encima de la pila tecnológica?


70

Me gusta SOLID, y hago mi mejor esfuerzo para usarlo y aplicarlo cuando estoy desarrollando. Pero no puedo evitar sentir que el enfoque SOLID convierte su código en código 'marco', es decir, código que diseñaría si estuviera creando un marco o biblioteca para que otros desarrolladores lo utilicen.

En general, he practicado 2 modos de programación: crear más o menos exactamente lo que se solicita a través de requisitos y KISS (programación típica), o crear lógica, servicios, etc. genéricos y reutilizables que brindan la flexibilidad que otros desarrolladores pueden necesitar (programación marco) .

Si el usuario realmente solo quiere que una aplicación haga cosas xey, ¿tiene sentido seguir SOLID y agregar un montón de puntos de entrada de abstracción, cuando ni siquiera sabe si ese es un problema válido para comenzar? ¿con? Si agrega estos puntos de entrada de abstracción, ¿realmente está cumpliendo con los requisitos de los usuarios, o está creando un marco sobre su marco existente y la pila de tecnología para facilitar futuras adiciones? ¿En qué caso atiende los intereses del cliente o del desarrollador?

Esto es algo que parece común en el mundo de Java Enterprise, donde se siente como si estuvieras diseñando tu propio framework sobre J2EE o Spring para que sea un mejor UX para el desarrollador, en lugar de enfocarte en UX para el usuario.


12
El problema con la mayoría de las reglas breves de programación es que están sujetas a interpretación, casos extremos y, a veces, las definiciones de las palabras en tales reglas no están claras en una inspección más cercana. Básicamente pueden significar una gran variedad de cosas para diferentes personas. Tener un poco de pragmatismo no ideológico generalmente le permite a uno tomar decisiones más sabias.
Mark Rogers

1
Haces que parezca que seguir los principios SOLID de alguna manera implica una gran inversión, mucho trabajo extra. No lo hace, es prácticamente gratis. Y probablemente le ahorrará a usted u otra persona una gran inversión en el futuro porque hace que su código sea más fácil de mantener y extender. Sigues haciendo preguntas como "¿deberíamos hacer nuestra tarea o hacer feliz al cliente?" Estas no son compensaciones.
Martin Maat

1
@MartinMaat Creo que las formas más extremas de SOLID implican una gran inversión. Es decir. Software empresarial. Fuera del software empresarial, tendría muy pocas razones para abstraer su ORM, pila de tecnología o base de datos porque hay una muy buena posibilidad de que se quede con la pila elegida. En el mismo sentido, al vincularse a un marco particular, base de datos u ORM, está rompiendo principios SÓLIDOS porque está acoplado a su pila. Ese nivel de flexibilidad de SOLID no se requiere en la mayoría de los trabajos.
Igneous01

1
Ver también el efecto de plataforma interna .
Maxpm

1
Convertir la mayoría del código en algo parecido a un marco no suena del todo terrible. Solo se vuelve terrible si se sobredimensiona. Pero los marcos pueden ser mínimos y obstinados. No estoy seguro de si esto sería una consecuencia inevitable de seguir a SOLID, pero definitivamente es una posible consecuencia y, creo, que debería aceptar.
Konrad Rudolph

Respuestas:


84

Su observación es correcta, los principios SOLID están hechos en mi humilde opinión teniendo en cuenta bibliotecas reutilizables o código de marco. Cuando solo los sigue a ciegas, sin preguntar si tiene sentido o no, se arriesga a generalizar en exceso e invertir mucho más esfuerzo en su sistema de lo que probablemente sea necesario.

Esto es una compensación, y se necesita algo de experiencia para tomar las decisiones correctas sobre cuándo generalizar y cuándo no. Un posible enfoque para esto es apegarse al principio de YAGNI: no haga que su código sea SÓLIDO "por si acaso" o, para usar sus palabras: no

Proporcionar la flexibilidad que otros desarrolladores pueden necesitar

en su lugar, brinde la flexibilidad que otros desarrolladores realmente necesitan tan pronto como la necesiten , pero no antes.

Entonces, siempre que tenga una función o clase en su código, no está seguro de si podría reutilizarse, no lo ponga en su marco en este momento. Espere hasta que tenga un caso real de reutilización y refactorice a "Suficientemente sólido para ese caso". No implemente más capacidad de configuración (siguiendo el OCP), o puntos de entrada de abstracción (usando el DIP) en una clase tal como realmente necesita para el caso de reutilización real. Agregue la siguiente flexibilidad cuando el siguiente requisito para la reutilización esté realmente allí.

Por supuesto, esta forma de trabajar siempre requerirá cierta cantidad de refactorización en la base de código de trabajo existente. Es por eso que las pruebas automáticas son importantes aquí. Por lo tanto, hacer que su código sea lo suficientemente SÓLIDO desde el principio para que se pueda probar la unidad no es una pérdida de tiempo, y hacerlo no contradice a YAGNI. Las pruebas automáticas son un caso válido para la "reutilización del código", ya que el código en juego se usa tanto del código de producción como de las pruebas. Pero tenga en cuenta, solo agregue la flexibilidad que realmente necesita para que las pruebas funcionen, nada menos y nada más.

Esto es en realidad vieja sabiduría. Hace mucho tiempo, antes de que el término SÓLIDO se popularizara, alguien me dijo que antes de intentar escribir código reutilizable , deberíamos escribir código utilizable . Y sigo pensando que esta es una buena recomendación.


23
Puntos de contención adicionales: espere hasta que tenga 3 casos de uso en los que vea la misma lógica antes de refactorizar su código para su reutilización. Si comienza a refactorizar con 2 piezas, es fácil terminar en una situación en la que los requisitos cambiantes o un nuevo caso de uso terminan rompiendo la abstracción que hizo. También mantenga los refactores limitados a cosas con el mismo caso de uso: 2 componentes pueden tener el mismo código pero hacen cosas completamente diferentes, y si combina esos componentes, terminará vinculando esa lógica, lo que puede causar problemas más adelante.
Nzall

8
Generalmente estoy de acuerdo con esto, pero siento que está demasiado enfocado en aplicaciones "únicas": usted escribe el código, funciona, bien. Sin embargo, hay muchas aplicaciones con "soporte a largo plazo". Puede escribir un código y 2 años después, los requisitos comerciales cambian, por lo que debe ajustar el código. Para ese momento, muchos otros códigos podrían depender de él; en ese caso, los principios SOLID facilitarán el cambio.
R. Schmitz

3
"Antes de intentar escribir código reutilizable, debemos escribir código utilizable" - ¡Muy sabio!
Graham

10
Es probablemente la pena señalar que esperar hasta que haya un caso de uso real hará que su código SÓLIDO mejor , ya que trabajar en situaciones hipotéticas es muy difícil y es probable que mis-adivinar cuáles serán las necesidades futuras. Nuestro proyecto tiene una serie de casos en los que las cosas fueron diseñadas para ser SÓLIDAS y flexibles para las necesidades futuras ... excepto que las necesidades futuras resultaron ser cosas en las que nadie pensaba en ese momento, por lo que ambos necesitábamos refactorizar y teníamos una flexibilidad adicional. todavía no era necesario, lo que tenía que mantenerse frente a la refactorización o desecharse.
KRyan

2
Por lo general, también tendrá que escribir código comprobable, lo que generalmente significa tener una primera capa de abstracción para poder pasar de una implementación concreta a una de prueba.
Walfrat

49

Según mi experiencia, al escribir una aplicación, tienes tres opciones:

  1. Escribir código únicamente para cumplir con los requisitos,
  2. Escriba código genérico que anticipe los requisitos futuros, además de cumplir con los requisitos actuales,
  3. Escriba código que solo cumpla con los requisitos actuales, pero de una manera que sea fácil de cambiar más adelante para satisfacer otras necesidades.

En el primer caso, es común terminar con un código estrechamente acoplado que carece de pruebas unitarias. Claro que es rápido de escribir, pero es difícil de probar. Y es un verdadero dolor real cambiar más tarde cuando cambian los requisitos.

En el segundo caso, se gasta una gran cantidad de tiempo tratando de anticipar las necesidades futuras. Y con demasiada frecuencia esos requisitos futuros anticipados nunca se materializan. Este parece el escenario que estás describiendo. Es una pérdida de esfuerzo la mayor parte del tiempo y da como resultado un código innecesariamente complejo que todavía es difícil de cambiar cuando aparece un requisito que no se había previsto.

El último caso es el que apunto en mi opinión. Use TDD o técnicas similares para probar el código a medida que avanza y terminará con un código débilmente acoplado, que es fácil de modificar pero rápido de escribir. Y la cuestión es que, al hacer esto, usted naturalmente sigue muchos de los principios SÓLIDOS: clases y funciones pequeñas; interfaces y dependencias inyectadas. Y la Sra. Liskov también se mantiene contenta en general, ya que las clases simples con responsabilidades únicas rara vez incumplen su principio de sustitución.

El único aspecto de SOLID que realmente no se aplica aquí es el principio abierto / cerrado. Para bibliotecas y marcos, esto es importante. Para una aplicación autónoma, no tanto. Realmente se trata de escribir código que sigue a " SLID ": fácil de escribir (y leer), fácil de probar y fácil de mantener.


¡Una de mis respuestas favoritas en este sitio!
TheCatWhisperer

No estoy seguro de cómo concluir que 1) es más difícil de probar que 3). Más difícil hacer cambios, claro, pero ¿por qué no puedes probar? En todo caso, una pieza de software de una sola mente es más fácil de probar contra los requisitos que una más general.
Sr. Lister

@MrLister Los dos van de la mano, 1. es más difícil de probar que 3. porque la definición implica que no está escrito "de una manera que sea fácil de cambiar más adelante para satisfacer otras necesidades".
Mark Booth

1
+0; En mi humilde opinión, está malinterpretando (aunque de manera común) la forma en que funciona 'O' (abierto-cerrado). Consulte, por ejemplo, codeblog.jonskeet.uk/2013/03/15/… - incluso en bases de código pequeñas, se trata más de tener unidades de código autónomas (por ejemplo, clases, módulos, paquetes, lo que sea), que se pueden probar de forma aislada y agregar / eliminado a medida que surge la necesidad. Un ejemplo de ello sería un paquete de métodos de utilidad: independientemente de la forma en que los agrupe, deben estar "cerrados", es decir, ser autónomos y "abiertos", es decir, ampliables de alguna manera.
vaxquis

Por cierto, incluso el tío Bob dice lo mismo en un punto: "Lo que significa [abierto-cerrado] es que debes esforzarte por colocar tu código en una posición tal que, cuando el comportamiento cambie de la manera esperada, no tengas que hacer un barrido cambios a todos los módulos del sistema. Idealmente, podrá agregar el nuevo comportamiento agregando un nuevo código y cambiando poco o ningún código antiguo ”. <- esto obviamente se aplica incluso a aplicaciones pequeñas, si están destinadas a ser modificadas o reparadas (y, IMVHO, ese suele ser el caso, especialmente cuando se trata de la risa de las soluciones )
vaxquis

8

La perspectiva que tiene puede verse sesgada por la experiencia personal. Esta es una pendiente resbaladiza de hechos que son correctos individualmente, pero la inferencia resultante no lo es, a pesar de que parece correcto a primera vista.

  • Los marcos tienen un alcance mayor que los proyectos pequeños.
  • La mala práctica es significativamente más difícil de tratar en bases de código más grandes.
  • La construcción de un marco (en promedio) requiere un desarrollador más calificado que la construcción de un proyecto pequeño.
  • Los mejores desarrolladores siguen más las buenas prácticas (SOLIDAS).
  • Como resultado, los marcos tienen una mayor necesidad de buenas prácticas y tienden a ser desarrollados por desarrolladores que tienen más experiencia con las buenas prácticas.

Esto significa que cuando interactúa con marcos y bibliotecas más pequeñas, el código de buenas prácticas con el que interactuará se encontrará más comúnmente en los marcos más grandes.

Esta falacia es muy común, por ejemplo, todos los médicos que me han tratado fueron arrogantes. Por lo tanto, concluyo que todos los médicos son arrogantes. Estas falacias siempre sufren de hacer una inferencia general basada en experiencias personales.

En su caso, es posible que haya experimentado predominantemente buenas prácticas en marcos más grandes y no en bibliotecas más pequeñas. Su observación personal no es incorrecta, pero es evidencia anecdótica y no aplicable universalmente.


2 modos de programación: crear más o menos exactamente lo que se solicita a través de los requisitos y KISS (programación típica), o crear lógica, servicios, etc. genéricos y reutilizables que brindan la flexibilidad que otros desarrolladores pueden necesitar (programación marco)

Estás confirmando esto aquí. Piensa en qué es un marco. No es una aplicación. Es una "plantilla" generalizada que otros pueden usar para hacer todo tipo de aplicaciones. Lógicamente, eso significa que un marco está construido en una lógica mucho más abstracta para que todos puedan usarlo.

Los creadores de framework no pueden tomar atajos porque ni siquiera saben cuáles son los requisitos de las aplicaciones posteriores. Construir un marco de trabajo los incentiva inherentemente a hacer que su código sea utilizable para otros.

Sin embargo, los creadores de aplicaciones tienen la capacidad de comprometer la eficiencia lógica porque están enfocados en entregar un producto. Su objetivo principal no es el funcionamiento del código sino la experiencia del usuario.

Para un marco, el usuario final es otro desarrollador, que interactuará con su código. La calidad de su código es importante para su usuario final.
Para una aplicación, el usuario final no es desarrollador y no interactuará con su código. La calidad de su código no es importante para ellos.

Esto es exactamente por qué los arquitectos de un equipo de desarrollo a menudo actúan como ejecutores de buenas prácticas. Están a un paso de entregar el producto, lo que significa que tienden a mirar el código de manera objetiva, en lugar de enfocarse en la entrega de la aplicación misma.


Si agrega estos puntos de entrada de abstracción, ¿realmente está cumpliendo con los requisitos de los usuarios, o está creando un marco sobre su marco existente y la pila de tecnología para facilitar futuras adiciones? ¿En qué caso atiende los intereses del cliente o del desarrollador?

Este es un punto interesante, y es (en mi experiencia) la razón principal por la cual las personas todavía tratan de justificar evitar las buenas prácticas.

Para resumir los siguientes puntos: omitir las buenas prácticas solo puede justificarse si sus requisitos (como se conocen actualmente) son inmutables, y nunca habrá ningún cambio / adición a la base de código. Alerta de spoiler: rara vez es así.
Por ejemplo, cuando escribo una aplicación de consola de 5 minutos para procesar un archivo en particular, no utilizo las buenas prácticas. Debido a que solo voy a usar la aplicación hoy, y no necesita actualizarse en el futuro (sería más fácil escribir una aplicación diferente en caso de que necesite otra).

Supongamos que puede compilar una aplicación de manera inadecuada en 4 semanas, y puede compilarla adecuadamente en 6 semanas. A primera vista, de mala calidad construirlo parece mejor. El cliente obtiene su solicitud más rápido y la empresa tiene que dedicar menos tiempo a los salarios de los desarrolladores. Ganar / ganar, ¿verdad?

Sin embargo, esta es una decisión tomada sin pensar en el futuro. Debido a la calidad de la base de código, realizar un cambio importante en el construido de manera inadecuada tomará 2 semanas, mientras que realizar los mismos cambios en el construido correctamente lleva 1 semana. Puede haber muchos de estos cambios en el futuro.

Además, hay una tendencia a que los cambios requieran inesperadamente más trabajo del que inicialmente pensabas en bases de código construidas de forma inadecuada, lo que probablemente aumenta el tiempo de desarrollo a tres semanas en lugar de dos.

Y luego también está la tendencia a perder el tiempo buscando insectos. Este suele ser el caso en los proyectos en los que se ha ignorado el registro debido a limitaciones de tiempo o la falta de voluntad para implementarlo porque distraídamente trabajas asumiendo que el producto final funcionará como se esperaba.

Ni siquiera necesita ser una actualización importante. En mi empleador actual, he visto varios proyectos que se construyeron de manera rápida y sucia, y cuando el más pequeño error / cambio tuvo que realizarse debido a una falta de comunicación en los requisitos, eso condujo a una reacción en cadena de la necesidad de refactorizar módulo tras módulo . Algunos de estos proyectos terminaron colapsando (y dejando un desastre imposible de mantener) incluso antes de que lanzaran su primera versión.

Las decisiones de acceso directo (programación rápida y sucia) solo son beneficiosas si puede garantizar de manera concluyente que los requisitos son exactamente correctos y que nunca será necesario cambiarlos. En mi experiencia, nunca me he encontrado con un proyecto donde eso sea cierto.

Invertir el tiempo extra en buenas prácticas es invertir en el futuro. Los errores y cambios futuros serán mucho más fáciles cuando la base de código existente se base en buenas prácticas. Ya estará pagando dividendos después de que solo se realicen dos o tres cambios.


1
Esta es una buena respuesta, pero debo aclarar que no estoy diciendo que abandonemos las buenas prácticas, pero ¿qué nivel de 'buenas prácticas' buscamos? ¿Es una buena práctica abstraer su ORM en cada proyecto porque 'podría' necesitar cambiarlo por otro más tarde? No lo creo, hay ciertos niveles de acoplamiento que estoy dispuesto a aceptar (es decir, estoy vinculado al marco, el lenguaje, ORM, la base de datos que se eligió). Si seguimos a SOLID en su grado extremista, ¿realmente estamos implementando nuestro propio marco en la parte superior de la pila elegida?
Igneous01

Estás negando la experiencia de OP como "falacia". No constructivo
max630

@ max630 No lo estoy negando. Pasé buena parte de la respuesta explicando por qué las observaciones de OP son válidas.
Flater

1
@ Igneous01 SOLID no es un marco. SOLID es una abstracción, que se encuentra más comúnmente en un marco. Al implementar cualquier tipo de abstracción (incluido SOLID), siempre hay una línea de razonabilidad. No se puede simplemente abstraer por el bien de la abstracción, pasaría años creando un código tan generalizado que es difícil de seguir. Solo abstraiga lo que sospeche razonablemente le será útil en el futuro. Sin embargo, no caiga en la trampa de asumir que está atado, por ejemplo, a su servidor de base de datos actual. Nunca se sabe qué nueva base de datos se lanzará mañana.
Flater

@ Igneous01 En otras palabras, tienes la idea correcta al no querer abstraer todo, pero tengo la sensación de que te estás inclinando demasiado en esa dirección. Es muy común que los desarrolladores asuman que los requisitos actuales están establecidos en piedra, y luego tomen decisiones arquitectónicas basadas en ese supuesto (ilusorio).
Flater

7

¿Cómo convierte SOLID el código simple en código marco? No soy un stan para SOLID de ninguna manera, pero realmente no es obvio lo que quieres decir aquí.

  • KISS es la esencia de la S Principio de Responsabilidad ingle.
  • No hay nada en el O Pen / Principio Cerrado (al menos tal como lo entiendo, vea Jon Skeet ) que va en contra de escribir código para hacer una cosa bien. (De hecho, cuanto más enfocado está el código, más importante se vuelve la parte "cerrada").
  • El Principio de sustitución de L iskov no dice que deba dejar que las personas subclasifiquen sus clases. Se dice que si usted subclase sus clases, subclases deben cumplir con el contrato de sus superclases. Eso es simplemente un buen diseño OO. (Y si no tiene ninguna subclase, no se aplica).
  • KISS es también la esencia de la I Nterface Segregación Principio.
  • El D ependency Inversion principio es el único que puede ver la aplicación de forma remota, pero creo que es ampliamente incomprendida y exagerada. No significa que tenga que inyectar todo con Guice o Spring. Simplemente significa que debe hacer un resumen donde corresponda y no depender de los detalles de implementación.

Admito que yo mismo no pienso en términos SÓLIDOS, porque llegué a través de las escuelas de programación Gang of Four y Josh Bloch , no de la escuela Bob Martin. Pero realmente creo que si piensas que “SOLIDO” = “agregar más capas a la pila tecnológica”, lo estás leyendo mal.


PD: No venda los beneficios de "mejor experiencia de usuario para el desarrollador" corto. Code pasa la mayor parte de su vida en mantenimiento. Un desarrollador eres tú .


1
Con respecto a SRP, se podría argumentar que cualquier clase con un constructor viola SRP, porque puede descargar esa responsabilidad a una fábrica. Con respecto a OCP: este es realmente un problema de nivel de marco, porque una vez que publica una interfaz para consumo para uso externo, no puede modificarla. Si la interfaz solo se usa dentro de su proyecto, entonces es posible cambiar el contrato, porque tiene el poder de cambiar el contrato dentro de su propio código. Con respecto al ISP, se podría argumentar que se debe definir una interfaz para cada acción individual (preservando así el SRP) y las preocupaciones de los usuarios externos.
Igneous01

3
1) uno podría, pero dudo que alguien que valga la pena escuchar lo haya hecho alguna vez. 2) se sorprenderá de lo rápido que puede crecer un proyecto a un tamaño que modificar libremente las interfaces internas se convierte en una mala idea. 3) ver 1) y 2). Basta decir que creo que estás leyendo demasiado en los tres principios. Pero los comentarios no son realmente el lugar para abordar estos argumentos; Le sugiero que plantee cada una de ellas como una pregunta separada y vea qué tipo de respuestas obtiene.
David Moles

44
@ Igneous01 Usando esa lógica, también podría abandonar los captadores y establecedores, ya que puede hacer una clase separada para cada establecedor de variables y una para cada captador. IE: class A{ int X; int Y; } class A_setX{ f(A a, int N) { a.X = N; }} class A_getX{ int f(A a) { return X; }} class A_setY ... etc.Creo que lo estás viendo desde un punto de vista demasiado meta con tu reclamo de fábrica. La inicialización no es un aspecto del problema del dominio.
Aaron

@Aaron esto. Las personas pueden usar SOLID para hacer malos argumentos, pero eso no significa hacer cosas malas = "seguir a SOLID".
David Moles
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.