SÓLIDO versus evitar la abstracción prematura

27

Entiendo lo que se supone que SOLID debe lograr y lo uso regularmente en situaciones donde la modularidad es importante y sus objetivos son claramente útiles. Sin embargo, dos cosas me impiden aplicarlo consistentemente en mi base de código:

  • Quiero evitar la abstracción prematura. En mi experiencia, dibujar líneas de abstracción sin casos de uso concretos (del tipo que existe ahora o en el futuro previsible ) lleva a que se dibujen en los lugares equivocados. Cuando intento modificar dicho código, las líneas de abstracción se interponen en lugar de ayudar. Por lo tanto, tiendo a equivocarme al no dibujar líneas de abstracción hasta tener una buena idea de dónde serían útiles.

  • Me resulta difícil justificar el aumento de la modularidad por sí mismo si hace que mi código sea más detallado, más difícil de entender, etc. y no elimina ninguna duplicación. Encuentro que el código de procedimiento o de objeto de Dios simple y estrechamente acoplado a veces es más fácil de entender que el código de ravioles muy bien factorizado porque el flujo es simple y lineal. También es mucho más fácil escribir.

Por otro lado, esta mentalidad a menudo conduce a objetos de Dios. Generalmente los refactorizo ​​de manera conservadora, agregando líneas de abstracción claras solo cuando veo que surgen patrones claros. ¿Qué hay de malo en los objetos de Dios y el código estrechamente acoplado, si es que no necesita más modularidad, no tiene una duplicación significativa y el código es legible?

EDITAR: En cuanto a los principios SÓLIDOS individuales, quise enfatizar que la sustitución de Liskov es en mi humilde opinión una formalización del sentido común y debe aplicarse en todas partes, ya que las abstracciones no tienen sentido si no lo es. Además, cada clase debe tener una responsabilidad única en algún nivel de abstracción, aunque puede ser un nivel muy alto con los detalles de implementación agrupados en una gran clase de 2,000 líneas. Básicamente, tus abstracciones deberían tener sentido donde elijas abstraer. Los principios que cuestiono en los casos en los que la modularidad no es claramente útil son los abiertos y cerrados, la segregación de la interfaz y especialmente la inversión de dependencia, ya que se trata de la modularidad, no solo de que las abstracciones tengan sentido.

dsimcha
fuente
10
@ S.Lott: La palabra clave allí es "más". Este es precisamente el tipo de cosas para las que se inventó YAGNI. Aumentar la modularidad o disminuir el acoplamiento simplemente por su propio bien es solo una programación de culto a la carga.
Mason Wheeler
3
@ S.Lott: Eso debería ser obvio por el contexto. Al leerlo, al menos, "más de lo que existe actualmente en el código en cuestión".
Mason Wheeler
3
@ S.Lott: Si insiste en ser pedante, "más" se refiere a más de lo que existe actualmente. La implicación es que algo, ya sea código o un diseño aproximado, ya existe y está pensando en cómo refactorizarlo.
dsimcha
55
@ back2dos: Correcto, pero soy escéptico de diseñar algo para la extensibilidad sin una idea clara de cómo es probable que se extienda. Sin esta información, sus líneas de abstracción probablemente terminarán en todos los lugares equivocados. Si no sabe dónde probablemente serán útiles las líneas de abstracción, le sugiero que simplemente escriba el código más conciso que funcione y sea legible, incluso si tiene algunos objetos de Dios.
dsimcha
2
@dsimcha: Tienes suerte si los requisitos cambian después de que escribiste un código, porque generalmente cambian mientras lo haces. Es por eso que las implementaciones de instantáneas no son adecuadas para sobrevivir un ciclo de vida real del software. Además, SOLID no exige que solo resumas a ciegas. El contexto para una abstracción se define a través de la inversión de dependencia. Reduce la dependencia de cada módulo a la abstracción más simple y clara de otros servicios en los que se basa. No es arbitrario, artificial o complejo, sino bien definido, plausible y simple.
back2dos

Respuestas:

8

