Dependency Hell - How Does One Pass Dependencies to Deeply Nested Objects

Dependency Hell — how does one pass dependencies to deeply nested objects?

It's a common misconception that dependencies need to be passed through the object graph. To summarize the example Miško Hevery gives in Clean Code: Don't look for things, a House that needs a Door, doesnt need to know about the Lock in the Door:

class HouseBuilder
{
public function buildHouse()
{
$lock = new Lock;
$door = new Door($lock);
$house = new House($door);

return $house;
}
}

As you can see, House is completely oblivious of the fact that the Door in it requires a lock. It's the responsibility of the HouseBuilder to create all the required dependencies and stack them together as they are needed. From the inside out.

Consequently, in your scenario you have to identify which objects should operate on which dependencies (cf Law of Demeter). Your Builder then has to create all collaborators and make sure the dependencies are injected into the appropriate objects.

Also see How to Think About the “new” Operator with Respect to Unit Testing

Micro-Dependencies, Avoiding Coupling, and Objects Creating Objects

Since your Log object is really just a Data Transfer Object or Value Object, you can create it inside the Logger class. It's okay to do so in this case. You dont need to pass anything to the Logger. But you are right in that you wont be able to mock/stub this easily then.

As an alternative, you could also inject a Factory if you want to decouple the Log class from the Logger:

$logger = new Logger($logWriter, new LogFactory);

and then create the Log Type from there:

