Tengo un módulo, digamos 'M', que tiene algunos clientes, digamos 'C1', 'C2', 'C3'. Quiero distribuir el espacio de nombres del módulo M, es decir, las declaraciones de las API y los datos que expone, en archivos de encabezado de tal manera que:
- para cualquier cliente, solo los datos y las API que requiere están visibles; el resto del espacio de nombres del módulo está oculto para el cliente, es decir, se adhieren al principio de segregación de interfaz .
- una declaración no se repite en varios archivos de encabezado, es decir, no viola DRY .
- El módulo M no tiene dependencias de sus clientes.
- un cliente no se ve afectado por los cambios realizados en partes del módulo M que no utiliza.
- Los clientes existentes no se ven afectados por la adición (o eliminación) de más clientes.
Actualmente trato con esto dividiendo el espacio de nombres del módulo según los requisitos de sus clientes. Por ejemplo, en la imagen a continuación se muestran las diferentes partes del espacio de nombres del módulo requerido por sus 3 clientes. Los requisitos del cliente se superponen. El espacio de nombres del módulo se divide en 4 archivos de encabezado separados: 'a', '1', '2' y '3' .
Sin embargo, esto viola algunos de los requisitos antes mencionados, es decir, R3 y R5. Se infringe el requisito 3 porque esta partición depende de la naturaleza de los clientes; también al agregar un nuevo cliente, esta partición cambia y viola el requisito 5. Como se puede ver en el lado derecho de la imagen de arriba, con la adición de un nuevo cliente, el espacio de nombres del módulo ahora se divide en 7 archivos de encabezado: 'a ',' b ',' c ',' 1 ',' 2 * ',' 3 * 'y' 4 ' . Los archivos de encabezado significaban para 2 de los cambios de clientes anteriores, lo que desencadena su reconstrucción.
¿Hay alguna manera de lograr la segregación de interfaz en C de manera no artificial?
En caso afirmativo, ¿cómo abordaría el ejemplo anterior?
Una solución hipotética irreal que imagino sería:
el módulo tiene 1 archivo de encabezado grueso que cubre todo su espacio de nombres. Este archivo de encabezado se divide en secciones y subsecciones direccionables, como una página de Wikipedia. Luego, cada cliente tiene un archivo de encabezado específico diseñado para ello. Los archivos de encabezado específicos del cliente son solo una lista de hipervínculos a las secciones / subsecciones del archivo de encabezado grueso. Y el sistema de compilación debe reconocer un archivo de encabezado específico del cliente como 'modificado' si se modifica alguna de las secciones a las que apunta en el encabezado del Módulo.
fuente
struct
es lo que usas en C cuando quieres una interfaz. Por supuesto, los métodos son un poco difíciles. Puede encontrar esto interesante: cs.rit.edu/~ats/books/ooc.pdfstruct
yfunction pointers
.Respuestas:
La segregación de interfaz, en general, no debe basarse en los requisitos del cliente. Debe cambiar todo el enfoque para lograrlo. Yo diría que modularice la interfaz agrupando las características en grupos coherentes . Es decir, la agrupación se basa en la coherencia de las características en sí, no en los requisitos del cliente. En ese caso, tendrá un conjunto de interfaces, I1, I2, ... etc. El Cliente C1 puede usar I2 solo. El cliente C2 puede usar I1 e I5, etc. Tenga en cuenta que, si un cliente usa más de un Ii, no es un problema. Si ha descompuesto la interfaz en módulos coherentes, ahí es donde está el meollo del asunto.
Nuevamente, el ISP no está basado en el cliente. Se trata de descomponer la interfaz en módulos más pequeños. Si esto se hace correctamente, también garantizará que los clientes estén expuestos a la menor cantidad de funciones que necesiten.
Con este enfoque, sus clientes pueden aumentar a cualquier número, pero usted no se ve afectado. Cada cliente utilizará una o alguna combinación de las interfaces en función de sus necesidades. ¿Habrá casos en que un cliente, C, deba incluir decir I1 e I3, pero no usar todas las características de estas interfaces? Sí, eso no es un problema. Solo usa la menor cantidad de interfaces.
fuente
El principio de segregación de interfaz dice:
Hay algunas preguntas sin respuesta aquí. Uno es:
¿Cuán pequeño?
Tu dices:
A este manual lo llamo escribir pato . Construye interfaces que exponen solo lo que un cliente necesita. El principio de segregación de la interfaz no es simplemente escribir manualmente.
Pero el ISP tampoco es simplemente un llamado a interfaces de roles "coherentes" que puedan reutilizarse. Ningún diseño de interfaz de roles "coherente" puede proteger perfectamente contra la adición de un nuevo cliente con sus propias necesidades de roles.
ISP es una forma de aislar a los clientes del impacto de los cambios en el servicio. Su objetivo era hacer que la compilación fuera más rápida a medida que realiza cambios. Claro que tiene otros beneficios, como no romper clientes, pero ese fue el punto principal. Si estoy cambiando la
count()
firma de la función de servicios , es bueno si los clientes que no usancount()
no necesitan ser editados y recompilados.Esto es POR QUÉ me importa el Principio de segregación de interfaz. No es algo que considero importante como la fe. Resuelve un problema real.
Entonces, la forma en que debe aplicarse debería resolver un problema para usted. No hay una forma de muerte cerebral de aplicar ISP que no pueda ser derrotada con el ejemplo correcto de un cambio necesario. Se supone que debe observar cómo está cambiando el sistema y tomar decisiones que permitan que las cosas se calmen. Exploremos las opciones.
Primero pregúntese: ¿es difícil hacer cambios en la interfaz de servicio en este momento? Si no, sal y juega hasta que te calmes. Este no es un ejercicio intelectual. Asegúrese de que la cura no sea peor que la enfermedad.
Si muchos clientes usan el mismo subconjunto de funciones, eso argumenta a favor de interfaces reutilizables "coherentes". El subconjunto probablemente se centra en una idea que podemos considerar como el rol que el servicio está proporcionando al cliente. Es bueno cuando esto funciona. Esto no siempre funciona.
Si muchos clientes usan diferentes subconjuntos de funciones, es posible que el cliente realmente esté usando el servicio a través de múltiples roles. Eso está bien, pero hace que los roles sean difíciles de ver. Encuéntralos e intenta separarlos. Eso puede volver a ponernos en el caso 1. El cliente simplemente usa el servicio a través de más de una interfaz. Por favor, no comience a enviar el servicio. En todo caso, eso significaría pasar el servicio al cliente más de una vez. Eso funciona, pero me hace preguntar si el servicio no es una gran bola de lodo que necesita ser dividida.
Si muchos clientes usan diferentes subconjuntos, pero no ve roles que permiten que los clientes usen más de uno, entonces no tiene nada mejor que escribir en pato para diseñar sus interfaces. Esta forma de diseñar las interfaces garantiza que el cliente no esté expuesto ni siquiera a una función que no esté utilizando, pero casi garantiza que agregar un nuevo cliente siempre implicará agregar una nueva interfaz que, aunque la implementación del servicio no necesita saber al respecto, la interfaz que agrega las interfaces de rol lo hará. Simplemente hemos cambiado un dolor por otro.
Si muchos clientes usan diferentes subconjuntos, se superponen, se espera que se agreguen nuevos clientes que necesitarán subconjuntos impredecibles, y usted no está dispuesto a dividir el servicio y luego considerar una solución más funcional. Dado que las dos primeras opciones no funcionaron y realmente estás en un mal lugar donde nada sigue un patrón y se están produciendo más cambios, entonces considera proporcionar a cada función su propia interfaz. Terminar aquí no significa que el ISP haya fallado. Si algo falló fue el paradigma orientado a objetos. Las interfaces de método único siguen al ISP en extremo. Es un poco digno de teclado, pero es posible que de repente esto haga que las interfaces sean reutilizables. Nuevamente, asegúrese de que no haya
Entonces resulta que pueden volverse muy pequeños.
He tomado esta pregunta como un desafío para aplicar ISP en los casos más extremos. Pero tenga en cuenta que es mejor evitar los extremos. En un diseño bien pensado que aplica otros principios SÓLIDOS, estos problemas generalmente no ocurren ni importan, casi tanto.
Otra pregunta sin respuesta es:
¿Quién posee estas interfaces?
Una y otra vez veo interfaces diseñadas con lo que yo llamo una mentalidad de "biblioteca". Todos hemos sido culpables de la codificación mono-ver-mono-hacer donde solo estás haciendo algo porque así es como lo viste hacer. Somos culpables de lo mismo con las interfaces.
Cuando miro una interfaz diseñada para una clase en una biblioteca, solía pensar: oh, estos tipos son profesionales. Esta debe ser la forma correcta de hacer una interfaz. Lo que no entendía es que el límite de una biblioteca tiene sus propias necesidades y problemas. Por un lado, una biblioteca ignora por completo el diseño de sus clientes. No todos los límites son iguales. Y a veces, incluso el mismo límite tiene diferentes formas de cruzarlo.
Aquí hay dos formas simples de ver el diseño de la interfaz:
Interfaz de propiedad del servicio. Algunas personas diseñan cada interfaz para exponer todo lo que un servicio puede hacer. Incluso puede encontrar opciones de refactorización en IDE que escribirán una interfaz para usted usando cualquier clase que alimente.
Interfaz propiedad del cliente. El ISP parece argumentar que esto es correcto y que el servicio es incorrecto. Debe dividir cada interfaz con las necesidades de los clientes en mente. Como el cliente posee la interfaz, debe definirla.
Entonces, ¿quién tiene razón?
Considere los complementos:
¿Quién posee las interfaces aquí? ¿Los clientes? ¿Los servicios?
Resulta que ambos.
Los colores aquí son capas. Se supone que la capa roja (derecha) no sabe nada sobre la capa verde (izquierda). La capa verde se puede cambiar o reemplazar sin tocar la capa roja. De esa manera, cualquier capa verde se puede conectar a la capa roja.
Me gusta saber qué se supone que debe saber sobre qué y qué se supone que no debe saber. Para mí, "¿qué sabe sobre qué?", Es la pregunta arquitectónica más importante.
Dejemos claro un poco de vocabulario:
Un cliente es algo que usa.
Un servicio es algo que se usa.
Interactor
Resulta ser ambos.ISP dice romper las interfaces para los clientes. Bien, apliquemos eso aquí:
Presenter
(un servicio) no debe dictar a laOutput Port <I>
interfaz. La interfaz debe reducirse a lo queInteractor
necesita (aquí actuando como cliente). Eso significa que la interfaz SABE sobre elInteractor
y, para seguir al ISP, debe cambiar con él. Y esto está bien.Interactor
(aquí actuando como un servicio) no debería dictar a laInput Port <I>
interfaz. La interfaz debe reducirse a lo queController
(un cliente) necesita. Eso significa que la interfaz SABE sobre elController
y, para seguir al ISP, debe cambiar con él. Y esto no está bien.El segundo no está bien porque no se supone que la capa roja sepa sobre la capa verde. Entonces, ¿está mal el ISP? Así un poco. Ningún principio es absoluto. Este es un caso en el que los tontos a quienes les gusta la interfaz para mostrar todo lo que el servicio puede hacer resultan ser correctos.
Al menos, tienen razón si
Interactor
no hace nada más que este caso de uso. SiInteractor
hace cosas para otros casos de uso, no hay razónInput Port <I>
para saber esto. No estoy seguro de por quéInteractor
no puede centrarse solo en un caso de uso, por lo que este no es un problema, sino que sucede algo.Pero la
input port <I>
interfaz simplemente no puede esclavarse a sí mismaController
cliente y hacer que este sea un verdadero complemento. Este es un límite de 'biblioteca'. Una tienda de programación completamente diferente podría estar escribiendo la capa verde años después de la publicación de la capa roja.Si está cruzando un límite de 'biblioteca' y siente la necesidad de aplicar ISP a pesar de que no posee la interfaz en el otro lado, tendrá que encontrar una manera de estrechar la interfaz sin cambiarla.
Una forma de lograrlo es con un adaptador. Ponlo entre clientes como
Controler
y laInput Port <I>
interfaz. El adaptador aceptaInteractor
comoInput Port <I>
y delega su trabajo. Sin embargo, expone solo lo que los clientesController
necesitan a través de una interfaz de rol o interfaces propiedad de la capa verde. El adaptador no sigue al ISP por sí mismo, pero permite clases más complejas comoController
disfrutar del ISP. Esto es útil si hay menos adaptadores que clientesController
que los usan y cuando se encuentra en una situación inusual en la que está cruzando el límite de una biblioteca y, a pesar de ser publicada, la biblioteca no dejará de cambiar. Mirándote Firefox. Ahora esos cambios solo rompen sus adaptadores.Entonces, ¿qué significa esto? Significa honestamente que no me has proporcionado suficiente información para decirte lo que debes hacer. No sé si no seguir a ISP te está causando un problema. No sé si seguirlo no te causaría más problemas.
Sé que estás buscando un principio rector simple. ISP intenta ser eso. Pero deja mucho sin decir. Yo creo en eso. Sí, ¡no obligue a los clientes a depender de métodos que no utilizan, sin una buena razón!
Si tiene una buena razón, como el diseño de algo para aceptar complementos, tenga en cuenta los problemas que no siguen las causas del ISP (es difícil cambiar sin romper clientes) y las formas de mitigarlos (mantener
Interactor
o al menosInput Port <I>
centrarse en uno estable caso de uso).fuente
Entonces este punto:
Renuncia a que estás violando otro principio importante que es YAGNI. Me importaría cuando tenga cientos de clientes. Pensar en algo por adelantado y luego resultará que no tienes clientes adicionales para este código supera el propósito.
Segundo
¿Por qué su código no usa DI, inversión de dependencia, nada, nada en su biblioteca debería depender de la naturaleza de su cliente?
Eventualmente parece que necesita una capa adicional debajo de su código para satisfacer las necesidades de cosas superpuestas (DI, por lo que su código frontal depende solo de esta capa adicional, y sus clientes dependen solo de su interfaz frontal) de esta manera vence a DRY.
Esto lo odiarías de verdad. Así que haces lo mismo que usas en tu capa de módulo debajo de otro módulo. De esta manera, logrando una capa debajo de usted:
si
si
si
si
fuente
La misma información que se proporciona en la declaración siempre se repite en la definición. Es solo la forma en que funciona este lenguaje. Además, repetir una declaración en varios archivos de encabezado no viola DRY . Es una técnica bastante utilizada (al menos en la biblioteca estándar).
Repetir la documentación o la implementación violaría DRY .
No me molestaría con esto a menos que el código del cliente no esté escrito por mí.
fuente
Renuncio a mi confusión. Sin embargo, su ejemplo práctico dibuja una solución en mi cabeza. Si puedo expresarlo con mis propias palabras: todas las particiones en el módulo
M
tienen una relación exclusiva de muchos a muchos con todos y cada uno de los clientes.Estructura de la muestra
Mh
Mc
En el archivo Mc, en realidad no tendría que usar los #ifdefs porque lo que coloca en el archivo .c no afecta a los archivos del cliente siempre que las funciones que utilizan los archivos del cliente estén definidas.
C1.c
C2.c
C3.c
Nuevamente, no estoy seguro si esto es lo que estás preguntando. Así que tómelo con cuidado.
fuente
P1_init()
yP2_init()
?P1_init()
yP2_init()
enlace a?_PREF_
con lo que se definió por última vez. Entonces_PREF_init()
seráP1_init()
debido a la última declaración #define. Luego, la siguiente instrucción define establecerá PREF igual a P2_, generando asíP2_init()
.