¿Por qué es una buena idea que las capas de aplicación "inferiores" no sean conscientes de las capas "superiores"?


66

En una aplicación web MVC típica (bien diseñada), la base de datos no conoce el código del modelo, el código del modelo no conoce el código del controlador y el código del controlador no conoce el código de vista. (Me imagino que incluso podría comenzar tan abajo como el hardware, o tal vez incluso más, y el patrón podría ser el mismo).

Yendo en la otra dirección, puede ir solo una capa hacia abajo. La vista puede conocer el controlador pero no el modelo; el controlador puede conocer el modelo pero no la base de datos; el modelo puede conocer la base de datos pero no el sistema operativo. (Cualquier cosa más profunda es probablemente irrelevante).

Puedo comprender intuitivamente por qué es una buena idea, pero no puedo articularla. ¿Por qué este estilo unidireccional de capas es una buena idea?


10
Tal vez sea porque los datos "suben" de la base de datos a la vista. "Comienza" en la base de datos y "llega" a la vista. La conciencia de capa va en la dirección opuesta a medida que los datos "viajan". Me gusta usar "comillas".
Jason Swett

1
Lo etiquetó en su última oración: Unidireccional. ¿Por qué las listas enlazadas son mucho más típicas que las listas doblemente enlazadas? El mantenimiento de las relaciones se vuelve infinitamente más simple con una lista individualmente vinculada. Creamos gráficos de dependencia de esta manera porque las llamadas recursivas se vuelven mucho menos probables y las características generales del gráfico se vuelven más fáciles de razonar en su conjunto. Las estructuras razonables son inherentemente más mantenibles, y las mismas cosas que afectan a los gráficos a nivel micro (implementación) también lo hacen a nivel macro (arquitectura).
Jimmy Hoffa

2
En realidad, no creo que sea una buena práctica en la mayoría de los casos que la Vista esté al tanto del Controlador. Dado que los Controladores son casi siempre conscientes de la Vista, tener la Vista al tanto del controlador crea una referencia circular
Amy Blankenship

8
mal tiempo de analogía: por la misma razón, el tipo que te golpea desde atrás con un automóvil mientras conduces es el responsable y responsable del accidente, en el caso general. Él puede ver lo que estás haciendo y debería tener el control, y si no puede evitarlo, significa que no respetó las reglas de seguridad. No de la otra manera. Y, al encadenarse, eso lo libera de preocuparse por lo que sucede detrás de él.
haylem

1
Obviamente, una vista es consciente de un modelo de vista fuertemente tipado.
DazManCat

Respuestas:


121

Las capas, los módulos, de hecho la arquitectura misma, son medios para hacer que los programas informáticos sean más fáciles de entender por los humanos . El método numéricamente óptimo para resolver un problema es casi siempre un enredo impuro de código no modular, autorreferenciado o incluso auto modificable, ya sea un código ensamblador altamente optimizado en sistemas embebidos con limitaciones de memoria paralizantes o secuencias de ADN después de millones de años. de presión de selección. Dichos sistemas no tienen capas, ninguna dirección discernible del flujo de información, de hecho, no hay una estructura que podamos discernir en absoluto. Para todos menos para su autor, parecen trabajar por pura magia.

En ingeniería de software, queremos evitar eso. La buena arquitectura es una decisión deliberada de sacrificar algo de eficiencia en aras de hacer que el sistema sea comprensible para las personas normales. Comprender una cosa a la vez es más fácil que comprender dos cosas que solo tienen sentido cuando se usan juntas. Es por eso que los módulos y las capas son una buena idea.

Pero inevitablemente los módulos deben invocar funciones entre sí y las capas deben crearse una encima de la otra. En la práctica, siempre es necesario construir sistemas para que algunas partes requieran otras. El compromiso preferido es construirlos de tal manera que una parte requiera otra, pero esa parte no requiere que se devuelva la primera. Y esto es exactamente lo que nos proporciona la estratificación unidireccional: es posible comprender el esquema de la base de datos sin conocer las reglas de negocio y comprender las reglas de negocio sin conocer la interfaz de usuario. Sería bueno tener independencia en ambas direcciones, permitiendo que alguien programe una nueva interfaz de usuario sin saber nadaen absoluto sobre las reglas comerciales, pero en la práctica esto prácticamente nunca es posible. Las reglas generales como "Sin dependencias cíclicas" o "Las dependencias solo deben alcanzar un nivel" simplemente capturan el límite prácticamente alcanzable de la idea fundamental de que una cosa a la vez es más fácil de entender que dos cosas.


