Hi For those who haven't heard about it, SOLID is acronym composed from principles names. (There are 5 of them.) SOLID doesn't have any particular meaning. It's just a good way to remember list of principles. These principles are about low-level design and partly interpretation of OOP basics.
Before doing anything it's good to know “why” you're going to do it. In other words operating without context you're becoming coding monkey or a screwdriver.
No Nerds: paraphrasing Victor Shtern from C++ book “ I wrote a program, guess what it does ” story. I'm so clever, I know how it works, but I won't bother to explain it or explain it well. By explanation mean explaining it personally or in your code (i.e. making your code easy to read). Nerdiness can also be subconscious. IT is full of nerds. No Hacking : “Give me a minute, I'll fix it.” If I can come up with one way to do a task , I'll do it right now no matters whether it fits with design, adds to the system accidental complexity or creates implicit assumptions (for example *MarketDataHandler in importer) . In other words, if something compiles and works, you don't bother finding solutions which make more sense in terms of understanding your code and your intentions. (Hacking is overloaded term. Here it's used in negative meaning.) No Bit-twiddling : Design is more creative (more like art) compared to arcane algorithms, ins and outs of hardware and so on. That is why imho it's less geeky activity.
For me all of the points have background assumption of “literate programming” . Martin Fowler quote: “Any fool can write code that a computer can understand. Good programmers write code that humans can understand” Literate programming is Donald Knuth's term. He meant that “code should read like a book”. What I mean by it is that code is written: 1) for person who's going to read it (and it's likely to happen); 2) for computer to execute. (it's also MF or KB quote)
One of the books which inspired me to change my views on programming is this one. This is also one of the most interesting books about OOP. This is the book where SOLID principles are described . The author is Robert Martin (the same guy who gather Agile Manifesto meeting). Published in 2002. UB was some C++ magazine editor and wrote these principles in the mid-90s. He didn't invented principles, but mostly interpreted from others. Because it was C++ some of them probably made a lot of sense for C++ development (with long compilation time, for example). I bought it accidentally and at the time I had no idea what agile is (in Russian “agile” was translated as “rapid”) Agile in title is misleading . (Tell story from UB forum.)
The next (and last) “why” is “why you should bother to design anything”. Once I read a Alcoa book about project management. And mentioned 5 whys technique (from TPS or Lean?). In my understanding it means “if you a have production problem, you should ask 5 whys to find the root cause and solve the root problem, otherwise you are dealing with symptoms”. If used a bit out of context , applied to what software developers do it boils down to “ we are to make profit ”. We create programs that make profit. And one of the parts of it is writing code.
From this (economic) point of view good design allows us reduce cost and increase quality of code. Maintenance cost – do you know how much money your project costs? How much money since first release? It takes less time to understand and modify a program that has good design. (Doesn't maintenance start after the 1 st month of a project?) Good design is more likely to suit your needs or at least make obvious that it doesn't suit and you have to change it. There is also “Broken window effect” (elaborate on it's origins) . That is when people work with bad code, they tend to write more of it. And the more bad code you have, the more costly maintenance becomes. Errors – good design is less fragile (it's harder to do the wrong thing), less viscous (you don't need to write a lot of code to make simple change). Good understanding – less errors . (Error in production costs way more than error fixed in code editor.) Reuse – agility, mobility, simplicity and care(!) make code reusable. It has a question mark because often it's hard to use something well only once . It's even harder to have “ reuse by design ” (when you intentionally create reusable code). And it's even less likely to have “ emergent reuse ”. Probably, reuse is one of the “broken promises” of OOP and partly it's because of bad designs. It all doesn't mean that you shouldn't strive to achieve reuse.
After having a lunch, sitting in front of display do you think about how much money you make for your employer ? I do :) When depressed. People use more egoistic things as motivation. From this point of view good design can bring you: Peace of mind – when you feel comfortable because you know you don't have to fight with design, crawl through hideous code in debugger after waiting for event from external system (and all that in multi-threaded system) and be aware all along that your efforts are half-doomed. Satisfaction – when feel you've really accomplished something (in reasonable time) and did it well. May be you can even be proud of it. Fun – when you really like what you do. To the point that you don't mind doing it at home and on weekends. Whether people are motivated by designing or coding is personal. Some may really enjoy hacking in a mess. These points are still relevant from economic point of view. They increase team morale and productivity. They support creativity. Hopefully this was convincing, so here are principles.
Principle in this context means heuristic that can help you make code better . They are by no means rules. If they help you write better code, do it. If they don't, don't use them. I don't like retelling other people ideas making it sound like they are mine, so I won't repeat book content but will relay my understanding . Because it's about low-level design, I will use code and UML-like diagrams. I tried to use TG positions “framework” code where possible. (Sorry no IDE, just screenshots :( )
Story about Curley . (Meaning of life – just one thing.) Not a bad piece of advice. There is similar theme in success books about having concrete thing you want to achieve (and writing it down). Principle : “A class should have only one reason to change.” UB reasoning for it is that if class have many reasons to change, you put at risk all other concepts every time you modify the class . The more responsibilities it has, the more invariants they are and the harder it is to understand them. Basically this is cohesion for OOP . (retell Joel podcast with UB on principles and SRP) To me the most valuable thing about this principle is readability of classes that follow SRP. If it does one thing, it's easier to mentally map class name to its behavior. It kind of supports good names . (quote “The most powerful refactoring I know is rename”). To make it more concrete here is a real-life example.
So that examples would make more sense here is some domain background . Instrument is some kind of asset. Instruments can be grouped into Books. Books can be grouped into Portfolios. Position is a value for an instrument (in this context it doesn't matter what this value means). What we want to do is to receive Positions from remote system. Here is the code...
Knowing this you can see this class almost the same way I've seen it for the first time. (Epos3 is the name of a system.) Let's try to guess what this class does and count its responsibilities . Guess meaning from the class name (Node – pipelines analogy. It collects and redistributes something, in this case positions.) AbstractPositionNode (too big hierarchy for a screenshot) deals with grouping positions and here we add code to actually receive something (for this purpose there is Subscriber, SnapshotProvider, HeartbeatMonitor and StateMachine for resubscription logic). This is responsibility of “Node” . Ok. Then you see executor and queue (it's also Executor). This means this class deals with multi-threading. Probably(!), second responsibility. After that there is TibrvDelegate (tibrv is messaging system). Weird... why it's not enough to have Subscriber and SnapshotProvider? Seems this class receives messages from tibrv by itself. Third responsibility. Then there is SnapshotLock, snapshotTimeout and stopper which deal with snapshots. So there is some logic for receiving snapshots. Forth responsibility. Finally there is a bunch of variables about resubscription. (Note there is misplaced constant which should be above in the collapsed block and a variable named as a constant. Kind of small broken windows.) So this class also deals with resubscribing. Fifth responsibility. I missed newSubjectStyle and location fields. Seem to be some kind of hacks. Let's not count them. SRP would certainly make this class better. It makes sense to: - move resubscription logic to StateMachine or separate class - move snapshot handling logic to SnapshotProvider or separate class - don't use networking in this class Beware of increasing conceptual weight of program by introducing more classes).
It's virtually always possible to find several responsibilities in real-world classes. But I like this example even more :) Guess where it violates SRP? It violates SRP by creating Service . One of the points of this example is that SRP is always a judgment call . This class is really simple, so moving creation responsibility won't improve code. As an experiment let's move this responsibility anyway.
Done. Now it's fully SRP-compatible :) If you know about dependency injection, this may look like constructor-based injection of Service.
This is how the previous example works. (Describe sequence.)
Taken one step further to use dependency injection container, the example looks like this. (Describe sequence.) The point is that using dependency injection is a way to extract responsibility of object creation . Although creation of objects can be as simple as using “new” keyword, there are some problems like : - creating objects in a sequence because some of them needs the other; - you find that you need reference to an already created instance, but you don't have reference to it. Now you have to either pass reference around or use singleton pattern . Passing reference might be ok unless there is a lot of them or some objects are needed in the half of all classes. Using singleton might also be ok, but its usage is less explicit than passing a reference (there will an example in the next principle).
Principle: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. (Author is Bertrand Meyer (creator of Eifel)). The motivation is that every time you change code you risk breaking it . Therefore, you should strive to extend code without modification (with as little modification as possible). This is what frameworks supposed to do. They should be open to certain changes without modifying code. ( Convention over configuration in RoR as an example of easy extending taken to extreme?) Being able to add functionality is “open”, not changing source code is “closed”.
Here is an example of framework I used to work with. The idea is that system receives positions from outside world grouping them by instrument or portfolio. You implement several interfaces, plug them in and you have another source of positions. Main interface you have to implement is BookPositionChangeManager (BPCManager for short). Framework will register BPCListeners in BPCManager. When there is position update for instrument from some book, BPCManager should notify listeners for this book. Framework can also ask BPCManager for SnapshotProvider, which can tell positions for all instruments. You have to implement SnapshotProvider too. Here is how all these interface look like in reality.
Listener. Pretty good. SubsriptionListener is just a bunch of constants (old-style, no static imports).
Manager probably is a bit ugly name for this class, Provider could be better. (Why don't use @GodObject annotation if you cannot come up with good name?) There is also inconsistency that Book PositionChangeManager has Portfolio SnapshotProvider. Otherwise, good.
Also quite sane. getSnapshot() methods are similar. initialize...() methods don't matter in this case..
Say, we've implemented BPCManager and SnapshotProvider interfaces. Now we have to tell framework that there is our implementation. This is the only code we need to modify. So this framework is pretty much closed against adding new positions sources. We can add more position sources and this will be the only place we'll have to modify. Say we make this modification, try to run it and guess what? (sorry no unit testing in this project, I suspect developers in this project are too qualified to use them :))
It doesn't work. There are two problems: First problem is that we have to call completeBookPositionChange() after adding a listener. There is nothing about it in javadocs. This is the case when Java interface do not fully describe conceptual interface . (Like in the case of notnull/nullable return value, whether implementations should be thread-safe or we don't care, which thread callback are going to be called, what will happen if callback throws exception, order of method calls, etc... Design by contract can probably solve it, at least partially (?)) Another problem is that we have to invoke raisePositionAlert() on singleton object for “unmapped positions”. (Unmapped positions are positions for instruments which we couldn't find. That is remote system can send position for instrument, we don't know about.) It's a really nasty thing because it doesn't fit with the rest of framework. Probably, singleton was added after designing framework interfaces as a consequence of framework being not open for this kind of change . He had to modify server side code anyway, but he didn't have to create singleton, though. What author could do instead? Add another callback to interfaces? Then he would have to modify all other implementations. This would be more consistent, but on the other hand he would have to make more changes than with singleton. He would have to modify all the other implementations risking to break them, so in a sense adding singleton might be a safer solution. (In this case it's also likely that author just didn't have rights to modify all the clients because of lack of rights to commit changes for all clients. Really strict VCS rules.) To summarize this framework is open-closed in terms of adding new position source. But it doesn't conform to OCP when you think of handling unmapped instruments.
Basically OCP means that we should have “ right” abstractions . Obviously, we cannot have abstractions for any change , but having them for some and making change easy to do is important. Compared to SRP it's difficult to say what you should to do to make you conform to OCP. It's depends very much on context. Paraphrasing Michael Feathers “ OCP is what you can observe if system is designed well” .
This principle is named after Barbara Liskov. Basically it says that subclasses should be substitutable to super classes. Preconditions can be weaker, postconditions stronger. Funnel metaphor (this is mine metaphor, some use Russian dolls metaphor). Motivation for it is to have higher possibility of reuse (because you can freely use subclasses anywhere you have a superclass), to decrease conceptual weight of program (because if abstractions are good, every time you come across a class from hierarchy you can think of it as “some implementation of this concept”). This is basics of using inheritance. Probably it's really important for creating core classes and less of a problem if you have just one weird subclass which is used once only.
Simple example. 1 st code snippet shows violation of parent precondition . 2 nd code snipper violates postcondition with exception (luckily Java knows about it), note that 1 st one doesn't violate by using subclass of IOException. (this is how inheritance with exceptions works in java)
Here is more complicated example. This “classic” Ellipse-Circle (Rectangle-Square) OOP problem. (See http://en.wikipedia.org/wiki/Circle-ellipse_problem for details.) This class represents an ellipse. Besides storing ellipse size, it can calculate its area. Note that this class is not immutable (actually mutability creates the problem).
Here is Circle class which is made Ellipse subclass. Since circles have same sizes in both directions, setters are modified to change both sides.
And here is test code which shows how Circle breaks Ellipse contract . We set width and height for Ellipse and then check its area. Since Circle's setters change both width and height, area for circle is calculated as for circle with 4x4 sides. There is also tricky stuff with equals() contract when adding value-field to subclass class. See Item 8 in Effective Java (page 40 in 2 nd ed) for more details.
Most common LSP violation I've seen was in UI components when “it does almost what I need, if only I could change this and this”. In this case you're at risk of your code being dependent on superclass implementation details and breaking when superclass implementation changes . In ideal world you should use delegation . More evil example of violating LSP is when you do “copy-paste through inheritance”. Don't do it :) Sometimes you may have to violate LSP. Be aware when you do it. It may be a good idea to document it. What about @IViolateLSP annotation?
There is no definition of this principle in the book. Mine definition is “each collaboration of a class which violates SRP should be articulated through separate interface”. This is about clearly defining class contract. Kind of SRP moved to interfaces . (“Interface” here doesn't necessarily means ”java interface”, it can also be abstract class.) In other words it's kind of “ program to interfaces ” adage. Motivation from UB is that when classes using part of interface depend on its other parts, they will “ have to be recompiled, retested every time it changes” (good for java, but probably makes even more sense for C++). For me most value of this principle is having clarity in code. When using just one of interfaces in client code, you can tell for sure which part of object contract is used. (There is also a danger that if you have several fine grained interfaces, it's may happen that as program evolves client code needs more and more features from several interfaces. So you will have to constantly change or merge interfaces.)
Here is another example from positions framework. This is the same Epos3BookPositionNode we've seen in SRP. Methods in this class can be roughly divided into three categories: - overridden methods from superclass - methods called from position framework - methods called only from StateMachine class (these methods are highlighted) (The last two categories have small intersection.) If we were going to apply ISP here, I would choose to extract interface for collaboration between Node and StateMachine (third category).
This is how it would look. StateMachine uses special ResubscribingNode interface. The rest of framework uses superclass of BookPositionNode. The benefit here is that StateMachine doesn't have to know anything about node. In fact I tried doing it. IMO it hasn't really improved BookPositionNode.
This is visualization of what ISP is like in more general form. Each client uses only one interface (or interfaces) it needs.
Next principle is about higher-level design than above principles, since it is about layering application . Dependency Inversion Principle says: 1) High-level modules should not depend on low-level modules. Both should depend on abstractions. 2) Abstractions should not depend upon details. Details should depend upon abstractions. Motivation is that if high-level code depends on low-level code, you'll have to recompile, retest whole project every time low-level code changes. There is an implication that low-level code changes often. For example, it's totally ok to depend on java.lang.String class since its interface won't be broken in next JDK. But if you're in progress of writing your own library for communication with hardware (for example), it's better not to depend on its low-level details.
Here is a simple example. Client class uses Service. Assuming it's likely that Service will change, we don't want high-level code to depend on it.
So we extract an interface. Now Client and ServiceImpl depend on abstraction which conforms to the 1 st point of DIP which says “ High-level modules should not depend on low-level modules. Both should depend on abstractions .” . (Note that Service is now interface . Not using name like IService for interfaces is kind of encapsulation of naming.)
Now let's look at these classes from packaging point of view. The first attempt to package classes may look like this. This is kind of most “natural” way to package classes. We put client classes into “client” package and service classes into “service” package. This is service-centric approach, which implies that we have one service and several clients. Clients adapt to Service. However, there is a subtle problem with it. Since Service interface is packaged as a part of “service” package, it is authors of “service” package who “owns” this interface and decide how to change it. (It may be that you work on your own on the whole project, but there is still difference in your thinking when you have Service interface in “service” package.)
What you can is move Service interface into “client” package. With this packaging Service is part of client. In a sense it's application of programming outside-in on package level . This is client-centric approach, which implies that we have one client and several implementation of services . Services adapt to Client needs. (This kind of packaging is also more likely to yield readable code, since client code is written using client terms, it doesn't have to adapt to Service terms.) Such packaging can also be a consequence of TDD when you write high-level tests first mocking lower-level classes. (TDD is not just about testing, it's can also be design tool.) This packaging satisfies 2 nd point of DIP which says “Abstractions should not depend upon details. Details should depend upon abstractions”. This is the reason principle is has “inversion” in its name.
We can take a step further and extract Service interface into its own package. Now you can think of it as an API specification. This packaging implies there are several clients and several service implementations . This is the hardest way to go, since with several clients and services you'll have to find common ground which satisfies them all. Reusability is hard.
These are the SOLID principles. Besides their impact on code, they can also be used as a common vocabulary to discuss code. Don't underestimate their value. From geeky technical point of view OOP is just: - additional scope (object scope); - “if”s moved into virtual tables; - “copy-paste” through inheritance. But OOP is not only technical tool, it's very much mental tool . Consider subtlety of patterns when read them the first time. It's all the same when you think about these principles . I've known them for ~4 years and I'm still figuring out when and how use(don't use) them. (Nice way to say “I suck at it”.) No silver bullets, of course!
5 SOLID principles is kind of a lot :) But there are even more principles and even scarier acronyms. You can find them in the Uncle Bob book. (story from basketball with luxsoft - don't program if you do it because you couldn't get job as economist)
If someone is telling you about benefits and never mentions problems, be sure you're not getting full picture. What I've be saying is basically that designing is good and worthwhile activity. But there are alternative “schools of thought”. (I heard “school of though” term on Uncle Bob blog.)
When you program, you normally are not doing it just for fun. You're solving a problem. As you know there are problem and solution domains. The above techniques are focused on solution domain. It doesn't mean they imply you shouldn't care about problem domain, but rather emphasize that you can do better in solution domain. These techniques imply that solution domain matters , that you should constantly improve. But there is opinion that technology is not an obstacle. (All technological problems are solved, can be solved or can be replaced with technological problems solvable by scaling, that is having more people working on this). This opinion implies that problem domain is much more important and difficult to get right so it's pointless to care about programming after a certain level. (It depends on application domain, these points are generalization.)
Why should I care about design? Technical debt? Isn't it a geek-term to justify activities they like to do instead of doing real work ? You know your code will never be perfect, so why don't you make it just work and then stop (sounds a lot like XP ;))? If you have problems understanding/modifying program, it's problem with you not with bad code. If program regularly fails, it's you not bad design. Don't trick yourself that TDD, automated testing, Refactoring or Design are investments cloudless future. You don't know whether your code is ever going to be useful, if something doesn't work today, now, it's a waste. Acknowledging that these techniques may help, how do you know when they help, how much money will they safe and who will benefit from it? Will you benefit if in a year some other programmer adds feature 50% faster than he would without your refactoring? But you much more about how you will benefit if you deliver feature 50% faster than your “good designing” mates. (Hint: better position, higher salary.) (“Design” on the slide is umbrella term for all code-improving activities.)
Most of existing code is messy inside. Why don't accept it? Enter Zen Programming. Never think “why”, think only “how” . Everything is beautiful as it's, don't improve anything, unless there is a business need. You live in your code and you know it's won't be what you consider perfect. If you write a piece of code, you know how it works and it doesn't matter how clear and robust it is. You always know how to fix things. You develop in debugger. Good software costs a lot. The trick is that you don't necessarily need it to be that good. Right-brain experience with code matters much more than left-brain logical structures. Dive into code instead of looking at useless diagrams. … Basically what I mean by that is just “hacking” (in a good sense of this word).
There are problems with this approach. It's hard to rely knowledge like this one (for example, compared to SOLID principles). Therefore, it's not scalable. It's hard to find Zen developers who deliver and don't get into total mess. Ideally you should be able to do both styles and know when to use each one. I'm inclined to designing :)
Learning is not push process. It's pull process. You can only learn on your own by reading and trying hard. The best thing presentations can do for you is to inspire. Read the book, practice and write good code.