Suppose we have 4 coupled/chained objects: A, B, C, D, accessible through the properties A.B.C.D, and the objects have the following properties:
A: B, A1, A2
B: C, B1, B2, B3
C: D, C1, C2
D: D1, D2, D3
Usually within a certain area/component, we don't need all that object tree, but just a projection/subset of it: eg. IView { A1, B1, C1, D1. }
In practice, the problem is that most of the people will use/scatter these properties all over their code, leading to maintenance monsters:
- navigation is all over the place, violating the 'Law of Demeter'.
- Testing is getting a lot more difficult: in order to test a user of IView you need to instatiate A, B, C, D, these might have good constructors which force you to instatiate other X objects, etc.
- The amount of code using chained objects/train wrecks will crush future refactorings: if you'll want to reverse the navigation from B.C to C.B, you'll have to change all the usage points, the same goes for changing a data type.
Possible solutions:
a. Define the projection as required by that component/use case, eg IView { A1, B1, C1, D1 } (properties, or getter/setter, etc.)
- Define a repository service IRepository.get(...) -> IView
- Work against your abstraction IRepository/IView
The IView/IRepository could build through:
- DTOs mapping (a la dozer & co.)
- wrap & lazy fetch:
View {
ctor(A a) { _a = a; }
A1 { _a.A1 }
B1 { _a.B.B1}
C1 { _a.B.C.C1 }
D1 { _a.B.C.D1 }
}
(a pretty slick solution might be build with LINQ expressions)
In this way we have isolated our component from external changes, we are depending only on our IRepository/IView, future refactorings will afect only that area.
The IRepository/IView abstractions represent the membrane to the external world. (Ralf refers to the pattern as 'external adapter', probably refering to Alistair Cockburn's 'Hexagonal Architecture' paper, I prefer the Alan Kay's metaphor).
b. Instead of defining the projection IView we could add the required operations on A. This has the benefit of aggregating/reusing common operations on A (usually a root entity).
In some cases it might lead to the polution of A: all the properties/operations beneath the A will emerge into A, transforming the tree into a list. Class A will become too big, containing its data, and n+ operations (C# has region-folding, smalltalk has protocols for organizing big classes)
Final thoughts:
- it is not about DDD/Tell don't Ask vs EJB/DTOs vs XXX, but is about isolation/visibility.
- (Unit) Testing asap helps a lot identifying architecture/design smells.
Friday, January 16, 2009
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment