35

I understand what SOLID is supposed to accomplish and use it regularly in situations where modularity is important and its goals are clearly useful. However, two things prevent me from applying it consistently across my codebase:

  • I want to avoid premature abstraction. In my experience drawing abstraction lines without concrete use cases (the kind that exist now or in the foreseeable future) leads to them being drawn in the wrong places. When I try to modify such code, the abstraction lines get in the way rather than helping. Therefore, I tend to err on the side of not drawing any abstraction lines until I have a good idea of where they would be useful.

  • I find it hard to justify increasing modularity for its own sake if it makes my code more verbose, harder to understand, etc. and doesn't eliminate any duplication. I find simple, tightly coupled procedural or God object code is sometimes easier to understand than very well-factored ravioli code because the flow is simple and linear. It's also much easier to write.

On the other hand, this mindset often leads to God objects. I generally refactor these conservatively, adding clear abstraction lines only when I see clear patterns emerging. What, if anything, is wrong with God objects and tightly coupled code if you don't clearly need more modularity, don't have significant duplication and the code is readable?

EDIT: As far as individual SOLID principles, I meant to emphasize that Liskov Substitution is IMHO a formalization of common sense and should be applied everywhere, since abstractions make no sense if it isn't. Also, every class should have a single responsibility at some level of abstraction, though it may be a very high level with the implementation details all crammed into one huge 2,000 line class. Basically, your abstractions should make sense where you choose to abstract. The principles I question in cases where modularity isn't clearly useful are open-closed, interface segregation and especially dependency inversion, since these are about modularity, not just having abstractions make sense.

12
  • 11
    @S.Lott: The key word there is "more". This is precisely the sort of thing YAGNI was invented for. Increasing modularity or decreasing coupling simply for its own sake is just cargo-cult programming. Commented Apr 6, 2011 at 18:28
  • 3
    @S.Lott: That should be obvious from the context. As I read it, at least, "more than currently exists in the code in question." Commented Apr 6, 2011 at 18:43
  • 3
    @S.Lott: If you insist on being pedantic, "more" refers to more than exists currently. The implication is that something, whether code or a rough design, already exists and you're thinking about how to refactor it.
    – dsimcha
    Commented Apr 6, 2011 at 19:00
  • 6
    @back2dos: Right, but I'm skeptical of engineering something for extensibility without a clear idea of how it's likely to be extended. Without this information, your abstraction lines will probably end up in all the wrong places. If you don't know where abstraction lines will likely be useful, I'm suggesting that you should just write the most concise code that works and is readable, even if you have a few God objects thrown in there.
    – dsimcha
    Commented Apr 6, 2011 at 22:11
  • 3
    @dsimcha: You're lucky if requirements change after you wrote a piece of code, because usually they change while you do it. This is why snapshot-implementations aren't suitable to survive an actual software life cycle. Also, SOLID doesn't demand you to just abstract blindly. The context for an abstraction is defined through dependency inversion. You reduce dependency of every module to the most simple and clear abstraction of other services it relies on. It is not arbitrary, artificial or complex, but well defined, plausible and simple.
    – back2dos
    Commented Apr 6, 2011 at 22:38

12 Answers 12

9

The following are simple principles you can apply to helping you understand how to balance your system design:

  • Single Responsibility Principle: the S in SOLID. You can have a very large object in terms of number of methods or the amount of data and still be upholding this principle. Take for instance the Ruby String object. This object has more methods than you can shake a stick at, but it still only has one responsibility: hold a string of text for the application. As soon as your object starts to take on a new responsibility, start thinking hard about that. The maintenance issue is asking yourself "where would I expect to find this code if I had problems with it later?"
  • Simplicity counts: Albert Einstein said "Make everything as simple as possible, but not simpler." Do you really need that new method? Can you accomplish what you need with the existing functionality? If you really think there needs to be a new method, can you change an existing method to satisfy all of what you need? In essence refactor as you build new stuff.

