Principios SÓLIDOS y estructura de código

150

En una entrevista de trabajo reciente, no pude responder una pregunta sobre SOLID , más allá de proporcionar el significado básico de los diversos principios. Realmente me molesta. He hecho un par de días para investigar y aún tengo que presentar un resumen satisfactorio.

La pregunta de la entrevista fue:

Si mirara un proyecto .Net que le dije que seguía estrictamente los principios de SOLID, ¿qué esperaría ver en términos del proyecto y la estructura del código?

Me tambaleé un poco, realmente no respondí la pregunta, y luego bombardeé.

¿Cómo podría haber manejado mejor esta pregunta?

Unidad S
fuente
3
Me preguntaba qué no está claro en la página wiki de SOLID
BЈовић
Bloques de construcción abstractos extensibles.
rwong
Siguiendo los Principios SÓLIDOS de Diseño Orientado a Objetos, sus clases tenderán naturalmente a ser pequeñas, bien factorizadas y fácilmente probadas. Fuente: docs.asp.net/en/latest/fundamentals/…
WhileTrueSleep

Respuestas:

188

S = Principio de responsabilidad única

Por lo tanto, esperaría ver una carpeta bien organizada / estructura de archivos y jerarquía de objetos. Cada clase / pieza de funcionalidad debe nombrarse de manera que su funcionalidad sea muy obvia, y solo debe contener lógica para realizar esa tarea.

Si viera grandes clases de gerentes con miles de líneas de código, eso sería una señal de que no se estaba siguiendo la responsabilidad individual.

O = Principio abierto / cerrado

Esta es básicamente la idea de que se debe agregar nueva funcionalidad a través de nuevas clases que tengan un mínimo impacto en / requieran la modificación de la funcionalidad existente.

Esperaría ver mucho uso de la herencia de objetos, subtipos, interfaces y clases abstractas para separar el diseño de una funcionalidad de la implementación real, permitiendo que otros vengan e implementen otras versiones sin afectar el original.

L = principio de sustitución de Liskov

Esto tiene que ver con la capacidad de tratar los subtipos como su tipo principal. Esto sale de la caja en C # si está implementando una jerarquía de objetos heredados adecuada.

Esperaría ver código que trate objetos comunes como su tipo base y métodos de llamada en las clases base / abstracta en lugar de instanciar y trabajar en los subtipos.

I = Principio de segregación de interfaz

Esto es similar a SRP. Básicamente, se definen subconjuntos más pequeños de la funcionalidad como interfaces y trabajar con aquellos a mantener su sistema desacoplado (por ejemplo, una FileManagerpodría tener el single A cargo de tratar con archivos de E / S, pero que podría poner en práctica una IFileReadery IFileWriterque contenía las definiciones de métodos específicos para la lectura y redacción de archivos).

D = Principio de inversión de dependencia.

Nuevamente, esto se relaciona con mantener un sistema desacoplado. Quizás serías en la búsqueda de la utilización de una biblioteca .NET Inyección de Dependencia, que se utilizó en la solución como Unityo Ninjecto un sistema ServiceLocator como AutoFacServiceLocator.

Eoin Campbell
fuente
36
He visto muchas violaciones de LSP en C #, cada vez que alguien decide que su subtipo particular es especializado y, por lo tanto, no necesita implementar una parte de la interfaz y simplemente lanza una excepción en esa parte ... Este es un enfoque junior común para resolver el problema de la malentendido de la implementación y el diseño de la interfaz
Jimmy Hoffa
2
@JimmyHoffa Esa es una de las principales razones por las que insisto en usar Contratos de Código; Pasar por el proceso de pensamiento de diseñar los contratos ayuda a muchas personas a salir de ese mal hábito.
Andy
12
No me gusta el "LSP sale de la caja en C #" y equiparar DIP a la práctica de inyección de dependencia.
Eufórico
3
+1 pero Inversión de dependencia <> Inyección de dependencia. Juegan bien juntos, pero la inversión de dependencia es mucho más que la inyección de dependencia. Referencia: DIP in the wild
Marjan Venema
3
@Andy: lo que también ayuda son las pruebas unitarias definidas en las interfaces contra las cuales se prueban todos los implementadores (cualquier clase que pueda / se instancia).
Marjan Venema
17