Los siguientes son principios simples que puede aplicar para ayudarlo a comprender cómo equilibrar el diseño de su sistema:

  • Principio de responsabilidad única: el Sen SÓLIDO. Puede tener un objeto muy grande en términos de número de métodos o la cantidad de datos y aún mantener este principio. Tomemos por ejemplo el objeto Ruby String. Este objeto tiene más métodos de los que puede sacudir un palo, pero todavía tiene una sola responsabilidad: mantener una cadena de texto para la aplicación. Tan pronto como su objeto comience a asumir una nueva responsabilidad, comience a pensar mucho en eso. El problema de mantenimiento es preguntarse "¿dónde esperaría encontrar este código si tuviera problemas con él más adelante?"
  • La simplicidad cuenta: Albert Einstein dijo "Haz todo lo más simple posible, pero no más simple". ¿Realmente necesitas ese nuevo método? ¿Puede lograr lo que necesita con la funcionalidad existente? Si realmente cree que debe haber un nuevo método, ¿puede cambiar uno existente para satisfacer todo lo que necesita? En esencia, refactoriza a medida que construyes nuevas cosas.

En esencia, está tratando de hacer lo que puede para no dispararse en el pie cuando llega el momento de mantener el software. Si sus objetos grandes son abstracciones razonables, entonces hay pocas razones para dividirlos solo porque a alguien se le ocurrió una métrica que dice que una clase no debe ser más que X líneas / métodos / propiedades / etc. En todo caso, esas son pautas y no reglas duras y rápidas.

Berin Loritsch
fuente
3
Buen comentario. Como se discutió en otras respuestas, un problema con la "responsabilidad única" es que la responsabilidad de una clase puede describirse en una variedad de niveles de abstracción.
dsimcha
5

Creo que en su mayor parte has respondido tu propia pregunta. SOLID es un conjunto de pautas sobre cómo refactorizar su código, cuando los conceptos deben subir niveles de abstracciones.

Como todas las otras disciplinas creativas, no hay absolutos, solo compensaciones. Cuanto más lo haga, "más fácil" será decidir cuándo es suficiente para el dominio o los requisitos del problema actual.

Una vez dicho esto, la abstracción es el corazón del desarrollo de software, por lo que si no hay una buena razón para no hacerlo, la práctica lo hará mejor y obtendrá una idea intuitiva de las compensaciones. Así que favorezca a menos que se vuelva perjudicial.

Stephen Bailey
fuente
5
I want to avoid premature abstraction.In my experience drawing abstraction lines without concrete use cases... 

Esto es parcialmente correcto y parcialmente incorrecto.

Incorrecto
Esta no es una razón para evitar que uno aplique los principios OO / SÓLIDO. Solo evita aplicarlos demasiado temprano.

Cual es...

Derecha
No refactorice el código hasta que sea refactible; cuando se trata de requisitos completos. O, cuando los "casos de uso" están todos allí como usted dice.

I find simple, tightly coupled procedural or God object code is sometimes easier to understand than very well-factored...

Cuando se trabaja solo o en un silo
El problema de no hacer OO no es inmediatamente obvio. Después de escribir la primera versión de un programa:

Code is fresh in your head
Code is familiar
Code is easy to mentally compartmentalize
You wrote it

3/4 de estas cosas buenas mueren rápidamente en poco tiempo.

Finalmente, reconoce que tiene objetos de Dios (objetos con muchas funciones), por lo que reconoce funciones individuales. Encapsular. Póngalos en carpetas. Use interfaces de vez en cuando. Hazte un favor para el mantenimiento a largo plazo. Especialmente porque los métodos no refactorizados tienden a hincharse infinitamente y convertirse en métodos de Dios.

En un equipo
Los problemas de no hacer OO se notan de inmediato. Un buen código OO es al menos algo autodocumentable y legible. Especialmente cuando se combina con un buen código verde. Además, la falta de estructura dificulta la separación de tareas y la integración es mucho más compleja.