public function write($message)
{
$log = $this->logFactory->createNew();

This capsules the creation logic inside the Factory class. The Factory will still have the Log type hardcoded inside, but it's okay for Factories to have that. You then just test that it returns the right type when you call createNew. And in your consumers, you can stub that call.

If you dont feel like creating a full-blown Factory class for this, you can also use a Lambda instead of a Factory. Since it captures the essential creation logic, it's effectively the same as a Factory, just without a class:

$logger = new Logger($logWriter, function() { return new Log; });

And then

public function write($message)
{
$log = call_user_func($this->createLogCallback);

Both, the Factoy and the Lambda approach allow for substituting the Log type in your Unit-Test. Then again, substituting the Log type doesn't seem that necessary in your scenario. The Log type doesn't have any dependencies of it's own, so you can pretty much use the real deal here. You can easily verify your write method by simply looking at what gets written by the LogWriter. You won't have an explicit assertion on a Log Mock, but if the writer produces the expected output for the given input to write, you can safely assume that the Log type collaborates as expected.

Also see http://misko.hevery.com/2008/09/30/to-new-or-not-to-new for more details.

Dealing with dependencies

There is one rule you can use - if the class instance is useless without the dependencies, i.e. the dependencies are "must have" / mandatory, they should be passed to constructor.

The idea is that the object should be fully (as possible) initialized after the constructor is called. After all that's what constructors are for, initialization.

Consequently should be optional dependencies set using setters.

Design patterns for often used and dependent objects in PHP

The common patterns dealing with object creation are the Gang of Four Creational Patterns.

Quoting Wikipedia:

Creational patterns are ones that create objects for you, rather than having you instantiate objects directly. This gives your program more flexibility in deciding which objects need to be created for a given case.

  • Abstract Factory groups object factories that have a common theme.
  • Builder constructs complex objects by separating construction and representation.
  • Factory Method creates objects without specifying the exact class to create.
  • Prototype creates objects by cloning an existing object.
  • Singleton restricts object creation for a class to only one instance.

For a general rule of thumb where to put the responsibility to create an instance, have a look at the Creator pattern in Wikipedia's article about GRASP:

In general, a class B should be responsible for creating instances of class A if one, or preferably more, of the following apply:

  • Instances of B contain or compositely aggregate instances of A
  • Instances of B record instances of A
  • Instances of B closely use instances of A
  • Instances of B have the initializing information for instances of A and pass it on creation.

The GoF Creational Patterns complement this. For instance, if you use a Factory pattern, the Factory is likely to hold the initializing information for instances of A and will pass it on creation:

class BMWFactory implements CarFactory
{
// properties, ctors and stuff …

public function createCar($color)
{
return new Bmw(
$color,
// … more arguments
);
}
}

On the other hand, it also means that a Collection may be allowed to create new instances because B then "contains or compositely aggregate instances of A.", which would lead to something like

class Cars
{
// properties, ctors and stuff …

public function createCar($color)
{
return new Car(/* … */);
}
}

$bmws = new Cars($config['bmw']);
$bmws[] = $bmws->createCar('alpine white');

We could argue whether it's a good idea to have your collections create objects over just storing them when the created objects have an independent lifecycle. If the As created by B have the same lifetime, it's a different thing.

Instead of having B create As, you could also delegate the creation to a Factory:

class Cars
{
// properties, ctors and stuff …

public function createCar($color)
{
return $this->carFactory->createNewCar(/* … */);
}
}
$bmws = new Cars(new BmwFactory);
$bmws[] = $bmws->createCar('alpine white');

The reason why you should always carefully consider where to to use new in your code is because it introduces strong coupling from B to A. Injecting the Factory loosens that coupling, especially since we programmed to an interface instead of the concrete Class.

Strong coupling makes it difficult to mock collaborators in your unit-tests. Quoting Miško Hevery's How to Think About the “new” Operator with Respect to Unit Testing

But the reason why Dependency Injection is so important is that within unit-tests you want to test a small subset of your application. The requirement is that you can construct that small subset of the application independently of the whole system. If you mix application logic with graph construction (the new operator) unit-testing becomes impossible for anything but the leaf nodes in your application.

The idea of separating creation from application logic is further explained in his Google Talk, which is linked in my related answer Dependency Hell — how does one pass dependencies to deeply nested objects?. The basic idea is to assemble everything you can reasonably assemble up-front in Factories or Builders or via a Dependency Injection Container.

TL;DR

if you want to create objects in a local scope (because it depends on runtime information), encapsulate the Creation logic into Factories or Builders and inject these into the objects that hold the runtime information and delegate the information to the Factories and Builders from there when needed.

PHP: DI Container

Yes, DICs need upfront Configuration, either in some config file or through stacking together factory closures or by annotating your source code.

Yes, it is easier to create instances in the ctor, but that will eliminate all the benefits of DI, because you are not injecting the dependencies anymore.

Also note that you dont need a DIC in order to do DI. DI is simply the act of injecting dependencies in your code through constructors, setters or the using methods.

Class Structure and Passing Data

You can try abstract class and static property to share it with all your instance

Flutter Provider Nested Objects

EDIT: answer to the updated question, original below

It was not clear what A, B, C and D stood for in your original question. Turns out those were models.

My current thinking is, wrap your app with MultiProvider/ProxyProvider to provide services, not models.

Not sure how you are loading your data (if at all) but I assumed a service that asynchronously fetches your fleet. If your data is loaded by parts/models through different services (instead of all at once) you could add those to the MultiProvider and inject them in the appropriate widgets when you need to load more data.

The example below is fully functional. For the sake of simplicity, and since you asked about updating name as an example, I only made that property setter notifyListeners().

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

main() {
runApp(
MultiProvider(
providers: [Provider.value(value: Service())],
child: MyApp()
)
);
}

class MyApp extends StatelessWidget {
@override
Widget build(context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Consumer<Service>(
builder: (context, service, _) {
return FutureBuilder<Fleet>(
future: service.getFleet(), // might want to memoize this future
builder: (context, snapshot) {
if (snapshot.hasData) {
final member = snapshot.data.aircrafts[0].crewMembers[1];
return ShowCrewWidget(member);
} else {
return CircularProgressIndicator();
}
}
);
}
),
),
),
);
}
}

class ShowCrewWidget extends StatelessWidget {

ShowCrewWidget(this._member);

final CrewMember _member;

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<CrewMember>(
create: (_) => _member,
child: Consumer<CrewMember>(
builder: (_, model, __) {
return GestureDetector(
onDoubleTap: () => model.name = 'Peter',
child: Text(model.name)
);
},
),
);
}
}