Muchas clases pequeñas e interfaces con inyección de dependencia por todas partes. Probablemente en un gran proyecto también usarías un marco de trabajo de IoC para ayudarte a construir y administrar las vidas de todos esos pequeños objetos. Ver https://stackoverflow.com/questions/21288/which-net-dependency-injection-frameworks-are-worth-looking-into

Tenga en cuenta que un gran proyecto .NET que sigue ESTRICTAMENTE principios SÓLIDOS no significa necesariamente una buena base de código para trabajar con todos. Dependiendo de quién era el entrevistador, él / ella puede haber querido que usted demuestre que comprende lo que significa SÓLIDO y / o que compruebe cuán dogmáticamente sigue los principios de diseño.

Usted ve, para ser SÓLIDO, debe seguir:

S principio de responsabilidad ingle, por lo que tendrá muchas pequeñas clases de cada uno de ellos haciendo una sola cosa

O principio de pluma cerrada, que en .NET generalmente se implementa con inyección de dependencia, que también requiere la I y la D a continuación ...

El principio de sustitución de L iskov es probablemente imposible de explicar en C # con una línea. Afortunadamente, hay otras preguntas que lo abordan, por ejemplo, https://stackoverflow.com/questions/4428725/can-you-explain-liskov-substitution-principle-with-a-good-c-sharp-example

Me nterface Segregación Principio trabaja en conjunto con el principio de Abierto Cerrado. Si se sigue literalmente, significaría preferir una gran cantidad de interfaces muy pequeñas en lugar de pocas interfaces "grandes"

D ependency principio de inversión de las clases de alto nivel no deben depender de las clases de bajo nivel, tanto debería depender de abstracciones.

Paolo Falabella
fuente
SRP no significa "hacer una sola cosa".
Robert Harvey
13