1
¿Qué quiere decir con "hacer que el sistema sea comprensible para la gente normal "? Creo que redactado de esa manera alienta a los nuevos programadores a rechazar sus puntos positivos porque, como la mayoría de las personas, piensan que son más inteligentes que la mayoría de las personas y esto no será un problema para ellos. Yo diría "hacer que el sistema sea comprensible para los humanos"
Thomas Bonini

12
Esta es una lectura obligatoria para aquellos que piensan que el desacoplamiento completo es ideal para luchar, pero no pueden entender por qué no funciona.
Robert Harvey

66
Bueno, @Andreas, siempre está Mel .
TRiG

66
Creo que "más fácil de entender" no es suficiente. También se trata de facilitar la modificación, extensión y mantenimiento del código.
Mike Weller

1
@Peri: dicha ley existe, ver en.wikipedia.org/wiki/Law_of_Demeter . Si está de acuerdo o no con él, es otro asunto.
Mike Chamberlain

61

La motivación fundamental es la siguiente: desea poder extraer una capa completa y sustituirla por una completamente diferente (reescrita), y NADIE DEBE (SER CAPAZ) DE NOTIFICAR LA DIFERENCIA.

El ejemplo más obvio es arrancar la capa inferior y sustituirla por otra diferente. Esto es lo que hace cuando desarrolla la (s) capa (s) superior (s) contra una simulación del hardware y luego la sustituye por el hardware real.

El siguiente ejemplo es cuando extrae una capa intermedia y la sustituye por una capa intermedia diferente. Considere una aplicación que utiliza un protocolo que se ejecuta sobre RS-232. Un día, tienes que cambiar la codificación del protocolo por completo, porque "algo más cambió". (Ejemplo: cambio de codificación ASCII directa a codificación Reed-Solomon de transmisiones ASCII, porque estaba trabajando en un enlace de radio desde el centro de Los Ángeles a Marina Del Rey, y ahora está trabajando en un enlace de radio desde el centro de Los Ángeles a una sonda en órbita alrededor de Europa , una de las lunas de Júpiter, y ese enlace necesita una corrección de errores hacia adelante mucho mejor).

La única forma de hacer que esto funcione es si cada capa exporta una interfaz conocida y definida a la capa de arriba, y espera una interfaz conocida y definida a la capa de abajo.

Ahora, no es exactamente el caso que las capas inferiores no sepan NADA sobre las capas superiores. Más bien, lo que la capa inferior sabe es que la capa inmediatamente superior a ella operará precisamente de acuerdo con su interfaz definida. No puede saber nada más porque, por definición, todo lo que no está en la interfaz definida está sujeto a cambios SIN PREVIO AVISO.

La capa RS-232 no sabe si está ejecutando ASCII, Reed-Solomon, Unicode (página de códigos en árabe, página de códigos en japonés, página de códigos Rigellian Beta) o qué. Simplemente sabe que está obteniendo una secuencia de bytes y está escribiendo esos bytes en un puerto. La próxima semana, podría estar obteniendo una secuencia de bytes completamente diferente de algo completamente diferente. No le importa Él solo mueve bytes.

La primera (y la mejor) explicación del diseño en capas es el clásico artículo de Dijkstra "Estructura del sistema de multiprogramación" . Se requiere lectura en este negocio.


Esto es útil, y gracias por el enlace. Desearía poder seleccionar dos respuestas como la mejor. Básicamente volteé una moneda en mi cabeza y elegí la otra, pero todavía voté por la tuya.
Jason Swett

+1 para excelentes ejemplos. Me gusta la explicación dada por el JRS
ViSu