In essence you are trying to do what you can to not shoot yourself in the foot when it comes time to maintain the software. If your large objects are reasonable abstractions, then there is little reason to split them up just because someone came up with a metric that says a class should be no more than X lines/methods/properties/etc. If anything, those are guidelines and not hard and fast rules.

1
  • 3
    Good comment. As discussed in other answers, one problem with "single responsibility" is that a class's responsibility can be described at a variety of abstraction levels.
    – dsimcha
    Commented Apr 6, 2011 at 21:20
5

I think for the most part you have answered your own question. SOLID is set of guidelines on how to refactor your code, when concepts needs to be move up levels of abstractions.

Like all other creative disciplines there are no absolutes - just tradeoffs. The more you do it the "easier" it becomes to decide when is enough for your current problem domain or requirements.

Having said that - abstracting is the heart of software development - so if there is no good reason not to do it, then practice will make you better at it, and you will get a gut feel for the tradeoffs. So favor it unless it becomes detrimental.

0
5
I want to avoid premature abstraction.In my experience drawing abstraction lines without concrete use cases... 

This is partially right and partially wrong.

Wrong
This is not a reason to prevent one from applying OO principles/SOLID. It only prevents applying them too early.

Which is...

Right
Do not refactor code until it is refactorable; when it is requirements complete. Or, when the "use cases" are all there as you say.

I find simple, tightly coupled procedural or God object code is sometimes easier to understand than very well-factored...

When working alone or in a silo
The problem with not doing OO isn't immediately obvious. After you write the first version of a program:

Code is fresh in your head
Code is familiar
Code is easy to mentally compartmentalize
You wrote it

3/4 of these good things quickly die over a short time.

Finally, you recognize that you have God objects (Objects with many functions), so you recognize individual functions. Encapsulate. Put them in folders. Use interfaces once in a while. Do yourself a favor for long term maintenance. Especially since non-refactored methods tend to bloat infinitely and become God methods.

In a team
The problems with not doing OO are immediately noticeable. Good OO code is at least somewhat self-documenting and readable. Especially when combined with good green code. Also, lack of structure makes it difficult to separate tasks and integration much more complex.

3
  • Right. I recognize vaguely that my objects do more than one thing, but I don't recognize more concretely how to usefully break them apart so that the abstraction lines will be in the right places when maintenance needs to be done. It seems like a waste of time to refactor if the maintenance programmer will probably have to do a lot more refactoring to get the abstraction lines in the right place anyhow.
    – dsimcha
    Commented Apr 6, 2011 at 20:04
  • Encapsulation and abstraction are two different concepts. Encapsulation is a basic OO concept that basically means you have classes. Classes provide modularity and readability in and of themselves. That is useful. Commented Apr 6, 2011 at 20:08
  • Abstraction can decrease maintenance when applied properly. Applied improperly it can increase maintenance. Knowing when and where to draw the lines requires a solid understanding of the business, framework, customer in addition to the basic technical aspects of just how to "apply a factory pattern" per se. Commented Apr 6, 2011 at 20:10
4

There's nothing wrong with large objects and tightly coupled code when they're appropriate and when nothing better-engineered is required. This is just another manifestation of a rule of thumb turning into dogma.

Loose coupling and small, simple objects tend to provide specific benefits in a lot of common cases, so it's a good idea to use them, in general. The problem lies in people who don't understand the rationale behind the principles blindly trying to apply them universally, even where they don't apply.

5
  • 2
    +1. Still need a good definition of what "appropriate" means.
    – jprete
    Commented Apr 6, 2011 at 18:47
  • 2
    @jprete: Why? Trying to apply absolute definitions is part of the cause of conceptual messes like this. There's a lot of craftsmanship involved in building good software. What's really needed IMO is experience and good judgment. Commented Apr 6, 2011 at 18:57
  • 4
    Well, yes, but a broad definition of some sort is still useful.
    – jprete
    Commented Apr 6, 2011 at 19:06
  • 2
    Having a definition of appropriate would be useful, but that's rather the point. It's difficult to pin down in a sound bite because it's only applicable on a case by case basis. Whether you make the appropriate choice on each case is largely down to experience. If you can't articulate why you are your high cohesion, monolithic solution is better than a low cohesion, highly modular solution, you would "probably" be better off going low cohesion and modular. It's always easier to refactor.
    – Ian
    Commented Apr 6, 2011 at 21:27
  • 1
    I prefer someone blindly writing decoupled code instead of blindly writing spaghetti code :) Commented Sep 3, 2011 at 10:13