P.Brian.Mackey
fuente
Correcto. Reconozco vagamente que mis objetos hacen más de una cosa, pero no reconozco más concretamente cómo separarlos de manera útil para que las líneas de abstracción estén en los lugares correctos cuando sea necesario realizar el mantenimiento. Parece una pérdida de tiempo refactorizar si el programador de mantenimiento probablemente tendrá que refactorizar mucho más para obtener las líneas de abstracción en el lugar correcto de todos modos.
dsimcha
La encapsulación y la abstracción son dos conceptos diferentes. La encapsulación es un concepto básico de OO que básicamente significa que tienes clases. Las clases proporcionan modularidad y legibilidad en sí mismas. Eso es útil
P.Brian.Mackey
La abstracción puede disminuir el mantenimiento cuando se aplica correctamente. Aplicado incorrectamente puede aumentar el mantenimiento. Saber cuándo y dónde trazar las líneas requiere una sólida comprensión del negocio, el marco y el cliente, además de los aspectos técnicos básicos de cómo "aplicar un patrón de fábrica" ​​per se.
P.Brian.Mackey
3

No hay nada malo con los objetos grandes y el código estrechamente acoplado cuando son apropiados y cuando no se requiere nada mejor diseñado . Esta es solo otra manifestación de una regla general que se convierte en dogma.

El acoplamiento flojo y los objetos pequeños y simples tienden a proporcionar beneficios específicos en muchos casos comunes, por lo que es una buena idea usarlos, en general. El problema radica en que las personas que no entienden la lógica detrás de los principios tratan ciegamente de aplicarlos universalmente, incluso cuando no se aplican.

Mason Wheeler
fuente
1
+1. Todavía necesito una buena definición de lo que significa "apropiado".
jprete
2
@jprete: ¿Por qué? Intentar aplicar definiciones absolutas es parte de la causa de problemas conceptuales como este. Hay mucha artesanía involucrada en la construcción de un buen software. Lo que realmente se necesita de la OMI es experiencia y buen juicio.
Mason Wheeler
44
Bueno, sí, pero una definición amplia de algún tipo sigue siendo útil.
jprete
1
Sería útil tener una definición de apropiado, pero ese es el punto. Es difícil precisar un fragmento de sonido porque solo es aplicable caso por caso. Si toma la decisión adecuada en cada caso, depende en gran medida de la experiencia. Si no puede articular por qué es su solución de alta cohesión, la solución monolítica es mejor que una solución de baja cohesión y altamente modular, "probablemente" sería mejor ir con baja cohesión y modular. Siempre es más fácil refactorizar.
Ian
1
Prefiero que alguien escriba ciegamente código desacoplado en lugar de escribir ciegamente código de espagueti :)
Boris Yankov
2

Tiendo a abordar esto más desde el punto de vista de "No lo vas a necesitar". Esta publicación se centrará en un elemento en particular: la herencia.

En mi diseño inicial creo una jerarquía de cosas que sé que habrá dos o más. Es probable que necesiten mucho del mismo código, por lo que vale la pena diseñarlo desde el principio. Después de que el código inicial está en su lugar , y necesito agregar más características / funcionalidad, miro lo que tengo y pienso: "¿Hay algo que ya haya implementado la misma función o similar?" Si es así, es muy probable que se trate de una nueva abstracción que pide ser lanzada.

Un buen ejemplo de esto es un marco MVC. Comenzando desde cero, creas una página grande con un código detrás. Pero luego quieres agregar otra página. Sin embargo, el código subyacente ya implementa mucha de la lógica que requiere la nueva página. Por lo tanto, abstrae una Controllerclase / interfaz que implementará la lógica específica de esa página, dejando atrás las cosas comunes en el código original "dios".

Michael K
fuente
1

Si USTED sabe como desarrollador cuándo NO aplicar los principios debido al futuro del código, entonces es bueno para usted.

Espero que SOLID permanezca en su mente para saber cuándo deben abstraerse las cosas para evitar la complejidad y mejorar la calidad del código si comienza a degradarse en los valores que usted indicó.

Sin embargo, lo que es más importante, considere que la mayoría de los desarrolladores están haciendo el trabajo diario y no les importa tanto como a usted. Si inicialmente estableció métodos de ejecución prolongada, ¿qué pensarán los otros desarrolladores que entran en el código cuando lo mantengan o lo extiendan? ... Sí, lo adivinó, funciones MÁS GRANDES, clases de ejecución MÁS LARGAS y MÁS Dios se opone, y no tienen los principios en mente para poder captar el código creciente y encapsularlo correctamente. Su código "legible" ahora se convierte en un desastre podrido.

Por lo tanto, puede argumentar que un mal desarrollador lo hará independientemente, pero al menos tiene una mejor ventaja si las cosas se mantienen simples y bien organizadas desde el principio.

