TDD is all the rage these days and an ever growing number of software shops are converting to 开发者_JAVA技巧agile,scrum, etc. I can certainly see advantages of automated testing but I also see TDD as contradicting some principles of good object oriented design.
TDD requires you to insert seams in your code which expose implementation details through the interface. Dependency injection or collaborator injection violates the principle of information hiding. If your class uses collaborator classes then the construction of these collaborators should be internal to the class and not exposed through the constructor or interface.
I haven't seen any literature addressing the conflicts between writing testable code and at the same time adhering to the principles of encapsulation, simplicity and information hiding. Have these problems been addressed in any standard way?
The methods and classes you think are implementation details are really seams that represent axes along which you can vary and recompose components into new constellations.
The classic idea about encapsulation tends to be too coarse-grained because when you hide a lot of moving parts you also make the code very inflexible. It also tends to violate a lot of the SOLID principles - most prominently the Single Responsibility Principle.
Most object-oriented design patterns tend to be rather fine-grained and fit well into the SOLID principles, which is another indicator that proper object-oriented design is fine-grained.
If your class uses collaborator classes then the construction of these collaborators should be internal to the class and not exposed through the constructor or interface.
These are two different things mixed together. I agree that in most cases the collaborators should not be exposed through the interfaces. However, exposing them through the constructors are the correct thing to do. Constructors are essentially implementation details of a class while the interfaces provide the real API.
If you want to preserve a coarse-grained API to target default functionality you can still do so by supplying a Facade. See this post for more details: Dependency Inject (DI) "friendly" library
Perhaps there is little literature because it is a false dichotomy?
TDD requires you to insert seams in your code which expose implementation details through the interface.
No, the constructor or method to inject the dependency with need not be part of the interface the calling class uses:
class Zoo {
Animal exhibit;
}
interface Animal {
void walk();
}
class Dog extends Animal {
DogFood food;
Dog(DogFood food) {
this.food = food;
}
}
If your class uses collaborator classes then the construction of these collaborators should be internal to the class and not exposed through the constructor or interface.
In the above example, the Zoo
can not access the DogFood
, as it gets the Dog
after it has already been fed, and the Dog
doesn't expose its food.
Mark Seemann's answer is excellent. "Units" in industry often break the Single Responsibility Principle and make the system difficult to maintain due to all the internally hard-wired dependencies. TDD exposes flaws like this nicely. Units can then be built up like Lego blocks to form larger functional units that actually perform useful business logic. Really, I see TDD as supporting building good OO systems very well.
Don't worry about hiding things so much. Think of private
as meaning "you are not allowed to access this" not "the system does not currently need to access this". Use it carefully - usually for things that screw up the state of an object if accessed from outside at the wrong time.
There are a few things that can be done to integrate TDD with OOP, depending on the language in question. In Java, you can use reflection in order to test private functionality, and the test could be placed in the same package (preferably in a separate source tree) in order to test package-private functionality. Personally, I prefer testing functionality only via the public API of the code in question.
I don't know of any "official" resources on the subject, though I know that Uncle Bob has written extensively on the subject of TDD, and considers it compatible with his "SOLID" principles of OOP.
In my experience TDD is highly supportive of the principles of object oriented design.
Dependency Injection isn't an artifact of TDD, it's commonly used in the design of OO frameworks regardless of the development methodology.
Dependency Injection is about loose coupling - if Class A uses one or more objects from Class b, a good design will minimise the knowledge class A has of the internals of class B. I believe this is what you are referring to when you mention 'information hiding'.
Consider what happens if Class B changes it's implementation. Or, a more complex but still common occasion, what if you want to dynamically substitute in different sub-classes of B depending on the situation (you might be using the Strategy pattern, for example), but the class that makes this decision is not class A.
For this reason Java has Interfaces. Instead of making class A dependent on class B, you make it dependent on the Interface which class B implements. Then you can substitute any class that implements that Interface, without changing the code inside A.
This includes, but is in no way limited to, substituting fake objects for testing purposes.
TDD makes uses of Dependency Injection, absolutely. Just like TDD makes use of many principles of OO. But DI is a principle of OO, not a principle of TDD.
When using dependency injection, the object construction and object graph management will be handled in the factory. Objects are created via factories, so the object creator would not be aware of the dependencies of the created class.
For example if class A has dependencies B and C that are passed via the constructor, B and C will be provided by the factory method. The object creating A would not be aware of the objects B and C.
Dependency injection frameworks like Google Guice and Ninject can be used automate the factory creation.
精彩评论