3

I tend to approach this more from the You Ain't Gonna Need It standpoint. This post will focus on one item in particular: inheritance.

In my initial design I create a hierarchy of things I know there will be two or more of. Chances are they will need much of the same code, so it's worth designing that it from the start. After the initial code is in place, and I need to add more features/functionality, I look at what I have and think, "Does something I have already implement the same or similiar function?" If so, that's most likely a new abstraction begging to be released.

A good example of this is an MVC framework. Starting from scratch, you create one big page with a code behind. But then you want to add another page. However, the one code behind already implements a lot of the logic the new page requires. So, you abstract out a Controller class/interface that will implement the logic specific to that page, leaving the common stuff in the original "god" code-behind.

2

If YOU know as a developer when to NOT apply the principles because of the future of the code, then good for you.

I hope SOLID remains in the back of your mind to know when things do need to be abstracted to avoid complexity and improve the quality of the code if it's beginning to degrade in the values you stated.

More importantly though, consider that the majority of developers are doing the day job and don't care as much as you. If you initially set out long running methods, what are the other developers that come in to the code going to think when they come to maintain it or extend it?... Yes, you guessed, BIGGER functions, LONGER running classes, and MORE god objects, and they don't have the principles in mind to be able to catch the growing code and correctly encapsulate it. You're "readable" code now becomes a rotting mess.

So you may argue that a bad developer is going to do that regardless, but at least you have a better advantage if things are kept simple and well organised from the start.

I don't particularly mind a global "Registry Map" or a "God Object" if they're simple. It really depends on the system design, sometimes you can get away with it and remain simple, sometimes you can't.

Let us also not forget that large functions and God Objects can prove extremely difficult to test. If you want to remain Agile and feel "Safe" when re-factoring and changing code, you need tests. Tests are difficult to write when functions or objects do many things.

2
  • 1
    Basically a good answer. My one gripe is that, if some maintenance developer comes along and makes a mess, that's his/her fault for not refactoring, unless such changes seemed likely enough in foresight to justify planning for them or the code was so poorly documented/tested/etc. that refactoring would have been a form of insanity.
    – dsimcha
    Commented Apr 7, 2011 at 16:21
  • That's true dsimcha. It's just that I thought in a system where a developer creates a folder "RequestHandlers" and each class under that folder is a request handler, then the next developer that comes along and thinks, "I need to put in a new request handler", the current infrastructure almost forces him down the route to follow the convention. I think this is what I was trying to say. Setting up an obvious convention from the start, can guide even the most inexperienced developers to continue the pattern. Commented Apr 7, 2011 at 19:15
2

I use a simpler guideline: if you can write unit tests for it, and have no code duplication, it's abstract enough. Plus, you're in a good position for later refactoring.

Beyond that, you should keep SOLID in mind, but more as a guideline than a rule.

1