@JasonSwett: ¡Si hubiera lanzado la moneda, la habría lanzado hasta que designara esa respuesta! ^^ +1 a John.
Olivier Dulac

No estoy de acuerdo con esto de alguna manera, porque rara vez quieres poder extraer la capa de reglas de negocio e intercambiarla por otra. Las reglas de negocio cambian mucho más lentamente que la UI o las tecnologías de acceso a datos.
Andy

Ding Ding Ding !!! Creo que la palabra que estabas buscando es 'desacoplar'. Para eso están las buenas API. Definir las interfaces públicas de un módulo para que pueda usarse universalmente.
Evan Plaice

8

Porque los niveles más altos pueden cambiar.

Cuando eso sucede, ya sea debido a cambios en los requisitos, los nuevos usuarios, la tecnología diferente, una aplicación modular (es decir, unidireccionalmente en capas) debería requerir menos mantenimiento y adaptarse más fácilmente para satisfacer las nuevas necesidades.


4

Creo que la razón principal es que hace que las cosas estén más estrechamente unidas. Cuanto más apretado sea el acoplamiento, más probabilidades hay de tener problemas más tarde. Ver este artículo más información: Acoplamiento

Aquí hay un extracto:

Desventajas

Los sistemas estrechamente acoplados tienden a exhibir las siguientes características de desarrollo, que a menudo se consideran desventajas: un cambio en un módulo generalmente fuerza un efecto dominó de los cambios en otros módulos. El ensamblaje de módulos puede requerir más esfuerzo y / o tiempo debido a la mayor dependencia entre módulos. Un módulo en particular puede ser más difícil de reutilizar y / o probar porque se deben incluir módulos dependientes.

Dicho esto, la razón para tener un sistema acoplado más tig es por razones de rendimiento. El artículo que mencioné también tiene información sobre esto.


4

OMI, es muy simple. No puede volver a usar algo que sigue haciendo referencia al contexto en el que se usa.


4

Las capas no deben tener dependencias bidireccionales

Las ventajas de una arquitectura en capas son que las capas deben ser utilizables independientemente:

  • debería poder crear una capa de presentación diferente además de la primera sin cambiar la capa inferior (por ejemplo, construir una capa API además de una interfaz web existente)
  • debería poder refactorizar o eventualmente reemplazar la capa inferior sin cambiar la capa superior

Estas condiciones son básicamente simétricas . Explican por qué generalmente es mejor tener solo una dirección de dependencia, pero no cuál .

La dirección de dependencia debe seguir la dirección del comando

La razón por la que preferimos una estructura de dependencia de arriba hacia abajo es porque los objetos superiores crean y usan los objetos inferiores . Una dependencia es básicamente una relación que significa "A depende de B si A no puede funcionar sin B". Entonces, si los objetos en A usan los objetos en B, así deberían ser las dependencias.

Esto es de alguna manera algo arbitrario. En otros patrones, como MVVM, el control fluye fácilmente desde las capas inferiores. Por ejemplo, puede configurar una etiqueta cuyo título visible esté vinculado a una variable y cambie con ella. Sin embargo, normalmente todavía es preferible tener dependencias de arriba hacia abajo, porque los objetos principales son siempre con los que el usuario interactúa, y esos objetos hacen la mayor parte del trabajo.

Mientras que de arriba hacia abajo usamos la invocación de métodos, de abajo hacia arriba (típicamente) usamos eventos. Los eventos permiten que las dependencias vayan de arriba abajo incluso cuando el control fluye al revés. Los objetos de la capa superior se suscriben a eventos en la capa inferior. La capa inferior no sabe nada sobre la capa superior, que actúa como un complemento.

También hay otras formas de mantener una sola dirección, por ejemplo:

  • continuaciones (pasar un lambda o un método para ser llamado y evento a un método asíncrono)
  • subclase (cree una subclase en A de una clase primaria en B que luego se inyecta en la capa inferior, un poco como un complemento)

3

Me gustaría agregar mis dos centavos a lo que Matt Fenwick y Kilian Foth ya han explicado.

