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.

dsimcha
  • 17,284

12 Answers12

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.

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.

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.

P.Brian.Mackey
  • 11,121
  • 8
  • 53
  • 88
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.

Mason Wheeler
  • 83,213
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.

Michael K
  • 15,659
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.

Martin Blore
  • 4,685
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.

jprete
  • 1,519
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

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.

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.

kevin cline
  • 33,798
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.

Newtopian
  • 7,221