The problem with a god object is that you can usually profitably break it into pieces. By definition, it does more than one thing. The whole purpose of breaking it into smaller classes is so that you can have a class that does one thing well (and you shouldn't stretch the definition of "one thing" either). This means that, for any given class, once you know the one thing it's supposed to do, you should be able to read it fairly easily and say whether or not it's doing that one thing correctly.

I think there is such a thing as having too much modularity, and too much flexibility, but that's more of an over-design and over-engineering problem, where you're accounting for requirements that you don't even know the customer wants. A lot of design ideas are oriented around making it easy to control changes to how the code is run, but if nobody expects the change, then it's pointless to include the flexibility.

3
  • "Does more than one thing" can be difficult to define, depending on what level of abstraction you're working at. It could be argued that any method with two or more lines of code is doing more than one thing. At the opposite end of the spectrum, something complex like a scripting engine is going to need a whole lot of methods that all do individual "things" that are not directly related to each other, but each comprise an important part of the "meta-thing" which is getting your scripts to run properly, and there's only so much breaking apart that can be done without breaking the script engine. Commented Apr 6, 2011 at 18:55
  • There is definitely a spectrum of conceptual levels, but we can pick a point on it: the size of a "class that does one thing" should be such that you can almost immediately understand the implementation. I.e. the implementation details fit in your head. If it gets bigger, then you can't understand the whole class at once. If it's smaller, then you are abstracting too far. But as you said in another comment, there's a lot of judgment and experience involved.
    – jprete
    Commented Apr 6, 2011 at 19:14
  • IMHO the best level of abstraction to think at is the level that you expect consumers of the object to use. Say you have an object called Queryer that optimizes database queries, queries the RDBMS and parses the results into objects to return. If there's only one way to do this in your app and Queryer is hermetically sealed and encapsulates this one way, then it only does one thing. If there are multiple ways to do this and someone might care about the details of one part of it, then it does multiple things and you might want to split it up.
    – dsimcha
    Commented Apr 6, 2011 at 23:38
1

I share your concerns about excessive and inappropriate abstraction, but I'm not necessarily so concerned about premature abstraction.

That probably sounds like a contradiction, but using an abstraction is unlikely to cause a problem so long as you don't overcommit to it too early - ie as long as you are willing and able to refactor if necessary.

What this implies is some degree of prototyping, experimentation and backtracking in the code.

That said, there's no simple deterministic rule to follow that will solve all problems. Experience counts for a lot, but you have to gain that experience by making mistakes. And there's always more mistakes to make that previous mistakes won't have prepared you for.

Even so, treat the textbook principles as a starting point, then learn programming lots and seeing how those principles work out. If it were possible to give better, more precise and reliable guidelines than things like SOLID, someone would probably have done so now - and even if they had, those improved principles would still be imperfect, and people would be asking about the limitations in those.

Good job too - if anyone could provide a clear, deterministic algorithm for designing and coding programs, there'd only be one last program for humans to write - the program that would automatically write all future programs without human intervention.

0

What, if anything, is wrong with God objects and tightly coupled code if you don't clearly need more modularity, don't have significant duplication and the code is readable?

If the application is small enough, anything is maintainable. In larger applications, God objects quickly become a problem. Eventually implementing a new feature requires modifying 17 God objects. People don't do very well following 17-step procedures. The God objects are constantly being modified by multiple developers, and those changes have to be merged repeatedly. You don't want to go there.

0

Drawing the appropriate abstraction lines is something one draws from experience. You will make mistakes in doing so that is undeniable.

SOLID is a concentrated form of that experience that is handed to you by people who gained this experience the hard way. You pretty much nailed it when you say you would refrain from creating abstraction before you see a need for it. Well the experience SOLID provides you will help you solve problems you never new existed. But trust me... there are very real.

SOLID is about modularity, modularity is about maintainability, and maintainability is about allowing you to keep a humane work schedule once the software reach production and client start noticing bugs you have not.

The biggest benefit of modularity is testability. The more modular your system is the easier it is to test and the faster you can build solid foundations to get on with the harder aspects of your problem. So your problem may not demand this modularity but just the fact it will allow you to create better testing code faster is a no brainer to strive for modularity.

Being agile is all about attempting to strike the delicate balance between shipping fast and providing good quality. Agile does not mean that we should cut corner, in fact, the most successful agile projects I have been involved with are the ones that paid close attention to such guidelines.

0

An important point that seems to have been overlooked is that SOLID is practiced along with TDD. TDD tends to highlight what is "appropriate" in your particular context and help alleviate a lot of the equivocation that appears to be in the advice.

1
  • 1
    Please consider expanding upon your answer. As it stands, you're bringing together two orthogonal concepts and it's not clear how the result addresses the OP's concerns.
    – user53019
    Commented Aug 14, 2013 at 3:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.