enum Manufacturer {
Airbus, Boeing, Embraer
}

class Fleet extends ChangeNotifier {
List<Aircraft> aircrafts = [];
}

class Aircraft extends ChangeNotifier {
Manufacturer aircraftManufacturer;
double emptyWeight;
double length;
List<Seat> seats;
Map<int,CrewMember> crewMembers;
}

class CrewMember extends ChangeNotifier {
CrewMember(this._name);

String _name;
String surname;

String get name => _name;
set name(String value) {
_name = value;
notifyListeners();
}

}

class Seat extends ChangeNotifier {
int row;
Color seatColor;
}

class Service {

Future<Fleet> getFleet() {
final c1 = CrewMember('Mary');
final c2 = CrewMember('John');
final a1 = Aircraft()..crewMembers = { 0: c1, 1: c2 };
final f1 = Fleet()..aircrafts.add(a1);
return Future.delayed(Duration(seconds: 2), () => f1);
}

}

Run the app, wait 2 seconds for data to load, and you should see "John" which is crew member with id=1 in that map. Then double-tap the text and it should update to "Peter".

As you can notice, I am using top-level registering of services (Provider.value(value: Service())), and local-level registering of models (ChangeNotifierProvider<CrewMember>(create: ...)).

I think this architecture (with a reasonable amount of models) should be feasible.

Regarding the local-level provider, I find it a bit verbose, but there might be ways to make it shorter. Also, having some code generation library for models with setters to notify changes would be awesome.

(Do you have a C# background? I fixed your classes to be in line with Dart syntax.)

Let me know if this works for you.


If you want to use Provider you'll have to build the dependency graph with Provider.

(You could choose constructor injection, instead of setter injection)

This works:

main() {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<D>(create: (_) => D()),
ChangeNotifierProxyProvider<D, C>(
create: (_) => C(),
update: (_, d, c) => c..d=d
),
ChangeNotifierProxyProvider<C, B>(
create: (_) => B(),
update: (_, c, b) => b..c=c
),
ChangeNotifierProxyProvider<B, A>(
create: (_) => A(),
update: (_, b, a) => a..b=b
),
],
child: MyApp(),
));
}

class MyApp extends StatelessWidget {
@override
Widget build(context) {
return MaterialApp(
title: 'My Flutter App',
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Current selected Color',
),
Consumer<D>(
builder: (context, d, _) => Placeholder(color: d.color)
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => Provider.of<D>(context, listen: false).color = Colors.black,
tooltip: 'Increment',
child: Icon(Icons.arrow_forward),
),
),
);
}
}

This app works based on your A, B, C and D classes.

Your example does not use proxies as it only uses D which has no dependencies. But you can see Provider has hooked up dependencies correctly with this example:

Consumer<A>(
builder: (context, a, _) => Text(a.b.c.d.runtimeType.toString())
),

It will print out "D".

ChangeColor() did not work because it is not calling notifyListeners().

There is no need to use a stateful widget on top of this.

How to mock Python classes when nested several dependencies deep

The mock module does indeed seem to be a good fit in this case. You can specify the mock (your mock) to use when calling the various patch methods.

If you are importing only the class rather than the module you can patch the imported DependentDependentClass in DependentClass:

import .DependentClass as dependent_class
from .DependentDependentClass import MockDependentDependentClass

with patch.object(dependent_class, 'DependentDependentClass', MockDependentDependentClass):
# do something while class is patched

Alternatively:

with patch('yourmodule.DependentClass.DependentDependentClass', MockDependentDependentClass):
# do something while class is patched

or the following will only work if you are accessing the class via a module or import it after it is being patched:

with patch('yourmodule.DependentDependentClass.DependentDependentClass', MockDependentDependentClass):
# do something while class is patched

Just bare in mind what object is being patched, when.

Note: you might find it less confusing naming your files in lower case, slightly different to the embedded class(es).

Note 2: If you need to mock a dependency of a dependency of the module under test then it might suggest that you are not testing at the right level.



Related Topics



Leave a reply



Submit