¿Debo pasar un objeto a un constructor o crear una instancia en clase?

10

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?

Prisionero
fuente
¿Podría ser esto mejor en codereview.stackexchange.com/questions ?
FrustratedWithFormsDesigner
99
@FrustratedWithFormsDesigner: esto no es adecuado para la revisión de código.
ChrisF

Respuestas:

14

Creo que el primero le dará la capacidad de crear un configobjeto en otro lugar y pasárselo ExampleA. 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 un configobjeto 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.

FrustratedWithFormsDesigner
fuente
1
Entonces, A es bueno para pruebas unitarias, B es bueno para otras circunstancias ... ¿por qué no tener ambas? A menudo tengo dos constructores, uno para DI manual, uno para uso normal (uso Unity para DI, que generará constructores para cumplir con sus requisitos de DI).
Ed James
3
@EdWoodcock no se trata realmente de pruebas unitarias en comparación con otras circunstancias. El objeto requiere la dependencia desde el exterior o lo gestiona desde el interior. Nunca ambos / tampoco.
MattDavey
9

¡No te olvides de la capacidad de prueba!

Por lo general, si el comportamiento de la Exampleclase 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 de Exampleclase).

Por lo tanto, iría con la primera opción de ExampleA

Paul
fuente
Es por eso que mencioné las pruebas, gracias por agregar eso :)
Prisionero
5

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.

//Object which manages the lifetime of its dependency (C#):
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA()
    {
        this.Config = new Config(); // Tightly coupled to Config class...
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}

// Object which does not manage its dependency:
public class ClassA
{
    public Config Config { get; set; }

    public ClassA(Config config) // dependency may be injected...
    {
        this.Config = config;
    }
}

// Object which manages its dependency in a testable way:
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA(IConfigFactory configFactory) // dependency may be mocked...
    {
        this.Config = configFactory.BuildConfig();
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}
MattDavey
fuente
2

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.

JBRWilkinson
fuente
1

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 misma Configinstancia en varias clases. Sin embargo, si debería haber solo una Configinstancia dentro de la aplicación completa, considere aplicar el patrón Singleton Configpara evitar tener múltiples instancias de Config. Y si Configes un Singleton, puede hacer lo siguiente en su lugar:

class ExampleA
{
  private $config;
  public function __construct()
  {
     $this->config = Config->getInstance();
  }
}
$exampleA = new ExampleA();

En ExampleB, por otro lado, siempre obtendrá una instancia separada de Configpara cada instancia de ExampleB.

La versión que debe aplicar realmente depende de cómo la aplicación manejará las instancias de Config:

  • si cada instancia de ExampleXdebe tener una instancia separada de Config, vaya con ExampleB;
  • si cada instancia de ExampleXcompartirá una (y solo una) instancia de Config, use ExampleA with Config Singleton;
  • si las instancias de ExampleXpueden usar diferentes instancias de Config, quédese con ExampleA.

Por qué está mal convertirse Configen 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):

  1. Si ExampleArecupera su propia referencia a la Configinstancia, las clases estarán estrechamente acopladas. No habrá forma de tener una instancia de ExampleAusar una versión diferente de Config(digamos alguna subclase). Esto es horrible si quieres probar ExampleAusando una instancia de maqueta Configya que no hay forma de proporcionarla ExampleA.

  2. La premisa de que habrá una, y solo una, instancia de Configtal 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 que Configserán deseables múltiples instancias de , no hay forma de lograr esto sin reescribir el código.

  3. A pesar de que la instancia de one-and-only-one Configes tal vez cierta para toda la eternidad, puede suceder que desee poder usar alguna subclase de Config(mientras todavía tenga una sola instancia). Pero, dado que el código obtiene directamente la instancia a través getInstance()de Config, que es un staticmétodo, no hay forma de obtener la subclase. Nuevamente, el código debe ser reescrito.

  4. El hecho de que los ExampleAusos Configestarán ocultos, al menos cuando solo esté viendo la API de ExampleA. 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 cambios Configsin analizar la implementación de todas las demás clases.

  5. Incluso si el hecho de que ExampleAuse un Singleton Config 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.

gablin
fuente
44
No recomendaría hacerlo ... de esta manera, unirán estrechamente la clase ExampleAy Config, lo que no es algo bueno.
Paul
@Paul: Eso es verdad. Buena captura, no pensé en eso.
gablin
3
Siempre recomendaría no usar Singletons por razones de comprobabilidad. Son esencialmente variables globales y es imposible burlarse de la dependencia.
MattDavey
44
Siempre recomendaría usar nuevamente Singletons por razones de que lo hagas mal.
Raynos
1

Lo más sencillo que hacer es acoplar ExampleAa Config. 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 ExampleAy Configsería mejorar la capacidad de prueba de ExampleA. El acoplamiento directo degradará la capacidad de prueba de ExampleAsi Configtiene 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 de Configson sencillos y rápidos, entonces yo tomo el enfoque simple y acoplar directamente ExampleAa Config.

Kevin Cline
fuente
1

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.

GordonM
fuente
0

Si su clase no expone las $configclases externas, entonces la crearía dentro del constructor. De esta manera, mantendrá privado su estado interno.

Si los $configrequisitos 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.

TMN
fuente
0

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.

Tulains Córdova
fuente