There are some traits that I use to check my software against. Probably the main one is Reused Abstraction Principle. It says that your abstraction is good only if there are more than one implementation. In other words, it poses that abstractions should be, well, abstract.
Mark Seemann has a nice post about solution space approaches that can lead to better abstractions.
But OOP starts much earlier than you start writing code. It starts when you talk to your domain expert.
Plato, in his Phaedrus dialogue, says that our design should be domain-driven and should start in problem space. He describes this principle as follows:
[The principle of] perceiving and bringing together in one idea the scattered particulars, that one may make clear by definition the particular thing which he wishes to explain; just as now, in speaking of Love [I think Eros was meant here], we said what he is and defined it, whether well or ill. Certainly by this means the discourse acquired clearness and consistency.
This resonates with Parnas’s approach to decomposing system into modules.
When I’ve figured out my domain, I set off to decomposition. Plato describes the main trait that decomposition must possess:
[The principle] of dividing things again by classes, where the natural joints are, and not trying to break any part, after the manner of a bad carver.
Procedural programming proved to be a poor guide to good decomposition. Neither data nor procedures suit for that purpose. But what does? Well, since we’re doing OOP here, no surprise that the answer is behavior. The natural joints mentioned above are the objects’ boundaries.By the way, the same principle applies to identifying service boundaries.
The first one is that the elements resulted from right decomposition are composable, while the number of abstractions is relatively low. It is not a scientific statement, more like empirical. Look at chemistry: less than 130 elements in Periodic table. Euclidean geometry: 13 axioms. Algebra: 10 numbers and a bunch of operations. Reused Abstraction Principle is naturally satisfied.
Composability implies that the entities share the same interface. It means that there are a lot of replaceable things around us. I don’t care what bus takes me to the office — I only care about its number. I don’t care what jeans I wear — the size is probably the only criteria. Or what chair, table or pen I use.
David West in his book Object Thinking describes some useful metaphors that characterize the right decomposition. They all are applied to problem space, i.e., to domain.
The first one is Lego. There are a lot of parts in Lego, but there are way fewer types of parts. And all of the parts are highly composable.The second one is a person metaphor. I used it before I read the book, so it’s the one most natural for me. Even more specific, I conjure not just a person, but an adult person, who makes decisions, tells right from wrong, and decides how to do anything he or she should do.The third one is theater. Every actor knows how to play its role, and every actor is fine with possible replacement of some of their colleagues.The metaphor of object collaboration that I liked is that of an ants. Scientifically proven (there is even a link in a book!) that there is absolutely no orchestration of ants. They all are equal. So as objects should.
I tried to apply these principles in designing DateTime library.I basically need two things: create datetime object out of arbitrary format, and output them in arbitrary format. Hence two interfaces — ISO8601DateTime and FormattedDateTime. Besides I need an interface to work with intervals. Three directories correspond to these interfaces.I also need a possibility to specify datetimes relatively. So I have a timeline directory with three classes: Past, Now and Future.And finally I want a comparison capability, I expressed it with two classes: Min and Max. It is possible to implement any comparison with them.
Here is an example of what can be expressed with this library:
// outputs truevar_dump( (new Max( new Future( new Past( new FromMilliseconds( (new ToMilliseconds( new FromISO8601('2017-08-18T15:08:13+04:00') )) ->value() ), new ISO8601Interval('P1Y2M21DT24H56M26S') ), new ISO8601Interval('PT23H') ), new Now() )) ->equalsTo(new Now()));
Objects are composable, and there are way more classes than interfaces. So my goal is achieved.
This library is just an experiment for now, it lacks some functionality and already has some issues (I don’t like copy-paste in equalsTo()
methods), but the whole decomposition seems to be fine.
So start with problem space — your domain. Use behavior as a decomposition criteria. Remember the metaphors. Let the composability be your goal.