Un principio de la arquitectura de software es que los programas complejos deben construirse componiendo bloques más pequeños y autónomos (cajas negras): esto minimiza las dependencias y, por lo tanto, reduce la complejidad. Por lo tanto, esta dependencia unidireccional es una buena idea porque facilita la comprensión del software, y la gestión de la complejidad es uno de los problemas más importantes en el desarrollo de software.

Entonces, en una arquitectura en capas, las capas inferiores son cajas negras que implementan capas de abstracción sobre las cuales se construyen las capas superiores. Si una capa inferior (por ejemplo, la capa B) puede ver detalles de una capa superior A, entonces B ya no es un cuadro negro: sus detalles de implementación dependen de algunos detalles de su propio usuario, pero la idea de un cuadro negro es que su ¡El contenido (su implementación) es irrelevante para su usuario!


3

Solo por diversión.

Piensa en una pirámide de porristas. La fila inferior está apoyando las filas por encima de ellos.

Si la animadora en esa fila está mirando hacia abajo, son estables y se mantendrán equilibrados para que los de arriba no se caigan.

Si levanta la vista para ver cómo están todos los que están por encima de ella, perderá el equilibrio y hará que toda la pila se caiga.

No es realmente técnico, pero fue una analogía que pensé que podría ayudar.


3

Si bien la facilidad de comprensión y, en cierta medida, los componentes reemplazables son buenas razones, una razón igualmente importante (y probablemente la razón por la que se inventaron las capas en primer lugar) es desde el punto de vista del mantenimiento del software. La conclusión es que las dependencias causan el potencial de romper cosas.

Por ejemplo, suponga que A depende de B. Dado que nada depende de A, los desarrolladores son libres de cambiar A a su gusto sin tener que preocuparse de que puedan romper algo que no sea A. Sin embargo, si el desarrollador quiere cambiar B, cualquier cambio en B que se hace podría potencialmente romper A. Este fue un problema frecuente en los primeros días de la computadora (piense en el desarrollo estructurado) donde los desarrolladores corregirían un error en una parte del programa y generarían errores en partes del programa aparentemente totalmente ajenas en otros lugares. Todo por dependencias.

Para continuar con el ejemplo, supongamos que A depende de B y B depende de A. IOW, una dependencia circular. Ahora, cada vez que se realiza un cambio en cualquier lugar, podría romper el otro módulo. Un cambio en B aún podría romper a A, pero ahora un cambio en A también podría romper a B.

Entonces, en su pregunta original, si está en un equipo pequeño para un proyecto pequeño, entonces todo esto es excesivo porque puede cambiar libremente los módulos a su antojo. Sin embargo, si está en un proyecto considerable, si todos los módulos dependían de los demás, cada vez que se necesitara un cambio podría potencialmente romper los otros módulos. En un proyecto grande, conocer todos los impactos podría ser difícil de determinar, por lo que probablemente perderá algunos impactos.

Empeora en un proyecto grande donde hay muchos desarrolladores (por ejemplo, algunos que solo trabajan en la capa A, en la capa B y en la capa C). Como es probable que cada cambio tenga que ser revisado / discutido con los miembros en las otras capas para asegurarse de que sus cambios no se rompan o obliguen a volver a trabajar en lo que están trabajando. Si sus cambios fuerzan los cambios en otros, entonces debe convencerlos de que deberían hacer el cambio, porque no van a querer asumir más trabajo solo porque tiene esta nueva forma de hacer las cosas en su módulo. IOW, una pesadilla burocrática.

Pero si limita las dependencias a A depende de B, B depende de C, entonces solo las personas de la capa C necesitan coordinar sus cambios en ambos equipos. La capa B solo necesita coordinar los cambios con el equipo de la capa A y el equipo de la capa A es libre de hacer lo que quiera porque su código no afecta a la capa B o C. Por lo tanto, idealmente, diseñará sus capas para que la capa C cambie muy poco, la capa B cambia un poco y la capa A hace la mayor parte del cambio.


