.net - ¿Debo tener un ensamblaje separado para interfaces?
namespaces assemblies (6)
Actualmente tenemos bastantes clases en un proyecto, y cada una de esas clases implementa una interfaz, principalmente por razones de DI.
Ahora, mi opinión personal es que estas interfaces deben colocarse en un espacio de nombres separado dentro del mismo ensamblaje (por lo tanto, tenemos un ensamblado MyCompany.CoolApp.DataAccess
, y dentro de eso hay un espacio de nombres de Interfaces
proporciona MyCompany.CoolApp.DataAccess.Interfaces
).
Sin embargo, alguien ha sugerido que estas interfaces deberían estar en su propio ensamblaje. Y mi pregunta es, ¿tienen razón? Puedo ver que hay algunos beneficios (por ejemplo, otros proyectos solo necesitarán consumir el ensamblaje de la interfaz), pero al final de ese día todos estos ensamblajes deberán cargarse. También me parece que podría haber un problema de implementación un poco más complejo, ya que Visual Studio no colocará automáticamente el ensamblaje de implementación en la carpeta bin del destino.
¿Hay guías de mejores prácticas para esto?
EDITAR:
Para aclarar un poco más mi punto: ya separamos UI, DataAccess, DataModel y otras cosas en diferentes ensamblajes. Actualmente, también podemos intercambiar nuestra implementación con una implementación diferente sin ningún problema, ya que asignamos la clase de implementación a la interfaz mediante Unity (IOC framework). Debo señalar que nunca escribimos dos implementaciones de la misma interfaz, excepto por razones de polimorfismo y creación de simulacros para pruebas de unidad. Por lo tanto, actualmente no "cambiamos" una implementación excepto en las pruebas unitarias.
El único inconveniente que veo de tener la interfaz en el mismo ensamblaje que la implementación es que todo el ensamblaje (incluida la implementación no utilizada) se habrá cargado.
Sin embargo, puedo ver el punto de que tenerlos en un ensamblaje diferente significa que los desarrolladores no harán "nuevos" accidentalmente a la clase implementadora en lugar de crearlos utilizando el contenedor de IOC.
Un punto que no he entendido de las respuestas es el problema de la implementación. Si solo dependo de los ensamblados de la interfaz, tendré una estructura similar a la siguiente:
MyCompany.MyApplication.WebUI
References:
MyCompany.MyApplication.Controllers.Interfaces
MyCompany.MyApplication.Bindings.Interfaces
etc...
Cuando compilo esto, los ensamblajes que se colocan automáticamente en la carpeta bin son solo esos ensamblados de interfaz. Sin embargo, mis asignaciones de tipo en la unidad asignan diferentes interfaces a sus implementaciones reales. ¿Cómo terminan los ensamblajes que contienen mis implementaciones en la carpeta bin?
¿El habitual esperado ? la práctica es colocarlos en su propio ensamblaje, porque entonces un proyecto dado que consume esas interfaces no requiere una referencia firme a la implementación de esas interfaces. En teoría, esto significa que puede cambiar la implementación con poco o ningún dolor.
Dicho esto, no recuerdo cuándo fue la última vez que hice esto, al punto de @ David_001, esto no es necesariamente "usual". Solemos tener nuestras interfaces en línea con una implementación, nuestro uso más común para las interfaces que se están probando.
Creo que hay diferentes posturas que tomar dependiendo de lo que estés produciendo. Tiendo a producir aplicaciones LOB, que necesitan interoperar internamente con otras aplicaciones y equipos, por lo que hay algunas partes interesadas en la API pública de cualquier aplicación determinada. Sin embargo, esto no es tan extremo como producir una biblioteca o un marco para muchos clientes desconocidos, donde la API pública de repente se vuelve más importante.
En un escenario de implementación, si cambiara la implementación, en teoría podría simplemente implementar esa única DLL, dejando así, por ejemplo, solo la interfaz de usuario y las DLL de interfaz. Si compiló las interfaces y la implementación juntas, es posible que deba volver a implementar la UI DLL ...
Otro beneficio es una segregación limpia de su código: tener una interfaz (o biblioteca compartida) DLL indica explícitamente a cualquier miembro del equipo de desarrollo dónde colocar nuevos tipos, etc. Ya no cuento esto como un beneficio como no hemos tenido. Cualquier problema que no lo haga de esta manera, el contrato público aún se encuentra fácilmente, independientemente de dónde se coloquen las interfaces.
No sé si existen prácticas recomendadas a favor o en contra, lo importante es que en el código, siempre está consumiendo las interfaces y nunca permite que ningún código se filtre al usar la implementación.
El patrón que sigo para lo que llamo tipos compartidos (y también uso DI) es tener un ensamblaje separado que contenga lo siguiente para los conceptos de nivel de aplicación (en lugar de conceptos comunes que entran en ensamblados comunes):
- Interfaces compartidas.
- DTOs.
- Excepciones.
De esta manera, las dependencias entre los clientes y las bibliotecas de aplicaciones principales se pueden administrar, ya que los clientes no pueden depender de una implementación concreta directamente ni como consecuencia involuntaria de agregar una referencia de ensamblaje directo y luego acceder a cualquier tipo público antiguo.
Luego tengo un diseño de tipo de tiempo de ejecución en el que configuro mi contenedor DI al inicio de la aplicación, o el inicio de un conjunto de pruebas unitarias. De esta manera, hay una clara separación entre las implementaciones y cómo puedo variarlas a través de DI. Los módulos de mis clientes nunca tienen una referencia directa a las bibliotecas centrales reales, solo a la biblioteca "SharedTypes".
La clave para mi diseño es tener un concepto de tiempo de ejecución común para los clientes (ya sea una aplicación WPF o NUnit) que configura las dependencias necesarias, es decir, implementaciones concretas o algún tipo de mocks / stubs.
Si los tipos compartidos anteriores no se eliminan, sino que los clientes agregan una referencia al ensamblaje con la implementación concreta, es muy fácil para los clientes usar implementaciones concretas en lugar de las interfaces, tanto de manera obvia como no obvia. Es muy fácil terminar gradualmente con un exceso de acoplamiento que es casi imposible de resolver sin mucho esfuerzo y, lo que es más importante, tiempo.
Actualizar
Para aclarar con un ejemplo de cómo las dependencias terminan en la aplicación de destino.
En mi situación tengo una aplicación cliente WPF. Uso Prism y Unity (para DI) donde, de manera importante, Prism se usa para la composición de aplicaciones.
Con Prism, su ensamblaje de aplicación es solo un Shell, las implementaciones reales de la funcionalidad residen en los ensamblajes "Módulo" (puede tener un ensamblaje separado para cada Módulo conceptual, pero esto no es un requisito, tengo un módulo ATM para ensamblaje). Es responsabilidad de la shell cargar los módulos; la composición de estos módulos es la aplicación. Los módulos utilizan el conjunto SharedTypes, pero el shell hace referencia a los conjuntos concretos. El diseño de tipo de tiempo de ejecución que discutí es responsable de inicializar las dependencias, y esto se hace en el Shell.
De esta manera, los ensamblajes de módulos que tienen toda la funcionalidad no dependen de implementaciones concretas. Son cargados por el shell que ordena las dependencias. El shell hace referencia a los ensamblajes concretos, y así es como entran en el directorio bin.
Bosquejo de la dependencia:
Shell.dll <-- Application
--ModuleA.dll
--ModuleB.dll
--SharedTypes.dll
--Core.dll
--Common.dll + Unity.dll <-- RuntimeDI
ModuleA.dll
--SharedTypes.dll
--Common.dll + Unity.dll <-- RuntimeDI
ModuleB.dll
--SharedTypes.dll
--Common.dll + Unity.dll <-- RuntimeDI
SharedTypes.dll
--...
Estoy de acuerdo con la respuesta marcada. Bien por ti, David. De hecho, me sentí aliviado al ver la respuesta, pensé que me estaba volviendo loco.
Veo este interesante patrón de "bolígrafos en un puchero" en los trabajos freelance de C # de empresa todo el tiempo, donde las personas siguen la convención de la multitud y el equipo debe conformarse, y no conformarse es crear problemas.
El otro loco es el único espacio de nombres por ensamblado sin sentido. Así que obtienes un espacio de nombres SomeBank.SomeApp.Interfaces
y todo está en él.
Para mí, significa que los tipos están dispersos en espacios de nombres y ensamblajes que contienen toda una serie de cosas que no me importan, y que hay que hacer referencia en todo el lugar.
En cuanto a las interfaces, ni siquiera uso interfaces en mis aplicaciones privadas; DI trabaja en tipos, concretos con virtuales, clases base o interfaces. Elijo en consecuencia y coloco tipos en DLL de acuerdo con lo que hacen.
Nunca he tenido un problema con DI o intercambio de lógica más tarde.
• Los ensamblados .NET son una unidad de seguridad, alcance y despliegue de API, y son independientes de los espacios de nombres.
• Si dos conjuntos dependen uno del otro, entonces no se pueden implementar y versionar por separado y deben fusionarse.
• Tener muchos archivos DLL a menudo significa hacer muchas cosas públicas, por lo que es difícil distinguir la API pública real de los miembros de tipo que tuvieron que hacerse públicos porque se colocaron arbitrariamente en su propio ensamblaje.
• ¿El código fuera de mi DLL necesita alguna vez usar mi tipo?
• Iniciar conservador; Por lo general, puedo mover fácilmente un tipo a una capa, es un poco más difícil de otra manera.
• ¿Podría empaquetar perfectamente mi área de características o marco en un paquete de NuGet para que sea completamente opcional y de versión, como cualquier otro paquete?
• ¿Mis tipos se alinean con la entrega de una característica y podrían colocarse en un espacio de nombres de características?
• Muchas bibliotecas y marcos reales están marcados, lo que facilita su discusión y no queman los nombres de los espacios de nombres que implican su uso o son ambiguos, ¿podría calificar los componentes de mi aplicación usando ''nombres de código'' como Steelcore en lugar de genéricos? Cliché y términos confusos, errm ''Servicios''?
Editar
Esta es una de las cosas mal entendidas que veo en el desarrollo de hoy. Es muy malo.
Usted tiene una API, así que ponga todos sus tipos dentro del proyecto de API única. Muévalos solo cuando tenga la necesidad de compartirlos / reutilizarlos. Cuando los mueva, muévalos directamente a un paquete de NuGet con un nombre claro que lleve la intención y el enfoque del paquete. Si estás luchando por un nombre y considerando "Común", es probable que sea porque estás creando un vertedero.
Debe incluir su paquete NuGet en una familia de paquetes relacionados. Su paquete "core" debe tener dependencias mínimas en otros paquetes. Los tipos en el interior están relacionados por el uso y dependen unos de otros.
A continuación, crea un nuevo paquete para los tipos y subtipos más especializados que requieren conjuntos adicionales de dependencias; más claramente: se divide una biblioteca por sus dependencias externas, no por el tipo de tipo o si es una interfaz o una excepción.
Por lo tanto, puede pegar todos sus tipos en una sola biblioteca grande, pero algunos tipos más especializados (puntos de colores) dependen de ciertas librerías externas, por lo que ahora su biblioteca necesita incorporar todas estas dependencias. Eso es innecesario, en lugar de eso, debe dividir esos tipos en otras bibliotecas especializadas que sí toman las dependencias necesarias.
Los tipos en el paquete A y B pueden pertenecer al mismo espacio de nombres. Hacer referencia a A trae un conjunto de tipos y luego, opcionalmente, hacer referencia a B complementa el espacio de nombres con un montón más.
Eso es.
Lucas
Estoy viendo System.Data.dll (4.0) en Object Browser y puedo ver que es autónomo en sí mismo, no solo con interfaces sino con todas las clases instrumentales como DataSet, DataTable, DataRow, DataColumn, etc. Además, repasando la lista de espacios de nombres que contiene como System.Data, System.Data.Common, System.Configuration & System.Xml, sugiere primero tener interfaces contenidas en sus propios ensamblajes con todos los códigos relevantes y necesarios que se mantienen juntos y segundo y, lo que es más importante, reutilizar los mismos espacios de nombres en toda la aplicación (o marco) para segregar las clases virtualmente también.
Las respuestas hasta ahora parecen decir que colocar las interfaces en su propio ensamblaje es la práctica "habitual". No estoy de acuerdo con colocar interfaces no relacionadas en un conjunto común "compartido", por lo que esto implicaría que necesitaré tener un conjunto de interfaz para cada conjunto de "implementación".
Sin embargo, si lo pienso más a fondo, no puedo pensar en muchos ejemplos log4net de esta práctica (por ejemplo, ¿ log4net o NUnit proporcionan ensamblados de interfaz pública para que los consumidores puedan decidir sobre diferentes implementaciones? Si es así, ¿qué otra implementación de nunit puede hacer? ¿Yo suelo?). Pasando las edades buscando en google, he encontrado una serie de recursos.
¿Tener ensamblajes separados implica un acoplamiento suelto? Lo siguiente sugiere que no:
http://www.theserverside.net/tt/articles/showarticle.tss?id=ControllingDependencies
http://codebetter.com/blogs/jeremy.miller/archive/2008/09/30/separate-assemblies-loose-coupling.aspx
El consenso general que pude encontrar en Google fue que menos asambleas es mejor, a menos que haya una buena razón para agregar nuevas asambleas. Vea también esto:
http://www.cauldwell.net/patrick/blog/ThisIBelieveTheDeveloperEdition.aspx
Como no estoy produciendo API públicas, y ya estoy poniendo interfaces en sus propios espacios de nombres, tiene sentido no crear nuevos ensamblados a ciegas. Los beneficios de este enfoque parecen superar los beneficios potenciales de agregar más ensamblajes (donde es poco probable que alguna vez obtengan los beneficios).
Sé que este hilo es muy, muy viejo, pero tengo un pensamiento sobre esto que quiero poner ahí.
Obtengo el montaje para "reutilizar". ¿Pero no podemos ir un paso más allá para nuestra SOLUCIÓN particular?
Si tenemos un ensamblaje en nuestra SOLUCIÓN que tiene las interfaces adecuadas, podemos construir ese ensamblaje y usarlo donde sea que tenga sentido, incluida la reutilización.
Pero, para otros proyectos dentro de OUR MISMA solución, ¿por qué no simplemente agrega el archivo de la interfaz LINK a los otros proyectos que necesitan la interfaz definida?
Al hacerlo, se simplificará la implementación de su proyecto particular (no es necesario implementar el conjunto de la interfaz). Por otro lado, si desea reutilizar la interfaz en una solución diferente a la suya, tiene la opción de copiar el archivo de la interfaz, o simplemente hacer referencia al conjunto de la interfaz.
Parece lo mejor de ambos mundos. Tenemos la opción de cómo obtener la interfaz, y aún es la versión controlada.
Franco