Algunas cosas básicas que esperaría ver en la base de código de una tienda que adoptaba SOLID en su trabajo diario:

  • Muchos archivos de código pequeños: con una clase por archivo como práctica recomendada en .NET, y el Principio de responsabilidad única que fomenta las estructuras de clases modulares pequeñas, esperaría ver muchos archivos, cada uno con una clase pequeña y enfocada.
  • Muchos patrones adaptadores y compuestos: esperaría el uso de muchos patrones adaptadores (una clase que implementa una interfaz al "pasar" a la funcionalidad de una interfaz diferente) para agilizar la conexión de una dependencia desarrollada para un propósito en un poco diferentes lugares donde también se necesita su funcionalidad. Las actualizaciones tan simples como reemplazar un registrador de consola con un registrador de archivos violarán LSP / ISP / DIP si la interfaz se actualiza para exponer un medio para especificar el nombre de archivo a utilizar; en cambio, la clase del registrador de archivos expondrá a los miembros adicionales, y luego un Adaptador hará que el registrador de archivos se vea como un registrador de consola al ocultar las cosas nuevas, por lo que solo el objeto que une todo esto debe saber la diferencia.

    Del mismo modo, cuando una clase necesita agregar una dependencia de una interfaz similar a una existente, para evitar cambiar el objeto (OCP), la respuesta habitual es implementar un patrón Compuesto / Estrategia (una clase que implementa la interfaz de dependencia y consume varios otros implementaciones de esa interfaz, con cantidades variables de lógica que permiten a la clase pasar una llamada a una, algunas o todas las implementaciones).

  • Muchas interfaces y ABC: DIP requiere necesariamente que existan abstracciones, y el ISP recomienda que estas sean de alcance limitado. Por lo tanto, las interfaces y las clases base abstractas son la regla, y necesitará muchas de ellas para cubrir la funcionalidad de dependencia compartida de su base de código. Si bien SOLID estricto necesitaría inyectar todo , es obvio que debe crear en algún lugar, por lo que si un formulario GUI solo se crea como hijo de un formulario principal al realizar alguna acción en dicho padre, no tengo reparos en actualizar el formulario secundario desde el código directamente dentro del padre. Por lo general, creo que ese código es su propio método, por lo que si dos acciones de la misma forma alguna vez abrieron la ventana, simplemente llamo al método.
  • Muchos proyectos: el objetivo de todo esto es limitar el alcance del cambio. El cambio incluye la necesidad de volver a compilar (un ejercicio relativamente trivial, pero aún importante en muchas operaciones críticas para el procesador y el ancho de banda, como implementar actualizaciones en un entorno móvil). Si un archivo en un proyecto tiene que ser reconstruido, todos los archivos lo hacen. Eso significa que si coloca interfaces en las mismas bibliotecas que sus implementaciones, se está perdiendo el punto; Tendrá que volver a compilar todos los usos si cambia una implementación de la interfaz porque también volverá a compilar la definición de la interfaz, lo que requerirá que los usos apunten a una nueva ubicación en el binario resultante. Por lo tanto, mantener las interfaces separadas de los usos y Las implementaciones, si bien además las segregan por área de uso general, es una práctica recomendada típica.
  • Se prestó mucha atención a la terminología de "Gang of Four": los patrones de diseño identificados en el libro Design Patterns de 1994 enfatizan el diseño de código modular de tamaño pequeño que SOLID busca crear. El principio de inversión de dependencia y el principio abierto / cerrado, por ejemplo, están en el corazón de la mayoría de los patrones identificados en ese libro. Como tal, esperaría que una tienda que se adhiriera firmemente a los principios SÓLIDOS también adoptara la terminología en el libro de Gang of Four y nombrara las clases de acuerdo con su función en ese sentido, como "AbcFactory", "XyzRepository", "DefToXyzAdapter "," A1Command "etc.
  • Un repositorio genérico: de acuerdo con ISP, DIP y SRP como se entiende comúnmente, el repositorio es casi omnipresente en el diseño SÓLIDO, ya que permite que el código de consumo solicite clases de datos de manera abstracta sin necesidad de un conocimiento específico del mecanismo de recuperación / persistencia, y coloca el código que hace esto en un lugar en lugar del patrón DAO (en el que si tuviera, por ejemplo, una clase de datos Factura, también tendría un InvoiceDAO que produjera objetos hidratados de ese tipo, y así sucesivamente) todos los objetos de datos / tablas en la base de código / esquema).
  • Un contenedor de IoC: dudo en agregar este, ya que en realidad no uso un marco de IoC para hacer la mayor parte de mi inyección de dependencia. Rápidamente se convierte en un anti-patrón de Dios Objeto de tirar todo dentro del contenedor, sacudiéndolo y vertiendo la dependencia hidratada verticalmente que necesita a través de un método de fábrica inyectado. Suena genial, hasta que te das cuenta de que la estructura se vuelve bastante monolítica, y el proyecto con la información de registro, si es "fluido", ahora tiene que saber todo sobre todo en tu solución. Esas son muchas razones para cambiar. Si no es fluido (registros vinculados tarde utilizando archivos de configuración), entonces una pieza clave de su programa se basa en "cadenas mágicas", un conjunto completamente diferente de gusanos.
KeithS
fuente
1
¿Por qué los votos negativos?
KeithS
Creo que esta es una buena respuesta. En lugar de ser similar a las muchas publicaciones de blog sobre cuáles son estos términos, ha enumerado ejemplos y explicaciones que muestran su uso y valor
Crowie
10

Distraiga con la discusión de Jon Skeet de cómo la 'O' en SOLID es "inútil y poco entendida" y haga que hablen de la "variación protegida" de Alistair Cockburn y el "diseño de herencia de Josh Bloch, o prohíbala".

