Avoid “holder”, “context”, and “kitchen sink” objects (these take all sorts of other objects and are a grab bag of collaborators). Pass in the specific object you need as a parameter, instead of a holder of that object. Avoid reaching into one object, to get another, etc. Do not look for the object of interest by walking the object graph (which needlessly makes us intimate with all of the intermediary objects. The chain does not need to be deep. If you count more than one period in the chain, you’re looking right at an example of this flaw. Inject directly the object(s) that you need, rather than walking around and finding them.
Warning Signs |
---|
|
Why this is a Flaw
Hard for Testing
If you have to test a method that takes a context object, when you exercise that method it’s hard to guess what is pulled out of the context, and what isn’t cared about. Typically this means we pass in an empty context, get some null pointers, then set some state in the context and try again. We repeat until all the null pointers go away. See also Breaking the Law of Demeter is Like Looking for a Needle in the Haystack.
Recognizing the Flaw
This is also known as a “Train Wreck” or a “Law of Demeter” violation (See Wikipedia on the Law of Demeter).
- Symptom: having to create mocks that return mocks in tests
- Symptom: reading an object named “context”
- Symptom: seeing more than one period “.” in a method chaining where the methods are getters
- Symptom: difficult to write tests, due to complex fixture setup
Example violations:
getUserManager().getUser(123).getProfile().isAdmin() // this is egregiously bad (all you need to know if the user is an admin)
Fixing the Flaw
Instead of looking for things, simply ask for the objects you need in the constructor or method parameters. By asking for your dependencies in constructor you move the responsibility of object finding to the factory which created the class, typically a factory or GUICE.
- Only talk to your immediate friends.
- Inject (pass in) the more specific object that you really need.
- Leave the object location and configuration responsibility to the caller ie the factory or GUICE.
Concrete Code Examples Before and After
Fundamentally, “Digging into Collaborators” is whenever you don’t have the actual object you need, and you need to dig around to get it. Whenever code starts going around asking for other objects, that is a clue that it is going to be hard to test. Instead of asking other objects to get your collaborators, declare collaborators as dependencies in the parameters to the method or constructor. (Don’t look for things; Ask for things!)
Problem: Service Object Digging Around in Value Object
Before: Hard to Test | After: Testable and Flexible Design |
// This is a service object that works with a value // object (the User and amount). class SalesTaxCalculator { TaxTable taxTable; SalesTaxCalculator(TaxTable taxTable) { this.taxTable = taxTable; } float computeSalesTax(User user, Invoice invoice) { // note that "user" is never used directly Address address = user.getAddress(); float amount = invoice.getSubTotal(); return amount * taxTable.getTaxRate(address); } } |
// Reworked, it only asks for the specific objects // that it needs to collaborate with. class SalesTaxCalculator { TaxTable taxTable; SalesTaxCalculator(TaxTable taxTable) { this.taxTable = taxTable; } // Note that we no longer use User, nor do we dig inside // the address. (Note: We would use a Money, BigDecimal, // etc. in reality). float computeSalesTax(Address address, float amount) { return amount * taxTable.getTaxRate(address); } } |
// Testing exposes the problem by the amount of work // necessary to build the object graph, and test the // small behavior you are interested in. class SalesTaxCalculatorTest extends TestCase { SalesTaxCalculator calc = new SalesTaxCalculator(new TaxTable()); // So much work wiring together all the objects needed Address address = new Address("1600 Amphitheatre Parkway..."); User user = new User(address); Invoice invoice = new Invoice(1, new ProductX(95.00)); // ... assertEquals( 0.09, calc.computeSalesTax(user, invoice), 0.05); } |
// The new API is clearer in what collaborators it needs. class SalesTaxCalculatorTest extends TestCase { SalesTaxCalculator calc = new SalesTaxCalculator(new TaxTable()); // Only wire together the objects that are needed Address address = new Address("1600 Amphitheatre Parkway..."); // ... assertEquals( 0.09, calc.computeSalesTax(address, 95.00), 0.05); } } |
This example mixes object lookup with calculation. The core responsibility is to multiply an amount by a tax rate.
- Flaw: To test this class you need to instantiate a User and an Invoice and populate them with a Zip and an amount. This is an extra burden to testing.
- Flaw: For users of the method, it is unclear that all that is needed is an Address and an Invoice. (The API lies to you).
- Flaw: From code reuse point of view, if you wanted to use this class on another project you would also have to supply source code to unrelated classes such as Invoice, and User. (Which in turn may pull in more dependencies)
The solution is to declare the specific objects needed for the interaction through the method signature, and nothing more.
Problem: Service Object Directly Violating Law of Demeter
Before: Hard to Test | After: Testable and Flexible Design |
// This is a service object which violates the // Law of Demeter. class LoginPage { RPCClient client; HttpRequest request; LoginPage(RPCClient client, HttpServletRequest request) { this.client = client; this.request = request; } boolean login() { String cookie = request.getCookie(); return client.getAuthenticator() .authenticate(cookie); } } |
// The specific object we need is passed in // directly. class LoginPage { LoginPage(@Cookie String cookie, Authenticator authenticator) { this.cookie = cookie; this.authenticator = authenticator; } boolean login() { return authenticator.authenticate(cookie); } } |
// The extensive and complicated easy mock usage is
// a clue that the design is brittle.
class LoginPageTest extends TestCase {
public void testTooComplicatedThanItNeedsToBe() {
Authenticator authenticator =
new FakeAuthenticator();
IMocksControl control = EasyMock.createControl();
RPCClient client =
control.createMock(RPCClient.class);
EasyMock.expect(client.getAuthenticator())
.andReturn(authenticator);
HttpServletRequest request =
control.createMock(HttpServletRequest.class);
Cookie[] cookies =
new Cookie[]{new Cookie("g", "xyz123")};
EasyMock.expect(request.getCookies())
.andReturn(cookies);
control.replay();
LoginPage page = new LoginPage(client, request);
// ...
assertTrue(page.login());
control.verify();
}
|
// Things now have a looser coupling, and are more // maintainable, flexible, and testable. class LoginPageTest extends TestCase { public void testMuchEasier() { Cookie cookie = new Cookie("g", "xyz123"); Authenticator authenticator = new FakeAuthenticator(); LoginPage page = new LoginPage(cookie, authenticator); // ... assertTrue(page.login()); } |
The most common Law of Demeter violations have many chained calls, however this example shows that you can violate it with a single chain. Getting the Authenticator from the RPCClient is a violation, because the RPCClient is not used elsewhere, and is only used to get the Authenticator.
- Flaw: Nobody actually cares about the RPCCllient in this class. Why are we passing it in?
- Flaw: Nobody actually cares about the HttpRequest in this class. Why are we passing it in?
- Flaw: The cookie is what we need, but we must dig into the request to get it. For testing, instantiating an HttpRequest is not a trivial matter.
- Flaw: The Authenticator is the real object of interest, but we have to dig into the RPCClient to get the Authenticator.
For testing the original bad code we had to mock out the RPCClient and HttpRequest. Also the test is very intimate with the implementation since we have to mock out the object graph traversal. In the fixed code we didn’t have to mock any graph traversal. This is easier, and helps our code be less brittle. (Even if we chose to mock the Authenticator in the “after” version, it is easier, and produces a more loosely coupled design).
Problem: Law of Demeter Violated to Inappropriately make a Service Locator
Before: Hard to Test | After: Testable and Flexible Design |
// Database has an single responsibility identity // crisis. class UpdateBug { Database db; UpdateBug(Database db) { this.db = db; } void execute(Bug bug) { // Digging around violating Law of Demeter db.getLock().acquire(); try { db.save(bug); } finally { db.getLock().release(); } } } |
// The revised Database has a Single Responsibility. class UpdateBug { Database db; Lock lock; UpdateBug(Database db, Lock lock) { this.db = db; } void execute(Bug bug) { // the db no longer has a getLock method lock.acquire(); try { db.save(bug); } finally { lock.release(); } } } // Note: In Database, the getLock() method was removed |
// Testing even the happy path is complicated with all
// the mock objects that are needed. Especially
// mocks that take mocks (very bad).
class UpdateBugTest extends TestCase {
public void testThisIsRidiculousHappyPath() {
Bug bug = new Bug("description");
// This both violates Law of Demeter and abuses
// mocks, where mocks aren't entirely needed.
IMocksControl control = EasyMock.createControl();
Database db = control.createMock(Database.class);
Lock lock = control.createMock(Lock.class);
// Yikes, this mock (db) returns another mock.
EasyMock.expect(db.getLock()).andReturn(lock);
lock.acquire();
db.save(bug);
EasyMock.expect(db.getLock()).andReturn(lock);
lock.release();
control.replay();
// Now we're done setting up mocks, finally!
UpdateBug updateBug = new UpdateBug(db);
updateBug.execute(bug);
// Verify it happened as expected
control.verify();
// Note: another test with multiple execute
// attempts would need to assert the specific
// locking behavior is as we expect.
}
}
|
// Two improved solutions: State Based Testing
// and Behavior Based (Mockist) Testing.
// First Sol'n, as State Based Testing.
class UpdateBugStateBasedTest extends TestCase {
public void testThisIsMoreElegantStateBased() {
Bug bug = new Bug("description");
// Use our in memory version instead of a mock
InMemoryDatabase db = new InMemoryDatabase();
Lock lock = new Lock();
UpdateBug updateBug = new UpdateBug(db, lock);
// Utilize State testing on the in memory db.
assertEquals(bug, db.getLastSaved());
}
}
// Second Sol'n, as Behavior Based Testing.
// (using mocks).
class UpdateBugMockistTest extends TestCase {
public void testBehaviorBasedTestingMockStyle() {
Bug bug = new Bug("description");
IMocksControl control = EasyMock.createControl();
Database db = control.createMock(Database.class);
Lock lock = control.createMock(Lock.class);
lock.acquire();
db.save(bug);
lock.release();
control.replay();
// Two lines less for setting up mocks.
UpdateBug updateBug = new UpdateBug(db, lock);
updateBug.execute(bug);
// Verify it happened as expected
control.verify();
}
}
|
We need our objects to have one responsibility, and that is not to act as a Service Locator for other objects. When using Guice, you will be able to remove any existing Service Locators. Law of Demeter violations occur when one method acts as a locator in addition to its primary responsibility.
- Flaw: db.getLock() is outside the single responsibility of the Database. It also violates the law of demeter by requiring us to call db.getLock().acquire() and db.getLock().release() to use the lock.
- Flaw: When testing the UpdateBug class, you will have to mock out the Database‘s getLock method.
- Flaw: The Database is acting as a database, as well as a service locator (helping others to find a lock). It has an identity crisis. Combining Law of Demeter violations with acting like a Service Locator is worse than either problem individually. The point of the Database is not to distribute references to other services, but to save entities into a persistent store.
The Database’s getLock() method should be eliminated. Even if Database needs to have a reference to a lock, it is a better if Database does not share it with others. You should never have to mock out a setter or getter.
Two solutions are shown: one using State Based Testing, the other with Behavior Based Testing. The first style asserts against the state of objects after work is performed on them. It is not coupled to the implementation, just that the result is in the state as expected. The second style uses mock objects to assert about the internal behavior of the System Under Test (SUT). Both styles are valid, although different people have strong opinions about one or the other.
Problem: Object Called “Context” is a Great Big Hint to look for a Violation
Before: Hard to Test | After: Testable and Flexible Design |
// Context objects can be a java.util.Map or some // custom grab bag of stuff. class MembershipPlan { void processOrder(UserContext userContext) { User user = userContext.getUser(); PlanLevel level = userContext.getLevel(); Order order = userContext.getOrder(); // ... process } } |
// Replace context with the specific parameters that // are needed within the method. class MembershipPlan { void processOrder(User user, PlanLevel level, Order order) { // ... process } } |
// An example test method working against a // wretched context object. public void testWithContextMakesMeVomit() { MembershipPlan plan = new MembershipPlan(); UserContext userContext = new UserContext(); userContext.setUser(new User("Kim")); PlanLevel level = new PlanLevel(143, "yearly"); userContext.setLevel(level); Order order = new Order("SuperDeluxe", 100, true); userContext.setOrder(order); plan.processOrder(userContext); // Then make assertions against the user, etc ... } |
// The new design is simpler and will easily evolve. public void testWithHonestApiDeclaringWhatItNeeds() { MembershipPlan plan = new MembershipPlan(); User user = new User("Kim"); PlanLevel level = new PlanLevel(143, "yearly"); Order order = new Order("SuperDeluxe", 100, true); plan.processOrder(user, level, order); // Then make assertions against the user, etc ... } } |
Context objects may sound good in theory (no need to change signatures to change dependencies) but they are very hard to test.
- Flaw: Your API says all you need to test this method is a userContext map. But as a writer of the test, you have no idea what that actually is! Generally this means you write a test passing in null, or an empty map, and watch it fail, then progressively stuff things into the map until it will pass.
- Flaw: Some may claim the API is “flexible” (in that you can add any parameters without changing the signatures), but really it is brittle because you cannot use refactoring tools; users don’t know what parameters are really needed. It is not possible to determine what its collaborators are just by examining the API. This makes it hard for new people on the project to understand the behavior and purpose of the class. We say that API lies about its dependencies.
The way to fix code using context objects is to replace them with the specific objects that are needed. This will expose true dependencies, and may help you discover how to decompose objects further to make an even better design.
When This is not a Flaw:
Caveat (Not a Problem): Domain Specific Languages can violate the Law of Demeter for Ease of Configuration
Breaking the Law of Demeter may be acceptable if working in some Domain Specific Languages. They violate the Law of Demeter for ease of configuration. This is not a problem because it is building up a value object, in a fluent, easily understandable way. An example is in Guice modules, building up the bindings.
// A DSL may be an acceptable violation. // i.e. in a GUICE Module's configure method bind(Some.class) .annotatedWith(Annotation.class) .to(SomeImplementaion.class) .in(SomeScope.class);
9 responses so far ↓
Guide to Writing Testable Code | Miško Hevery // Nov 26, 2008 at 11:04 am
[...] Flaw #2: Digging into Collaborators [...]
Hi!
I enjoy your posts and I’ve been inspired to add more test to my code.
My question is if it is a flaw or not if one method has an argument that is both used directly and also to access some sub-object.
For example in the LoginPage, if login() needs both the client and the authenticator, is it okay to just send the client as a parameter, or should one send both client and authenticator?
best regards,
Vlad
Guide to Writing Testable Code / CODEISPOETRY // Dec 10, 2008 at 6:38 pm
[...] Flaw #2: Digging into Collaborators [...]
Great post, Misko. It helps a lot improving the code design by showing the problems of not passing explicitly the objects to methods or constructors. Just one remark: In the “Problem: Law of Demeter Violated to Inappropriately make a Service Locator” section, there’s no assignment to the Lock member variable at the UpdateBug constructor.
Einführung in TDD & Clean Code Talks | geekcloud.org // Nov 22, 2009 at 7:50 am
[...] Flaw #2: Digging into Collaborators [...]
Niceboomer and the future of changes » Blog Archive » Guide: Writing Testable Code // Feb 12, 2010 at 1:51 pm
[...] Flaw #2: Digging into Collaborators [...]
Swiss army knife for XP PHP projects (defining code quality) | Alternate Illusion // Mar 25, 2010 at 10:50 am
[...] Flaw #2: Digging into Collaborators [...]
Linking to Mi?ko’s, Russ’ and my Testability Guide at JAW Speak // Jun 7, 2010 at 9:59 pm
[...] Flaw #2: Digging into Collaborators (link) [...]
I wouldn’t worry about objects passed in but not used directly (well, depending on the use case). Take a look at your first concrete example. The Law of Demeter says use no more than one “dot,” so for example:
Instead of:
public Connection getDbConnection(String dbName, String dbPort, String username, String password) {
return myDb.createConnection(
dbName,
dbPort,
username,
password);
}
This would be preferred:
public Connection getDbConnection(ConnectionInfo connInfo) {
return myDb.createConnection(
connInfo.getDbName(),
connInfo.getDbPort(),
connInfo.getUsername(),
connInfo.getPassword());
}
And in this (fictional) example, creating a ConnectionInfo object during testing is trivial because the ConnectionInfo class is just a bag of properties (model object).
If, later, I need another property, I only have to modify ConnectionInfo and not my method declaration.
Leave a Comment