+1 En mi empleador, en realidad tenemos un diagrama interno que describe la esencia de su último párrafo, ya que se aplica al producto en el que trabajo, es decir, cuanto más baje su pila, menor será la tasa de cambio (y debería ser).
RobV 01 de

1

La razón más básica por la que las capas inferiores no deberían ser conscientes de las capas superiores es que hay muchos más tipos de capas superiores. Por ejemplo, hay miles y miles de programas diferentes en su sistema Linux, pero llaman a la misma mallocfunción de biblioteca C. Entonces la dependencia es de estos programas a esa biblioteca.

Tenga en cuenta que las "capas inferiores" son en realidad las capas intermedias.

Piense en una aplicación que se comunica a través del mundo exterior a través de algunos controladores de dispositivo. El sistema operativo está en el medio .

El sistema operativo no depende de los detalles dentro de las aplicaciones ni de los controladores del dispositivo. Existen muchos tipos de controladores de dispositivos del mismo tipo y comparten el mismo marco de controladores de dispositivos. A veces, los piratas informáticos del núcleo tienen que poner un manejo de casos especiales en el marco por el bien de un hardware o dispositivo en particular (ejemplo reciente que encontré: código específico de PL2303 en el marco de serie USB de Linux). Cuando eso sucede, generalmente ponen comentarios sobre cuánto apesta y debe eliminarse. Aunque el sistema operativo llama a las funciones en los controladores, las llamadas pasan por ganchos que hacen que los controladores se vean iguales, mientras que cuando los controladores llaman al sistema operativo, a menudo usan funciones específicas directamente por su nombre.

Entonces, de alguna manera, el sistema operativo es realmente una capa inferior desde la perspectiva de la aplicación y desde la perspectiva de la aplicación: un tipo de centro de comunicación donde las cosas se conectan y los datos se cambian para ir por las vías apropiadas. Ayuda al diseño del centro de comunicación para exportar un servicio flexible que puede ser utilizado por cualquier cosa, y no para mover ningún dispositivo o aplicación específica hacks en el centro.


Estoy feliz siempre y cuando no tenga que preocuparme por establecer voltajes específicos en pines específicos de la CPU :)
un CVn

1

La separación de las preocupaciones y los enfoques de división / conquista pueden ser otra explicación para estas preguntas. La separación de las preocupaciones brinda la capacidad de portabilidad y, en algunas arquitecturas más complejas, brinda a la plataforma ventajas independientes de escala y rendimiento.

En este contexto, si piensa en una arquitectura de 5 niveles (cliente, presentación, negocio, integración y nivel de recursos), el nivel inferior de arquitectura no debería ser consciente de la lógica y el negocio de los niveles superiores y viceversa. Me refiero a nivel inferior como integración y niveles de recursos. Las interfaces de integración de bases de datos proporcionadas en la integración y la base de datos real y los servicios web (proveedores de datos de terceros) pertenecen al nivel de recursos. Entonces, supongamos que cambiará su base de datos MySQL a una base de datos de documentos NoSQL como MangoDB en términos de escalabilidad o lo que sea.

En este enfoque, al nivel de negocio no le importa cómo el nivel de integración proporciona la conexión / transmisión por el recurso. Solo busca objetos de acceso a datos proporcionados por el nivel de integración. Esto podría ampliarse a más escenarios, pero básicamente, la separación de las preocupaciones podría ser la razón número uno para esto.


1

Ampliando la respuesta de Kilian Foth, esta dirección de estratificación corresponde a una dirección en la que un humano explora un sistema.

Imagine que es un nuevo desarrollador encargado de corregir un error en el sistema en capas.

Los errores generalmente no coinciden entre lo que el cliente necesita y lo que obtiene. A medida que el cliente se comunica con el sistema a través de la interfaz de usuario y obtiene resultados a través de la interfaz de usuario (la interfaz de usuario significa literalmente 'interfaz de usuario'), los errores también se informan en términos de interfaz de usuario. Por lo tanto, como desarrollador, no tiene muchas opciones, sino comenzar a mirar la interfaz de usuario también, para descubrir qué sucedió.

Por eso es necesario tener conexiones de capa de arriba hacia abajo. Ahora, ¿por qué no tenemos conexiones en ambos sentidos?

