A month ago I posted a personal UML style guide, without going into very much background as to why I had arrived at that particular subset and that particular usage. And so I’ve been asked to “explain the reasoning” behind point 5:
“I use only two kinds of relationship on a class diagram: «implements» and «uses». No containment or aggregation (even when describing C++ code), no bi-directional relationships, no roles or multiplicities, and no navigability.”
Looking at it again, I can see it does need some explanation! The statement is based on a whole raft of inter-related beliefs that I’ve held for a very long time, and which may require several articles to unravel. Anyway, here’s a beginning…
Firstly, I should reiterate the goals I have when creating a UML model. I use UML under only two circumstances:
- As a design tool, to sketch the main collaborators in a scenario we’re about to implement. This happens very frequently during a project, and is a 3-minute process of sketching on a whiteboard or the back of an envelope. The goal is to develop an outline understanding of how to assign responsibilities among the objects involved in one aspect of a system. This model is almost always a collaboration diagram, and will be thrown away very soon after it was drawn.
- To create a map of a system as an aid to future travellers. I prefer to let the code and the tests do the talking, and so these after-the-fact UML models are intended only as signposts and summaries. Again, I focus on the system’s behaviour, preferring collaboration diagrams, object state diagrams and swim-lane activity diagrams. I do usually include one or two static models, but each will document only a small, tight-knit cluster of classes; I never attempt to use them to show the whole system.
The rest of the time I consider programming languages to be perfectly adequate modelling tools, and I believe whole-heartedly that the source code is the design.
So, back to Dushkin’s question. In my original blog article I was primarily discussing the second use of models – as after-the-fact way-markers. I now see that I raised a couple of points above which need answered before I can continue:
- Why not model all of the system?
- Because programmers are the only people who need to know about the detailed internal design of the whole system, and they have the code to read. For anyone interested in the external design of the whole system, I prefer to provide a suite of automated customer-facing tests. Nevertheless both of these groups of travellers need to know where to begin, and this need can usually be met with a “six object” model of the system (the whole system, but not the whole detail). Both groups often also need models of key business rules. But I believe neither group should spend too long looking at false idols.
- Why focus on the dynamic, rather than the static?
- Because software doesn’t just sit there; software runs. Too often in the past I’ve been surprised when someone has read a class diagram as if it were a data model – or worse, as if the classes in the picture represented objects at run-time. The whole point of the object-oriented paradigm is that the black-box behaviour of a system is divided internally among autonomous agents, each with identity, state and responsibilities; and the externally-visible behaviour of the system emerges from the actions and interactions of those objects. As you can see, I can’t even begin to describe software statically.
So, returning to the original question (why have only «uses» and «implements» relationships on a class model?) once again, I think part of the answer is that I don’t want anyone to mistake my static models for data models. I don’t want the reader to think that there are permanent associations between classes, except where one class extends another or implements an abstract interface.
In thinking about this question I also note that both «uses» and «implements» are uni-directional dependencies. This kind of relationship is important when considering the maintainability of a system: anything that is depended upon is harder to change. Both Bertrand Meyer and Uncle Bob Martin have noted that the more mature and stable classes in a system will tend to be those that are suppliers, while their clients do not carry the same burden. Static models that are described using associations don’t convey this information, and therefore miss a very important aspect of the system’s design. Further, note that dependencies are uni-directional, whereas associations are by default bi-directional, and therefore could be mistaken for being circular. I avoid circular dependencies like the plague, even among tightly-coupled classes, because they represent a brittle design. Again, I like my models to be unambiguous in this regard, and to show only those dependencies that actually exist.
The above explains why I don’t include navigability or multiplicities: I feel they just don’t apply in this kind of model. So to finish, what about aggregation? Well, I consider it to be an implementation detail – one of many possible ways available to the programmer implementing a «uses» relationship. It’s an important detail to the programmer, but it can be discovered from the code. A driver’s roadmap may show certain landmarks, but never shows the building materials that were used in their construction.
I’m sure I’ve missed some points, but I also hope I’ve unravelled at least some of the reasons behind my personal UML style. It works for me…
I’ve now posted a worked example of the above style of static modelling.