Breve resumen del artículo de Skeet (¡aunque no recomendaría dejar caer su nombre sin leer la publicación original del blog!):

  • La mayoría de la gente no sabe lo que significa 'abierto' y 'cerrado' en 'principio abierto-cerrado', incluso si piensan que sí.
  • Las interpretaciones comunes incluyen:
    • que los módulos siempre deben extenderse a través de la herencia de implementación, o
    • que el código fuente del módulo original nunca se puede cambiar.
  • La intención subyacente de OCP, y la formulación original de Bertrand Meyer, está bien:
    • que los módulos deben tener interfaces bien definidas (no necesariamente en el sentido técnico de 'interfaz') de las que sus clientes pueden depender, pero
    • Debería ser posible ampliar lo que pueden hacer sin romper esas interfaces.
  • Pero las palabras "abierto" y "cerrado" solo confunden el problema, incluso si son un buen acrónimo pronunciable.

El OP preguntó: "¿Cómo podría haber manejado mejor esta pregunta?" Como ingeniero senior que realiza una entrevista, estaría enormemente más interesado en un candidato que pueda hablar de manera inteligente sobre los pros y los contras de los diferentes estilos de diseño de código que alguien que pueda recitar una lista de viñetas.

Otra buena respuesta sería: "Bueno, eso depende de qué tan bien lo hayan entendido. Si todo lo que saben es las palabras de moda SÓLIDAS, esperaría abuso de herencia, uso excesivo de marcos de inyección de dependencia, un millón de interfaces pequeñas, ninguna de las cuales reflejar el vocabulario de dominio utilizado para comunicarse con la gestión de productos ... "

David Moles
fuente
6

Probablemente hay varias maneras de responder a esto con diferentes cantidades de tiempo. Sin embargo, creo que esto es más parecido a "¿Sabes lo que significa SÓLIDO?" Por lo tanto, responder a esta pregunta probablemente solo se reduce a acertar y explicarlo en términos de un proyecto.

Entonces, espera ver lo siguiente:

  • Las clases tienen una responsabilidad única (por ejemplo, una clase de acceso a datos para clientes solo obtendrá datos de clientes de la base de datos de clientes).
  • Las clases se extienden fácilmente sin afectar el comportamiento existente. No tengo que modificar las propiedades u otros métodos para agregar funcionalidad adicional.
  • Las clases derivadas pueden sustituirse por clases base y las funciones que usan esas clases base no tienen que desenvolver la clase base al tipo más específico para poder manejarlas.
  • Las interfaces son pequeñas y fáciles de entender. Si una clase usa una interfaz, no necesita depender de varios métodos para realizar una tarea.
  • El código se abstrae lo suficiente como para que la implementación de alto nivel no dependa concretamente de una implementación específica de bajo nivel. Debería poder cambiar la implementación de bajo nivel sin afectar el código de alto nivel. Por ejemplo, puedo cambiar mi capa de acceso a datos SQL por una basada en servicios web sin afectar el resto de mi aplicación.
villecoder
fuente
4

Esta es una excelente pregunta, aunque creo que es una pregunta difícil para la entrevista.

Los principios SOLID realmente gobiernan las clases y las interfaces y cómo se relacionan entre sí.

Esta pregunta es realmente una que tiene más que ver con archivos y no necesariamente con clases.

Una breve observación o respuesta que daría es que generalmente verá archivos que contienen solo una interfaz, y a menudo la convención es que comienzan con una I mayúscula. Más allá de eso, mencionaría que los archivos no tendrían código duplicado (especialmente dentro de un módulo, aplicación o biblioteca), y ese código se compartiría cuidadosamente a través de ciertos límites entre módulos, aplicaciones o bibliotecas.

Robert Martin analiza este tema en el ámbito de C ++ al diseñar aplicaciones de C ++ orientadas a objetos utilizando el método Booch (consulte las secciones sobre Cohesión, cierre y reutilización) y en Código limpio .

J. Polfer
fuente
Los codificadores .NET IME generalmente siguen una regla de "1 clase por archivo", y también reflejan estructuras de carpetas / espacios de nombres; el IDE de Visual Studio fomenta ambas prácticas, y varios complementos como ReSharper pueden aplicarlas. Por lo tanto, esperaría ver una estructura de proyecto / archivo que refleje la estructura de clase / interfaz.
KeithS