software - Principio de Inversión de Dependencia(SOLID) vs Encapsulación(Pilares de OOP)
segregacion de interfaces (7)
Hace poco tuve un debate sobre el Principio de Inversión de Dependencia , la Inversión de Control y la Inyección de Dependencia . En relación con este tema, debatimos si estos principios violan uno de los pilares de OOP, a saber, la encapsulación .
Mi comprensión de estas cosas es:
- El Principio de Inversión de Dependencia implica que los objetos deben depender de abstracciones, no de concreciones: este es el principio fundamental sobre el cual se implementan el Patrón de Inversión de Control y la Inyección de Dependencia.
- Inversion of Control es una implementación de patrón del Principio de Inversión de Dependencia, donde las dependencias abstractas reemplazan las dependencias concretas, permitiendo que las concreciones de la dependencia se especifiquen fuera del objeto.
- Dependency Injection es un patrón de diseño que implementa Inversion of Control y proporciona resolución de dependencia. La inyección ocurre cuando una dependencia se pasa a un componente dependiente. En esencia, el patrón de Inyección de Dependencia proporciona un mecanismo para acoplar abstracciones de dependencia con implementaciones concretas.
- La encapsulación es el proceso mediante el cual los datos y la funcionalidad requeridos por un objeto de nivel superior se aíslan e inaccesibles, por lo tanto, el programador desconoce cómo se implementa un objeto.
El debate llegó a un punto de fricción con la siguiente declaración:
IoC no es OOP porque rompe la encapsulación
Personalmente, creo que el Principio de Inversión de Dependencia y el patrón de Inversión de Control deben ser observados religiosamente por todos los desarrolladores de OOP, y vivo por la siguiente cita:
Si hay (potencialmente) más de una manera de despellejar a un gato, entonces no se comporte como si hubiera solo una.
Ejemplo 1:
class Program {
void Main() {
SkinCatWithKnife skinner = new SkinCatWithKnife ();
skinner.SkinTheCat();
}
}
Aquí vemos un ejemplo de encapsulación. El programador solo tiene que llamar a Main()
y el gato será despellejado, pero ¿y si quisiera despellejar al gato con, digamos, un conjunto de dientes afilados?
Ejemplo 2:
class Program {
// Encapsulation
ICatSkinner skinner;
public Program(ICatSkinner skinner) {
// Inversion of control
this.skinner = skinner;
}
void Main() {
this.skinner.SkinTheCat();
}
}
... new Program(new SkinCatWithTeeth());
// Dependency Injection
Aquí observamos el Principio de Inversión de Dependencia y la Inversión de Control, ya que se proporciona un resumen ( ICatSkinner
) para permitir el ICatSkinner
dependencias concretas por parte del programador. Por fin, ¡hay más de una manera de despellejar a un gato!
La pelea aquí es; ¿Esto rompe la encapsulación? técnicamente uno podría argumentar que .SkinTheCat();
todavía está encapsulado dentro de la llamada al método Main()
, por lo que el programador desconoce el comportamiento de este método, por lo que no creo que esto rompa la encapsulación.
Profundizando un poco más, creo que los contenedores de IoC rompen OOP porque usan el reflejo, pero no estoy convencido de que IoC rompa OOP, ni estoy convencido de que IoC rompa la encapsulación. De hecho, iría tan lejos como para decir eso:
La encapsulación y la inversión de control coinciden alegremente entre sí, permitiendo a los programadores pasar solo las concreciones de una dependencia, a la vez que ocultan la implementación general a través de la encapsulación.
Preguntas:
- ¿Es la IoC una implementación directa del Principio de Inversión de Dependencia?
- ¿IoC siempre rompe la encapsulación y, por lo tanto, OOP?
- ¿Debería IoC ser usado con moderación, religiosamente o apropiadamente?
- ¿Cuál es la diferencia entre IoC y un contenedor IoC?
¿Es la IoC una implementación directa del Principio de Inversión de Dependencia?
Los dos están relacionados de una manera que hablan de abstracciones, pero eso es todo. La inversión del control es:
un diseño en el que partes de un programa informático personalizadas reciben el flujo de control de una biblioteca genérica y reutilizable ( source )
La inversión de control nos permite enganchar nuestro código personalizado en la tubería de una biblioteca reutilizable. En otras palabras, el control de la inversión se trata de marcos. Una biblioteca reutilizable que no aplica Inversion of Control es simplemente una biblioteca. Un marco es una biblioteca reutilizable que sí aplica Inversion of Control.
Tenga en cuenta que nosotros, como desarrolladores, solo podemos aplicar Inversion of Control si estamos escribiendo un framework nosotros mismos; no puede aplicar la inversión de control como desarrollador de aplicaciones. Sin embargo, podemos (y debemos) aplicar el Principio de Inversión de Dependencia y el patrón de Inyección de Dependencia.
¿IoC siempre rompe la encapsulación y, por lo tanto, OOP?
Como IoC está a punto de engancharse a la tubería de un marco, no hay nada que se escape aquí. Entonces, la verdadera pregunta es: ¿la Inyección de Dependencia rompe la encapsulación?
La respuesta a esa pregunta es: no, no es así. No rompe la encapsulación por dos razones:
- Dado que los Principios de Inversión en Dependencia establecen que debemos programar en contra de una abstracción, un consumidor no podrá acceder a las partes internas de la implementación utilizada y, por lo tanto, esa implementación no estará rompiendo la encapsulación con el cliente. La implementación podría no ser ni siquiera conocida ni accesible en tiempo de compilación (porque vive en un ensamblado sin referencia) y la implementación en ese caso no puede filtrar detalles de implementación y romper la encapsulación.
- Aunque la implementación acepta las dependencias que requiere a través de su constructor, esas dependencias típicamente se almacenarán en campos privados y nadie puede acceder a ellas (incluso si un consumidor depende directamente del tipo concreto) y, por lo tanto, no romperá la encapsulación.
¿Debería IoC ser usado con moderación, religiosamente o apropiadamente?
Nuevamente, la pregunta es "¿Deberían usarse DIP y DI con moderación?". En mi opinión, la respuesta es: NO, en realidad debería usarla en toda la aplicación. Obviamente, nunca debes aplicar cosas religiosamente. Debe aplicar principios SÓLIDOS y el DIP es una parte crucial de esos principios. Harán que su aplicación sea más flexible y más fácil de mantener y, en la mayoría de los escenarios, es muy apropiado aplicar los principios SÓLIDOS.
¿Cuál es la diferencia entre IoC y un contenedor IoC?
La Inyección de Dependencia es un patrón que se puede aplicar con o sin un contenedor IoC. Un contenedor IoC es simplemente una herramienta que puede ayudar a construir su gráfico objeto de una manera más conveniente, en caso de que tenga una aplicación que aplique los principios SOLID correctamente. Si tiene una aplicación que no aplica los principios SÓLIDOS, tendrá dificultades para usar un contenedor IoC. Te será difícil aplicar Dependency Injection. O déjenme decirlo de manera más amplia, de todos modos tendrá dificultades para mantener su aplicación. Pero de ninguna manera un contenedor IoC es una herramienta requerida. Estoy desarrollando y manteniendo un contenedor IoC para .NET, pero no siempre uso un contenedor para todas mis aplicaciones. Para los grandes BLOBA (línea aburrida de aplicaciones comerciales), a menudo uso un contenedor, pero para aplicaciones más pequeñas (o servicios de Windows) no siempre uso un contenedor. Pero casi siempre uso Dependency Injection como patrón, porque esta es la manera más efectiva de cumplir con DIP.
Nota: Dado que un contenedor de IoC nos ayuda a aplicar el patrón de Inyección de dependencias, el "Contenedor de IOC" es un nombre terrible para dicha biblioteca.
Pero a pesar de todo lo que dije antes, tenga en cuenta que:
en el mundo real del desarrollador de software, la utilidad supera a la teoría [del Principio Ágil, Patrones y Prácticas de Robert C. Martin]
En otras palabras, incluso si DI rompa la encapsulación, no importaría, porque estas técnicas y patrones han demostrado ser muy valiosos, porque da como resultado sistemas muy flexibles y mantenibles. La práctica supera la teoría.
¿IoC siempre rompe la encapsulación y, por lo tanto, OOP?
No, estas son preocupaciones relacionadas jerárquicamente. La encapsulación es uno de los conceptos más incomprendidos en OOP, pero creo que la relación se describe mejor a través de Abstract Data Types (ADT). Esencialmente, un ADT es una descripción general de los datos y el comportamiento asociado. Esta descripción es abstracta; omite los detalles de implementación. En cambio, describe un ADT en términos de condiciones previas y posteriores .
Esto es lo que Bertrand Meyer llama diseño por contrato . Puede leer más sobre esta descripción seminal de OOD en la construcción de software orientado a objetos .
Los objetos a menudo se describen como datos con comportamiento . Esto significa que un objeto sin datos no es realmente un objeto. Por lo tanto, debe obtener datos en el objeto de alguna manera.
Podría, por ejemplo, pasar datos a un objeto a través de su constructor:
public class Foo
{
private readonly int bar;
public Foo(int bar)
{
this.bar = bar;
}
// Other members may use this.bar in various ways.
}
Otra opción es usar una función o propiedad setter. Espero que podamos aceptar que, hasta el momento, la encapsulación no se ha violado.
¿Qué sucede si cambiamos la bar
de un entero a otra clase concreta?
public class Foo
{
private readonly Bar bar;
public Foo(Bar bar)
{
this.bar = bar;
}
// Other members may use this.bar in various ways.
}
La única diferencia con respecto a antes es que la bar
ahora es un objeto, en lugar de una primitiva. Sin embargo, esa es una distinción falsa, porque en el diseño orientado a objetos, un número entero también es un objeto . Debido a la optimización del rendimiento en varios lenguajes de programación (Java, C #, etc.), existe una diferencia real entre primitivos (cadenas, enteros, bools, etc.) y objetos ''reales''. Desde una perspectiva OOD, todos son iguales. Las cadenas también tienen comportamientos: puede convertirlas en mayúsculas, invertirlas, etc.
¿Se viola la encapsulación si Bar
es una clase concreta sellada / final con solo miembros no virtuales?
bar
es solo datos con comportamiento, como un entero, pero aparte de eso, no hay diferencia. Hasta ahora, la encapsulación no se viola.
¿Qué sucede si permitimos que Bar
tenga un solo miembro virtual?
¿La encapsulación está rota por eso?
¿Todavía podemos expresar las condiciones previas y posteriores a Foo
, dado que Bar
tiene un solo miembro virtual?
Si Bar
adhiere al Principio de Sustitución de Liskov (LSP), no haría la diferencia. El LSP establece explícitamente que cambiar el comportamiento no debe cambiar la corrección del sistema. Mientras se cumpla ese contrato , la encapsulación está intacta.
Por lo tanto, el LSP (uno de los principios SÓLIDOS , del cual el Principio de Inversión de Dependencia es otro) no infringe la encapsulación; describe un principio para mantener la encapsulación en presencia de polimorfismo .
¿La conclusión cambia si Bar
es una clase base abstracta? Una interfaz?
No, no: esos son solo diferentes grados de polimorfismo. Por lo tanto, podríamos cambiar el nombre de Bar
a IBar
(para sugerir que es una interfaz) y pasarlo a Foo
como sus datos:
public class Foo
{
private readonly IBar bar;
public Foo(IBar bar)
{
this.bar = bar;
}
// Other members may use this.bar in various ways.
}
bar
es solo otro objeto polimórfico, y mientras el LSP se mantenga, la encapsulación se mantiene.
TL; DR
Hay una razón por la cual SOLID también se conoce como los Principios de OOD . La encapsulación (es decir, diseño por contrato) define las reglas básicas. SOLID describe las pautas para seguir esas reglas.
He encontrado un caso cuando ioc y la inyección de dependencia rompen la encapsulación. Supongamos que tenemos una clase ListUtil. En esa clase hay un método llamado eliminar duplicados. Este método acepta una lista. Hay una interfaz ISortAlgorith con un método de ordenación. Hay una clase llamada QuickSort que implementa esta interfaz. Cuando escribimos el alogoritmo para eliminar duplicados, tenemos que ordenar la lista dentro. Ahora si RemoveDuplicates permite una interfaz ISortAlgorithm como parámetro (IOC / Dependency Injection) para permitir la extensibilidad para que otros elijan otro algoritmo para eliminar duplicados, estamos exponiendo la complejidad de eliminar la característica duplicada de la clase ListUtil. Violando así la primera piedra de Oops.
La encapsulación no está en contradicción con los Principios de Inversión de Dependencia en el mundo de la Programación Orientada a Objetos. Por ejemplo, en el diseño de un automóvil, tendrá un "motor interno" que se encapsulará del mundo exterior, y también "ruedas" que pueden reemplazarse fácilmente y considerarse como componentes externos del automóvil. El automóvil tiene especificación (interfaz) para girar el eje de las ruedas, y el componente de las ruedas implementa una parte que interactúa con el eje.
Aquí, el motor interno representa el proceso de encapsulado, mientras que los componentes de la rueda representan los Principios de Inversión de Dependencia (DIP) en el diseño del automóvil. Con DIP, básicamente evitamos la construcción de un objeto monolítico, y en cambio hacemos nuestro objeto composable. ¿Puedes imaginarte que construyes un automóvil, donde no puedes reemplazar las ruedas porque están incorporadas en el automóvil?
También puede leer más acerca de los Principios de Inversión de Dependencia en más detalles en mi blog Here .
Solo responderé una pregunta ya que muchas otras personas han respondido a todo lo demás. Y tenga en cuenta que no hay una respuesta correcta o incorrecta, solo las preferencias del usuario.
¿Debería IoC ser usado con moderación, religiosamente o apropiadamente? Mi experiencia me lleva a creer que la Inyección de Dependencia solo debe usarse en clases que son generales y que podrían necesitar cambios en el futuro. Usarlo religiosamente conducirá a algunas clases que necesitan 15 interfaces en el constructor que pueden consumir mucho tiempo. Eso tiende a generar un 20% de desarrollo y un 80% de mantenimiento.
Alguien mencionó un ejemplo de automóvil y cómo el constructor del automóvil querrá cambiar los neumáticos. La inyección de dependencia permite cambiar las llantas sin importar los detalles específicos de la implementación. Pero si tomamos la inyección de dependencia religiosamente ... Entonces tenemos que empezar a construir interfaces con los componentes de los neumáticos también ... Bueno, ¿y los hilos del neumático? ¿Qué hay de las costuras en esos neumáticos? ¿Qué pasa con el químico en esos hilos? ¿Qué hay de los átomos en esos químicos? Etc ... Bien! ¡Oye! ¡En algún momento tendrás que decir "ya es suficiente"! No conviertamos todo en una interfaz ... Porque eso puede consumir demasiado tiempo. ¡Está bien que algunas clases sean autónomas e instaladas en la clase misma! Es más rápido de desarrollar, y crear instancias de la clase es mucho más fácil.
Solo mis 2 centavos.
Trataré de responder a sus preguntas, según mi entendimiento:
¿Es la IoC una implementación directa del Principio de Inversión de Dependencia?
no podemos etiquetar IoC como la implementación directa de DIP, ya que DIP se enfoca en hacer que los módulos de nivel superior dependan de la abstracción y no en la concreción de los módulos de nivel inferior. Pero más bien IoC es una implementación de Dependency Injection.
¿IoC siempre rompe la encapsulación y, por lo tanto, OOP?
No creo que el mecanismo de IoC viole la encapsulación. Pero puede hacer que el sistema se convierta estrechamente.
¿Debería IoC ser usado con moderación, religiosamente o apropiadamente?
IoC se puede utilizar como en muchos patrones, como Bridge Pattern, donde la separación de Concretion from Abstraction mejora el código. Por lo tanto, se puede usar para lograr DIP.
¿Cuál es la diferencia entre IoC y un contenedor IoC?
IoC es un mecanismo de Dependency Inversion pero los contenedores son aquellos que usan IoC.
Resumiendo la pregunta:
Tenemos la capacidad de que un Servicio cree instancias de sus propias dependencias.
Sin embargo, también tenemos la capacidad de que un Servicio simplemente defina abstracciones y requiera una aplicación para conocer las abstracciones dependientes, crear implementaciones concretas y pasarlas.
Y la pregunta no es: "¿Por qué lo hacemos?" (Porque sabemos que hay una gran lista de por qué). Pero la pregunta es: "¿La opción 2 no rompe la encapsulación?"
Mi respuesta "pragmática"
Creo que Mark es la mejor apuesta para tales respuestas, y como él dice: No, la encapsulación no es lo que las personas piensan que es.
La encapsulación está ocultando detalles de implementación de un servicio o abstracción. Una Dependencia no es un detalle de implementación. Si piensas en un servicio como un contrato y sus subsecuentes dependencias de subservicios como subcontratos (etc., etc., encadenados), entonces realmente terminas con un gran contrato con apéndices.
Imagina que soy una persona que llama y quiero usar un servicio legal para demandar a mi jefe. Mi aplicación debería saber acerca de un servicio que lo haga. Solo eso rompe la teoría de que conocer los servicios / contratos requeridos para lograr mi objetivo es falso.
El argumento es ... sí, pero solo quiero contratar a un abogado, no me importa qué libros o servicios use. Voy a obtener algo aleatorio de la interwebz y no me importan sus detalles de implementación ... así:
sub main() {
LegalService legalService = new LegalService();
legalService.SueMyManagerForBeingMean();
}
public class LegalService {
public void SueMyManagerForBeingMean(){
// Implementation Details.
}
}
Pero resulta que se requieren otros servicios para realizar el trabajo, como la comprensión de la ley del lugar de trabajo. Y también resulta que ... estoy MUY interesado en los contratos que el abogado firma bajo mi nombre y las otras cosas que está haciendo para robar mi dinero. Por ejemplo ... ¿Por qué demonios está este abogado de Internet con sede en Corea del Sur? ¿¡Cómo me ayudará eso!?!? Eso no es un detalle de implementación, eso es parte de una cadena de dependencia de requisitos que me complace administrar.
sub main() {
IWorkLawService understandWorkplaceLaw = new CaliforniaWorkplaceLawService();
//IWorkLawService understandWorkplaceLaw = new NewYorkWorkplaceLawService();
LegalService legalService = new LegalService(understandWorkplaceLaw);
legalService.SueMyManagerForBeingMean();
}
public interface ILegalContract {
void SueMyManagerForBeingMean();
}
public class LegalService : ILegalContract {
private readonly IWorkLawService _workLawService;
public LegalService(IWorkLawService workLawService) {
this._workLawService = workLawService;
}
public void SueMyManagerForBeingMean() {
//Implementation Detail
_workLawService.DoSomething; // { implementation detail in there too }
}
}
Ahora, todo lo que sé es que tengo un contrato que tiene otros contratos que podrían tener otros contratos. Soy muy responsable de esos contratos, y no de sus detalles de implementación. Aunque estoy más que feliz de firmar esos contratos con concreciones que son relevantes para mis requisitos. Y nuevamente, no me importa cómo esas concreciones hacen su trabajo, siempre y cuando sepa que tengo un contrato vinculante que dice que intercambiamos información de alguna manera definida.