Je travaille sur une application permettant aux classes conçues dans plusieurs groupes:
Les tests unitaires sur mes immutables ont été simples. Je peux utiliser l'injection de dépendance à travers les constructeurs. Cela signifie que je peux échanger dans des classes de test pour vous assurer que je suis testé unitaire (par opposition aux tests d'intégration). Je peux utiliser les constructeurs pour construire mes objets de production. Cet élément de ma conception, je suis satisfait de tests et de production.
Toutefois, sur mes services, je ne semble que ne pouvoir conserver ces classes apatrides et l'unité testable en utilisant une injection de dépendance via les arguments de la méthode.
Pour mes services, un exemple de fonction passe de:
virtual unsigned long foo() const override final;
...à:
virtual unsigned long foo(const ISomeInterface & dependency) const override final;
Cela signifie que mes classes de services sont testables, mais je dois ensuite instancier des dépendances en dehors de la classe lors de l'utilisation du code dans la production. Par exemple:
// without dependency injection
Service service;
return service.foo();
// with dependency injection
Service service;
Dependency dependency;
return service.foo(dependency);
Cela a conduit à un grand nombre de mes classes de service nécessitant au moins un autre argument de plus pour chaque méthode de classe. NOTE - C'est l'approche que j'utilise actuellement dans mon code.
Ma question est la suivante:
Quelles alternatives ai-je à cette forme d'injection de dépendance qui me permettent de:
Remarque - J'effectue également des tests d'intégration, qui teste les dépendances réelles entre les objets.
Si vous n'aimez pas les arguments du constructeur supplémentaires pour les dépendances, vous avez besoin d'un conteneur DI pour gérer la création d'instance pour vous.
Vous pouvez utiliser un cadre de conteneur di-conteneur existant ou mettre en œuvre une version man's pauvre sur votre propre.
public PoorMansDiContainer : IPoorMansDiContainer {
private IService mService = null;
private IFooService mFooService = null;
public IService getSingletonService() {
if (mService == null) {
mService = new ServiceImpl(getSingletonFooService());
}
return mService;
}
public IFooService getSingletonFooService() {
if (mFooService == null) {
mFooService = new FooServiceImpl();
}
return mFooService;
}
}
Vous pouvez configurer vos classes apatrides pour se référer directement à leurs dépendances à l'aide de modèles. Par exemple:
// a dependency interface...
class ILogger
{
public:
virtual void logSomething (std::string message);
};
// an interface for a service
class IMyService
{
public:
virtual std::unique_ptr<MyClass> getMyObject () = 0;
};
template <LogProvider>
class MyServiceImpl : public IMyService
{
public:
virtual std::unique_ptr<MyClass> getMyObject ()
{
return LogProvider::logger->logSomething ("creating my object");
return std::make_unique<MyClass> ();
}
};
// at global level
struct GetLogger
{
static std::shared_ptr<ILogger> logger;
}
// initialisation code...
GetLogger::logger = // ... make a concrete logger here
std::unique_ptr<IMyServce> myService =
std::make_unique<MyServiceImpl<GetLogger>> ();
Vous pouvez ensuite créer des instances de myserviceImpl en utilisant n'importe quelle classe que vous souhaitez contenir un enregistreur. Cette approche crée des données globales, mais comme cela ne l'utilise que indirectement, je ne vois pas de problème avec elle.
Bien que cela dit, j'aurais personnellement abandonner la notion de classes de service apatrides et passez à la place de classes de service immuables, car elle couture une solution beaucoup plus simple.
J'ai réussi à nettoyer mes classes apatrides en utilisant des arguments par défaut. Par exemple,
virtual unsigned long foo(
const ISomeInterface & dependency = ProductionDependency()) const override final;
Je semble avoir rencontré tous les critères de ma question initiale ...
( Test de l'unité des classes apatrides (sans dépendances)
Je peux tester un appareil en spécifiant l'argument (par exemple, passer dans un faux objet de test).
// unit test snippet
Service service;
TestDependency dependency;
const auto actual = service.foo(dependency);
( Gardez ces classes apatrides
Les classes apatrides utilisent toujours uniquement les constructeurs par défaut et n'ont aucun membre de données.
(( Réduit/masquer le code qui instancie les dépendances (en particulier si un objet comporte plusieurs dépendances)
J'avais besoin de modifier les changements suivants:
Cependant, je n'ai plus besoin d'inclure ces dépendances dans mon code client. Par exemple,
// client code snippet
Service service;
return service.foo();
C'est même la même chose si ma classe de services a plusieurs dépendances. Tant que j'utilise des arguments par défaut, je peux utiliser le même extrait que ci-dessus. Les clients n'ont plus besoin de savoir sur les dépendances (ni plus spécifiquement, doivent instantiez des objets de dépendance, car vous l'obtenez gratuitement via des arguments par défaut).
Utilisez un puissant localisateur de services, qui est que Martin Fowler vous dira, presque fonctionnellement équivalent à DI. Ensuite, définissez le service sur tout maquette que vous aimez pour la durée du test dans le contexte dans lequel le test est en cours d'exécution. par exemple. (C # Code, désolé)
public class Service()
{
public void Foo()
{
...
IDependency dependency = ServiceLocator.Get<IDependency>();
...
}
}
Pour le test
ServiceLocator.SetForThisThread<IDependency>(typeof(MockDependency));
RunTest();
Si quelqu'un aimerait se plaindre que vous créez ensuite une dépendance sur le servicelocator, j'aimerais vraiment comprendre comment cela pourrait jamais être un problème.