Considere estos dos ejemplos:
Pasar un objeto a un constructor
class ExampleA
{
private $config;
public function __construct($config)
{
$this->config = $config;
}
}
$config = new Config;
$exampleA = new ExampleA($config);
Instanciar una clase
class ExampleB
{
private $config;
public function __construct()
{
$this->config = new Config;
}
}
$exampleA = new ExampleA();
¿Cuál es la forma correcta de manejar agregar un objeto como propiedad? ¿Cuándo debo usar uno sobre el otro? ¿Las pruebas unitarias afectan lo que debo usar?
object-oriented
unit-testing
configuration
Prisionero
fuente
fuente
Respuestas:
Creo que el primero le dará la capacidad de crear un
config
objeto en otro lugar y pasárseloExampleA
. Si necesita la inyección de dependencia, esto puede ser algo bueno porque puede asegurarse de que todas las intancias compartan el mismo objeto.Por otro lado, tal vez
ExampleA
requiera unconfig
objeto nuevo y limpio, por lo que podría haber casos en los que el segundo ejemplo sea más apropiado, como casos en los que cada instancia podría tener una configuración diferente.fuente
¡No te olvides de la capacidad de prueba!
Por lo general, si el comportamiento de la
Example
clase depende de la configuración, desea poder probarlo sin guardar / modificar el estado interno de la instancia de clase (es decir, desearía tener pruebas simples de ruta feliz y configuración incorrecta sin modificar la propiedad / miembro deExample
clase).Por lo tanto, iría con la primera opción de
ExampleA
fuente
Si el objeto tiene la responsabilidad de administrar la vida útil de la dependencia, entonces está bien crear el objeto en el constructor (y disponerlo en el destructor). *
Si el objeto no es responsable de administrar la vida útil de la dependencia, debe pasar al constructor y administrarse desde el exterior (por ejemplo, mediante un contenedor IoC).
En este caso, no creo que su ClassA deba asumir la responsabilidad de crear $ config, a menos que también sea responsable de eliminarlo, o si la configuración es única para cada instancia de ClassA.
* Para ayudar en la capacidad de prueba, el constructor podría hacer referencia a una clase / método de fábrica para construir la dependencia en su constructor, aumentando la cohesión y la capacidad de prueba.
fuente
He tenido esta misma discusión con nuestro equipo de arquitectura recientemente y hay algunas razones sutiles para hacerlo de una forma u otra. Principalmente se reduce a la Inyección de dependencias (como han señalado otros) y si realmente tiene control sobre la creación del objeto que es nuevo en el constructor.
En su ejemplo, ¿qué pasa si su clase de configuración:
a) tiene una asignación no trivial, como proceder de un grupo o método de fábrica.
b) podría no ser asignado. Pasarlo al constructor evita perfectamente este problema.
c) es en realidad una subclase de Config.
Pasar el objeto al constructor brinda la mayor flexibilidad.
fuente
La respuesta a continuación es incorrecta, pero la guardaré para que otros aprendan de ella (ver más abajo)
En
ExampleA
, puede usar la mismaConfig
instancia en varias clases. Sin embargo, si debería haber solo unaConfig
instancia dentro de la aplicación completa, considere aplicar el patrón SingletonConfig
para evitar tener múltiples instancias deConfig
. Y siConfig
es un Singleton, puede hacer lo siguiente en su lugar:En
ExampleB
, por otro lado, siempre obtendrá una instancia separada deConfig
para cada instancia deExampleB
.La versión que debe aplicar realmente depende de cómo la aplicación manejará las instancias de
Config
:ExampleX
debe tener una instancia separada deConfig
, vaya conExampleB
;ExampleX
compartirá una (y solo una) instancia deConfig
, useExampleA with Config Singleton
;ExampleX
pueden usar diferentes instancias deConfig
, quédese conExampleA
.Por qué está mal convertirse
Config
en Singleton :Debo admitir que ayer aprendí sobre el patrón Singleton (leyendo el libro de patrones de diseño Head First ). Ingenuamente me puse en práctica y lo apliqué para este ejemplo, pero como muchos han señalado, una forma es otra (algunas han sido más crípticas y solo dijeron "¡Lo estás haciendo mal!"), Esta no es una buena idea. Entonces, para evitar que otros cometan el mismo error que acabo de cometer, aquí sigue un resumen de por qué el patrón Singleton puede ser dañino (según los comentarios y lo que descubrí buscando en Google):
Si
ExampleA
recupera su propia referencia a laConfig
instancia, las clases estarán estrechamente acopladas. No habrá forma de tener una instancia deExampleA
usar una versión diferente deConfig
(digamos alguna subclase). Esto es horrible si quieres probarExampleA
usando una instancia de maquetaConfig
ya que no hay forma de proporcionarlaExampleA
.La premisa de que habrá una, y solo una, instancia de
Config
tal vez se mantenga ahora , pero no siempre puede estar seguro de que lo mismo se mantendrá en el futuro . Si en algún momento posterior resulta queConfig
serán deseables múltiples instancias de , no hay forma de lograr esto sin reescribir el código.A pesar de que la instancia de one-and-only-one
Config
es tal vez cierta para toda la eternidad, puede suceder que desee poder usar alguna subclase deConfig
(mientras todavía tenga una sola instancia). Pero, dado que el código obtiene directamente la instancia a travésgetInstance()
deConfig
, que es unstatic
método, no hay forma de obtener la subclase. Nuevamente, el código debe ser reescrito.El hecho de que los
ExampleA
usosConfig
estarán ocultos, al menos cuando solo esté viendo la API deExampleA
. Esto puede o no ser algo malo, pero personalmente siento que esto se siente como una desventaja; por ejemplo, cuando se realiza el mantenimiento, no existe una forma simple de averiguar a qué clases se verán afectados los cambiosConfig
sin analizar la implementación de todas las demás clases.Incluso si el hecho de que
ExampleA
use un SingletonConfig
no es un problema en sí mismo, puede convertirse en un problema desde el punto de vista de la prueba. Los objetos Singleton llevarán un estado que persistirá hasta la finalización de la aplicación. Esto puede ser un problema al ejecutar pruebas unitarias, ya que desea que una prueba esté aislada de otra (es decir, que haber ejecutado una prueba no debería afectar el resultado de otra). Para solucionar esto, el objeto Singleton debe destruirse entre cada ejecución de prueba (potencialmente tener que reiniciar toda la aplicación), lo que puede llevar mucho tiempo (sin mencionar tedioso y molesto).Dicho esto, me alegro de haber cometido este error aquí y no en la implementación de una aplicación real. De hecho, estaba considerando reescribir mi último código para usar el patrón Singleton para algunas de las clases. Aunque podría revertir fácilmente los cambios (todo está almacenado en un SVN, por supuesto), todavía habría perdido el tiempo haciéndolo.
fuente
ExampleA
yConfig
, lo que no es algo bueno.Lo más sencillo que hacer es acoplar
ExampleA
aConfig
. Debe hacer lo más simple, a menos que haya una razón convincente para hacer algo más complejo.Una razón para desacoplar
ExampleA
yConfig
sería mejorar la capacidad de prueba deExampleA
. El acoplamiento directo degradará la capacidad de prueba deExampleA
siConfig
tiene métodos que son lentos, complejos o que evolucionan rápidamente. Para las pruebas, un método es lento si se ejecuta más de unos pocos microsegundos. Si todos los métodos deConfig
son sencillos y rápidos, entonces yo tomo el enfoque simple y acoplar directamenteExampleA
aConfig
.fuente
Su primer ejemplo es un ejemplo del patrón de inyección de dependencia. Una clase con una dependencia externa recibe la dependencia por constructor, establecedor, etc.
Este enfoque da como resultado un código débilmente acoplado. La mayoría de la gente piensa que el acoplamiento flojo es algo bueno, porque puede sustituir fácilmente la configuración en los casos en que una instancia particular de un objeto necesita configurarse de manera diferente a las demás, puede pasar un objeto de configuración simulado para probar, y así en.
El segundo enfoque está más cerca del patrón creador GRASP. En este caso, el objeto crea sus propias dependencias. Esto da como resultado un código estrechamente acoplado, esto puede limitar la flexibilidad de la clase y dificultar la prueba. Si necesita una instancia de una clase para tener una dependencia diferente de las demás, su única opción es subclasificarla.
Sin embargo, puede ser el patrón apropiado en los casos en que la vida útil del objeto dependiente está dictada por la vida útil del objeto dependiente, y donde el objeto dependiente no se usa en ningún lugar fuera del objeto que depende de él. Normalmente, le recomendaría a DI que sea la posición predeterminada, pero no tiene que descartar el otro enfoque por completo siempre y cuando sea consciente de sus consecuencias.
fuente
Si su clase no expone las
$config
clases externas, entonces la crearía dentro del constructor. De esta manera, mantendrá privado su estado interno.Si los
$config
requisitos requieren que su propio estado interno se configure correctamente (por ejemplo, necesita una conexión de base de datos o algunos campos internos inicializados) antes de su uso, entonces tiene sentido diferir la inicialización a algún código externo (posiblemente una clase de fábrica) e inyectarlo en El constructor. O, como otros han señalado, si necesita ser compartido entre otros objetos.fuente
El Ejemplo A está desacoplado de la clase concreta Config, que es bueno , siempre que el objeto recibido no sea del tipo Config, sino del tipo de una superclase abstracta de Config.
El Ejemplo B está fuertemente acoplado a la configuración de clase concreta, que es mala .
Instanciar un objeto crea un fuerte acoplamiento entre clases. Debe hacerse en una clase de fábrica.
fuente