No me importa particularmente un "Mapa del Registro" global o un "Objeto de Dios" si son simples. Realmente depende del diseño del sistema, a veces puede salirse con la suya y seguir siendo simple, a veces no puede.

Tampoco olvidemos que las funciones grandes y los Objetos de Dios pueden resultar extremadamente difíciles de probar. Si desea permanecer ágil y sentirse "seguro" al volver a factorizar y cambiar el código, necesita pruebas. Las pruebas son difíciles de escribir cuando las funciones u objetos hacen muchas cosas.

Martin Blore
fuente
1
Básicamente una buena respuesta. Mi única queja es que, si algún desarrollador de mantenimiento aparece y hace un desastre, es su culpa por no refactorizar, a menos que tales cambios parezcan lo suficientemente previsibles como para justificar la planificación de ellos o el código esté tan mal documentado / probado / etc. . esa refactorización habría sido una forma de locura.
dsimcha
Eso es verdad dsimcha. Es solo que pensé en un sistema donde un desarrollador crea una carpeta "RequestHandlers" y cada clase debajo de esa carpeta es un controlador de solicitudes, luego el siguiente desarrollador que aparece y piensa: "Necesito poner un nuevo controlador de solicitudes", la infraestructura actual casi lo obliga a seguir la convención. Creo que esto es lo que estaba tratando de decir. Establecer una convención obvia desde el principio puede guiar incluso a los desarrolladores más inexpertos para continuar con el patrón.
Martin Blore
1

El problema con un objeto dios es que generalmente puedes dividirlo en pedazos de manera rentable. Por definición, hace más de una cosa. Todo el propósito de dividirlo en clases más pequeñas es para que pueda tener una clase que haga una cosa bien (y tampoco debe ampliar la definición de "una cosa"). Esto significa que, para cualquier clase, una vez que sepas lo que se supone que debe hacer, deberías poder leerlo con bastante facilidad y decir si está haciendo eso correctamente o no.

creo que ahi es algo así como tener demasiado modularidad, y un exceso de flexibilidad, pero eso es más de un problema de sobre-diseño y el exceso de ingeniería, donde le contabilización de los requisitos que ni siquiera saben que el cliente desea. Muchas ideas de diseño están orientadas a facilitar el control de los cambios en la forma en que se ejecuta el código, pero si nadie espera el cambio, entonces no tiene sentido incluir la flexibilidad.

jprete
fuente
"Hace más de una cosa" puede ser difícil de definir, dependiendo del nivel de abstracción en el que esté trabajando. Se podría argumentar que cualquier método con dos o más líneas de código está haciendo más de una cosa. En el extremo opuesto del espectro, algo complejo como un motor de secuencias de comandos necesitará una gran cantidad de métodos que hacen "cosas" individuales que no están directamente relacionadas entre sí, pero que comprenden una parte importante del "meta". cosa "que hace que sus scripts se ejecuten correctamente, y solo se puede separar mucho sin romper el motor de scripts.
Mason Wheeler
Definitivamente, hay un espectro de niveles conceptuales, pero puede escoger un punto en él: el tamaño de una "clase que hace una cosa" debe ser tal que pueda comprender casi inmediatamente la ejecución. Es decir, los detalles de implementación encajan en su cabeza. Si se hace más grande, entonces no puedes entender toda la clase a la vez. Si es más pequeño, entonces estás abstrayendo demasiado. Pero como dijiste en otro comentario, hay mucho juicio y experiencia involucrados.
jprete
En mi humilde opinión, el mejor nivel de abstracción para pensar es el nivel que espera que utilicen los consumidores del objeto. Supongamos que tiene un objeto llamado Queryerque optimiza las consultas de la base de datos, consulta el RDBMS y analiza los resultados en objetos para devolver. Si solo hay una manera de hacer esto en su aplicación y Queryerestá sellado herméticamente y encapsula de esta manera, entonces solo hace una cosa. Si hay varias formas de hacer esto y alguien podría preocuparse por los detalles de una parte, entonces hace varias cosas y es posible que desee dividirlo.
dsimcha
1

Utilizo una directriz más simple: si puede escribir pruebas unitarias para ello y no tiene duplicación de código, es lo suficientemente abstracto. Además, estás en una buena posición para una refactorización posterior.