Bueno, tienes tres escenarios de cómo podría ocurrir ese error.

Podría ocurrir en el propio código de la interfaz de usuario y, por lo tanto, estar localizado allí. Esto es fácil, solo necesitas encontrar un lugar y arreglarlo.

Podría ocurrir en otras partes del sistema como resultado de llamadas realizadas desde la IU. Lo cual es moderadamente difícil, trazas un árbol de llamadas, encuentras un lugar donde ocurre el error y lo arreglas.

Y podría ocurrir como resultado de una llamada EN su código de UI. Lo cual es difícil, debe capturar la llamada, encontrar su fuente y luego averiguar dónde se produce el error. Teniendo en cuenta que un punto en el que comienza está situado en el fondo de una sola rama de un árbol de llamadas, Y primero necesita encontrar un árbol de llamadas correcto, podría haber varias llamadas en el código de la interfaz de usuario, tiene su depuración cortada para usted.

Para eliminar el caso más difícil tanto como sea posible, se desaconsejan las dependencias circulares, las capas se conectan principalmente de arriba hacia abajo. Incluso cuando se necesita una conexión en sentido contrario, generalmente es limitada y está claramente definida. Por ejemplo, incluso con las devoluciones de llamada, que son una especie de conexión inversa, el código que se llama en devolución de llamada generalmente proporciona esta devolución de llamada en primer lugar, implementando una especie de "aceptación" para conexiones inversas y limita su impacto en la comprensión de un sistema.

La estratificación es una herramienta y está dirigida principalmente a desarrolladores que admiten un sistema existente. Bueno, las conexiones entre capas también reflejan eso.


-1

Otra razón por la que me gustaría que se mencione explícitamente aquí es la reutilización del código . Ya hemos tenido el ejemplo del medio RS232 que se reemplaza, así que vamos un paso más allá ...

Imagina que estás desarrollando controladores. Es tu trabajo y escribes bastante. Los protocolos probablemente podrían comenzar a repetirse en algún momento, al igual que los medios físicos.

Entonces, lo que comenzará a hacer, a menos que sea un gran fanático de hacer lo mismo una y otra vez, es escribir capas reutilizables para estas cosas.

Supongamos que tiene que escribir 5 controladores para dispositivos Modbus. Uno de ellos usa Modbus TCP, dos usan Modbus en RS485 y el resto pasa por RS232. No volverás a implementar Modbus 5 veces, porque estás escribiendo 5 controladores. Además, no volverá a implementar Modbus 3 veces, porque tiene 3 capas físicas diferentes debajo de usted.

Lo que haces es escribir un acceso a medios TCP, un acceso a medios RS485 y posiblemente un acceso a medios RS232. ¿Es inteligente saber que habrá una capa modbus arriba, en este punto? Probablemente no. El siguiente controlador que va a implementar también podría usar Ethernet pero usar HTTP-REST. Sería una pena si tuviera que volver a implementar el acceso a los medios de Ethernet para comunicarse a través de HTTP.

Una capa arriba, implementará Modbus solo una vez. Esa capa Modbus, una vez más, no sabrá de los controladores, que están una capa arriba. Estos controladores, por supuesto, tendrán que saber que se supone que deben hablar modbus, y se supone que deben saber que usan Ethernet. Sin embargo, como implementó la forma en que lo acabo de describir, no solo podría extraer una capa y reemplazarla. podría, por supuesto, y eso para mí es el mayor beneficio de todos, seguir adelante y reutilizar esa capa Ethernet existente para algo absolutamente no relacionado con el proyecto que originalmente causó su creación.

Esto es algo que probablemente vemos todos los días como desarrolladores y eso nos ahorra toneladas de tiempo. Hay innumerables bibliotecas para todo tipo de protocolos y otras cosas. Estos existen debido a principios como la dirección de dependencia que sigue a la dirección del comando, lo que nos permite construir capas de software reutilizables.


la reutilización ya se ha mencionado explícitamente en una respuesta publicada hace más de medio año
mosquito
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.