This is an extremely helpful talk. But I wonder how can the factory / builder step of wiring the dependencies together can work in C++. I.e. we have a class A that depends on B. The builder will allocate B in the heap, pass a pointer to B in A's constructor while also allocating in the heap and return a pointer to A. Who cleans up afterwards? Is it good to let the builder clean up after it's done? It seems to be the correct method since in the talk it says that the builder should setup objects that are expected to have the same lifetime or at least the dependencies have longer lifetime (I also have a question on that). What I mean in code:
class builder { public: builder():m_ClassA(NULL),m_ClassB(NULL) {} ~builder() { if (m_ClassB) { delete m_ClassB; } if (m_ClassA) { delete m_ClassA; } } ClassA *build() { m_ClassB = new class B; m_ClassA = new class A(m_ClassB); return m_ClassA; } };
Now if there is a dependency that is expected to last longer than the lifetime of the object we are injecting it into (say ClassC is that dependency) I understand that we should change the build method to something like:
ClassA *builder::build(ClassC *classC) { m_ClassB = new class B; m_ClassA = new class A(m_ClassB,classC); return m_ClassA; }
Doesn't this bloat the interface of the builder? Is there another way?
If I am understanding your question correctly, and correctly mimicking your C++ syntax, your build method should look more like this:
ClassA *builder::build() { m_ClassB = new class B; m_ClassC = new class C; m_ClassA = new class A(m_ClassB,m_classC); return m_ClassA; }
In other words, you want to concentrate all of the "wiring" information in one place if possible.
In practice, in a large system, this makes for a very bloated builder. The way Guice handles this is that the associations between interfaces and implementations are broken into modules, which are passed to the injector which then uses the accumulated mappings to construct and assemble the necessary objects.
I haven't done C++ in many years so I don't know if there's a C++ equivalent of Guice or Spring but it is worth searching for -- no doubt the authors have thought about the issues of memory lifecycle management that we can ignore in the Java world.
Excellent presentation! The one question I have is regarding the recommendation to not do null checks. Here are a couple thoughts.
1. If House has a Door received in its constructor, it seems that having a Door is an invariant of House. So if House is instantiated without a a Door, it is not in a stable state, and that could impact any of its public methods. For instance, regarding a paint function, the amount of paint needed may be in part based on physical traits of the Door.
2. For unit testing, you can still get the readability of intent with a Null Object, since its type name would indicate it was a Null Object, e.g., NullDoor.
Having the constructor do the null check enables it to throw a controlled exception with meaningful info if a null does get incorrectly passed in during acceptance testing. That can make debugging easier.
So I'm still unclear what value is lost by checking for null values.
This is an extremely helpful talk. But I wonder how can the factory / builder step of wiring the dependencies together can work in C++.
ReplyDeleteI.e. we have a class A that depends on B. The builder will allocate B in the heap, pass a pointer to B in A's constructor while also allocating in the heap and return a pointer to A.
Who cleans up afterwards? Is it good to let the builder clean up after it's done? It seems to be the correct method since in the talk it says that the builder should setup objects that are expected to have the same lifetime or at least the dependencies have longer lifetime (I also have a question on that). What I mean in code:
class builder {
public:
builder():m_ClassA(NULL),m_ClassB(NULL) {}
~builder() {
if (m_ClassB) {
delete m_ClassB;
}
if (m_ClassA) {
delete m_ClassA;
}
}
ClassA *build() {
m_ClassB = new class B;
m_ClassA = new class A(m_ClassB);
return m_ClassA;
}
};
Now if there is a dependency that is expected to last longer than the lifetime of the object we are injecting it into (say ClassC is that dependency) I understand that we should change the build method to something like:
ClassA *builder::build(ClassC *classC) {
m_ClassB = new class B;
m_ClassA = new class A(m_ClassB,classC);
return m_ClassA;
}
Doesn't this bloat the interface of the builder? Is there another way?
Hi Yorgos,
ReplyDeleteIf I am understanding your question correctly, and correctly mimicking your C++ syntax, your build method should look more like this:
ClassA *builder::build() {
m_ClassB = new class B;
m_ClassC = new class C;
m_ClassA = new class A(m_ClassB,m_classC);
return m_ClassA;
}
In other words, you want to concentrate all of the "wiring" information in one place if possible.
In practice, in a large system, this makes for a very bloated builder. The way Guice handles this is that the associations between interfaces and implementations are broken into modules, which are passed to the injector which then uses the accumulated mappings to construct and assemble the necessary objects.
I haven't done C++ in many years so I don't know if there's a C++ equivalent of Guice or Spring but it is worth searching for -- no doubt the authors have thought about the issues of memory lifecycle management that we can ignore in the Java world.
Excellent presentation! The one question I have is regarding the recommendation to not do null checks. Here are a couple thoughts.
ReplyDelete1. If House has a Door received in its constructor, it seems that having a Door is an invariant of House. So if House is instantiated without a a Door, it is not in a stable state, and that could impact any of its public methods. For instance, regarding a paint function, the amount of paint needed may be in part based on physical traits of the Door.
2. For unit testing, you can still get the readability of intent with a Null Object, since its type name would indicate it was a Null Object, e.g., NullDoor.
Having the constructor do the null check enables it to throw a controlled exception with meaningful info if a null does get incorrectly passed in during acceptance testing. That can make debugging easier.
So I'm still unclear what value is lost by checking for null values.