Más allá de eso, debe tener SOLID en mente, pero más como una guía que como una regla.

Jeffrey Faust
fuente
0

¿Qué hay de malo en los objetos de Dios y el código estrechamente acoplado, si es que no necesita más modularidad, no tiene una duplicación significativa y el código es legible?

Si la aplicación es lo suficientemente pequeña, cualquier cosa es mantenible. En aplicaciones más grandes, los objetos de Dios se convierten rápidamente en un problema. Eventualmente, implementar una nueva característica requiere modificar 17 objetos de Dios. A las personas no les va muy bien siguiendo los procedimientos de 17 pasos. Los objetos de Dios están siendo modificados constantemente por múltiples desarrolladores, y esos cambios tienen que fusionarse repetidamente. No quieres ir allí.

Kevin Cline
fuente
0

Comparto sus preocupaciones sobre la abstracción excesiva e inapropiada, pero no estoy necesariamente tan preocupado por la abstracción prematura.

Probablemente eso suene como una contradicción, pero es poco probable que el uso de una abstracción cause un problema siempre y cuando no se comprometa demasiado pronto, es decir, siempre que esté dispuesto y pueda refactorizar si es necesario.

Lo que esto implica es cierto grado de creación de prototipos, experimentación y retroceso en el código.

Dicho esto, no hay una regla determinista simple a seguir que resuelva todos los problemas. La experiencia cuenta mucho, pero tienes que ganar esa experiencia cometiendo errores. Y siempre hay más errores para cometer que los errores anteriores no te habrán preparado.

Aun así, trate los principios de los libros de texto como un punto de partida, luego aprenda a programar mucho y vea cómo funcionan esos principios. Si fuera posible dar pautas mejores, más precisas y confiables que cosas como SOLID, alguien probablemente lo habría hecho ahora, e incluso si lo hubieran hecho, esos principios mejorados seguirían siendo imperfectos, y la gente estaría preguntando sobre las limitaciones en esos .

Buen trabajo también: si alguien pudiera proporcionar un algoritmo claro y determinista para diseñar y codificar programas, solo habría un último programa para que los humanos lo escriban: el programa que escribiría automáticamente todos los programas futuros sin intervención humana.

Steve314
fuente
0

Dibujar las líneas de abstracción apropiadas es algo que uno extrae de la experiencia. Cometerá errores al hacerlo, lo que es innegable.

SOLID es una forma concentrada de esa experiencia que le brindan las personas que obtuvieron esta experiencia de la manera difícil. Casi lo has clavado cuando dices que te abstendrás de crear abstracciones antes de que veas la necesidad de hacerlo. Bueno, la experiencia que SOLID le brinda lo ayudará a resolver problemas que nunca existió . Pero confía en mí ... hay muy reales.

SOLID se trata de modularidad, la modularidad se trata de mantenibilidad, y la mantenibilidad se trata de permitirle mantener un horario de trabajo humano una vez que el software alcanza la producción y el cliente comienza a notar errores que usted no tiene.

El mayor beneficio de la modularidad es la capacidad de prueba. Cuanto más modular sea su sistema, más fácil será probarlo y más rápido podrá construir bases sólidas para continuar con los aspectos más difíciles de su problema. Por lo tanto, es posible que su problema no exija esta modularidad, pero el hecho de que le permita crear un mejor código de prueba más rápido es una obviedad para luchar por la modularidad.

Ser ágil se trata de intentar lograr el delicado equilibrio entre el envío rápido y proporcionar una buena calidad. Ágil no significa que debamos cortar el camino, de hecho, los proyectos ágiles más exitosos en los que he estado involucrado son aquellos que prestaron mucha atención a tales pautas.

Newtopian
fuente
0

Un punto importante que parece haberse pasado por alto es que SOLID se practica junto con TDD. TDD tiende a resaltar lo que es "apropiado" en su contexto particular y ayuda a aliviar muchas de las equivocaciones que parecen estar en el consejo.

delitescere
fuente
1
Por favor considere ampliar su respuesta. Tal como están las cosas, está reuniendo dos conceptos ortogonales y no está claro cómo el resultado aborda las preocupaciones del OP.