10

So probably like many, I often find myself running into headaches with design problems in which, for example, there is some design pattern/approach that seems to intuitively fit the problem and has desired benefits. Very often there is some caveat which makes it difficult to implement the pattern/approach without some kind of work around which then negates the benefit of the pattern/approach. I can very easily end up iterating through many patterns/approaches because predictably almost all of them have some very significant caveats in real-world situations in which there simply isn't an easy solution.


Example:

I'll give you a hypothetical example based loosely on a real one I recently encountered. Let's say I want to use composition over inheritance because inheritance hierarchies have hindered the scaleability of the code in the past. I might refactor the code but then find that there are some contexts where the superclass/baseclass simply needs to call functionality on the subclass, despite attempts to avoid it.

The next best approach seems to be implementing a half delegate/observer pattern and half composition pattern so that the superclass can delegate behaviour or so that the subclass can observe superclass events. Then the class is less scaleable and maintainable because its unclear how it should be extended, also it is tricky to extend existing listeners/delegates. Also information isn't hidden well because one starts needing to know the implementation to see how to extend the superclass (unless you use comments very extensively).

So after this I might opt to simply just use observers or delegates completely to get away from the drawbacks that comes with mixing up the approaches to heavily. However this comes with its own problems. For example, I might find that I end up needing observers or delegates for an increasing amount of behaviours until I end up needing observers/delegates for practically every behaviour. One option could be to just have one big listener/delegate for all of the behaviour but then the implementing class ends up with lots of empty methods etc.

Then I might try another approach but there are just as many issues with that. Then the next one, and the one after etc.


This iterative process gets very difficult when each approach seems to have as many problems as any of the others and leads to a sort of design decision paralysis. It is also difficult to accept that the code will end up equally problematic regardless of which design pattern or approach is used. If I end up in this situation does it mean that the problem itself needs to be rethought? What do others do when they encounter this situation?

Edit: There seems to be a number of interpretations of the question that I want to clear up:

  • I have taken OOP out of the question completely because it turns out that it is not actually specific to OOP, plus it is too easy to misinterpret some of the comments I made in passing about OOP.
  • Some have claimed I should take an iterative approach and try different patterns, or that I should discard a pattern when it ceases to work. This is the process I intended to refer to in the first place. I thought this was clear from the example but I could have made it clearer, so I've edited the question to do so.
6
  • 3
    Don't "subscribe to the OO philosophy". It's a tool, not a major life decision. Use it when it helps and don't use it when it doesn't. Commented Feb 28, 2018 at 5:11
  • If you're stuck thinking in terms of certain patterns, maybe try writing some moderately complex project in C, it'll be interesting to experience how much you can do without those patterns. Commented Feb 28, 2018 at 5:13
  • 2
    Oh C has patterns. They are just very different patterns. :) Commented Feb 28, 2018 at 6:37
  • I have cleared up most of these misinterpretations in the edit
    – Jonathan
    Commented Feb 28, 2018 at 12:11
  • Having these problems appear once in a great while is normal. Having them occur often should not be the case. Most likely, you are using the wrong programming paradigm for the problem or your designs are a mixture of paradigms in the wrong places.
    – Dunk
    Commented Mar 1, 2018 at 22:38

6 Answers 6

8

When I get to a tough decision like that, I usually ask myself three question:

  1. What are the pros and cons of all the available solutions?

  2. Is there a solution, I have not yet considered?

  3. Most importantly:
    What exactly are my requirements? Not the requirements on paper, the real fundamental ones?
    Can I somehow reformulate the problem / tweak its requirements, so that it allows for a simple, straight-forward solution?
    Can I represent my data in some different way, so that it allows for such a simple, straight-forward solution?

    This is the questions about the fundamentals of the perceived problem. It may turn out, that you were actually trying to solve the wrong problem. Your problem may have specific requirements that allow for a much simpler solution than the general case would. Question your formulation of your problem!

I find it very important to think about all three questions hard before continuing. Take a walk, pace your office, do whatever it takes to really mull over the answers to these questions. However, answering these questions should not take improper amounts of time. Proper times may range from 15 minutes to something like a week, they depend on the badness of the solutions you have already found, and its impact on the whole.

The value of this approach is, that you will sometimes find surprisingly good solutions. Elegant solutions. Solutions well worth the time you invested in answering these three questions. And you won't find those solution, if you just enter the next iteration immediately.

Of course, sometimes no good solution seems to exist. In that case, you are stuck with your answers to question one, and good is simply the least bad. In this case, the value of taking the time to answer these questions is that you possibly avoid iterations that are bound to fail. Just make sure you get back to coding within a proper amount of time.

3
  • I might well mark this as the answer, this makes the most sense to me and there appears to be a consensus about simply reassessing the problem itself.
    – Jonathan
    Commented Feb 28, 2018 at 16:43
  • Also I like how you broke it down into steps so there is some sort of loose procedure for assessing the situation.
    – Jonathan
    Commented Feb 28, 2018 at 16:44
  • OP, I see you stuck in the mechanics of code solution, so yeah you "might as well mark this answer." The best code we've written was after a very careful analysis of the requirements, derived requirements, class diagrams, class interactions, etc. Thank goodness we resisted all the heckling from the cheap seats (read management & co-workers): "You're spending too much time in design", "hurry up and get coding!", "that's too many classes!", etc. Once we got to it, coding was a joy - more than fun. Objectively that was the best code in the entire project.
    – radarbob
    Commented Mar 2, 2018 at 17:18
13

First things first - patterns are useful abstractions not the end all be all of design, let alone OO design.

Secondly - modern OO is smart enough to know that not everything is an object. Sometimes using plain old functions, or even some imperative style scripts will yield a better solution for certain problems.

Now to the meat of things:

This gets very difficult when each approach seems to have as many problems as any of the others.

Why? When you have a bunch of similar options, your decision should be easier! You won't lose much by picking the "wrong" option. And really, code isn't fixed. Try something, see if it is good. Iterate.

It is also difficult to accept that the code will end up very problematic regardless of which design pattern or approach is used.

Tough nuts. Hard problems - actual mathmatically hard problems are just hard. Provably hard. There is literally no good solution for them. And it turns out, the easy problems aren't really valuable.

But be wary. Too often I've seen people frustrated by no good options because they are stuck looking at the problem a certain way, or that they've cut their responsibilities in a way that is unnatural to the problem at hand. "No good option" can be a smell that there's something fundamentally wrong with your approach.

This results in a loss of motivation because "clean" code ceases to be an option sometimes.

Perfect is the enemy of good. Get something working, then refactor it.

Are there any approaches to design that can help to minimise this problem? Is there an accepted practice for what one should do in a situation like this?

As I've mentioned, iterative development generally minimizes this problem. Once you get something working, you're more familiar with the problem putting you in a better position to solve it. You have actual code to look at and evaluate, not some abstract design that doesn't seem right.

3
  • Just thought I should say that I have cleared up some misinterpretations in the edit. I generally do the "get it working" and refactor approach. However, often this ends up leading to a lot of technical debt in my experience. For example, if I just get a thing working, but then myself or others build on top of it, some of those things may have unavoidable dependencies which then also need a ton of refactoring. Of course you can't always know this in advance. But if you already know the approach is flawed, should one get it working and then iteratively refactor before building on the code?
    – Jonathan
    Commented Feb 28, 2018 at 12:21
  • @Jonathan - all designs are a series of trade offs. Yes, you should iterate immediately if the design is flawed and you have a clear way of improving it. If there’s no clear improvement, stop fucking about.
    – Telastyn
    Commented Feb 28, 2018 at 12:36
  • "they've cut their responsibilities in a way that is unnatural to the problem at hand. No good option can be a smell that there's something fundamentally wrong with your approach." I'd bet on this one. Some developers never run into the OP described issues and others seem to do it quite frequently. Often the solution isn't easy to see when posed by the original developer because they have already biased the design in the way they frame the problem. Sometimes the only way to find the quite obvious solution is start all the way back at the requirements for the design.
    – Dunk
    Commented Mar 1, 2018 at 22:25
8

The situation you're describing looks like a bottom-up approach. You take a leaf, try to fix it and find out it's connected to a branch which itself is also connected to another branch etc.

It's like trying to build a car starting with a tire.

What you need is to take a step back and look at the bigger picture. How does that leaf sit in the overall design? Is that still current and correct?

How would the module look if you would design and implement it from scratch? How far away from that "ideal" is your current implementation.

That way you have a bigger picture of what to work towards. (Or if you decide it's too much work, what the issues are).

4

Your example describes a situation which arises typically with larger pieces of legacy code, and when you are trying to make a "too big" refactoring.

The best advice I can give you for this situation is:

  • don't try to achieve your main goal in one "big bang",

  • learn how to improve your code in smaller steps!

Of course, that is easier written than done, so how to accomplish this in reality? Well, you need practice and experience, it depends very much on the case, and there is no hard-and-fast rule saying "do this-or-that" which fits for every case. But let me use your hypothetical example. "composition-over-inheritance" is not an all-or-nothing design goal, it is a perfect candidate to be achieved in several small steps.

Lets say you noticed "composition-over-inheritance" is the right tool for the case. There must be some indications for this being a sensible goal, otherwise you would not have chosen this. So lets assume there is a lot of functionality in the superclass which is just "called" from the subclasses, so this functionality is a candidate for not staying in that superclass.

If you notice you cannot immediately remove the superclass from the subclasses, you may start first with refactoring the superclass into smaller components encapsulating the functionality mentioned above. Start with the lowest-hanging fruits, extract some simpler components first, which will already make your superclass less complex. The smaller the superclass gets, the easier additional refactorings will become. Use these components from the subclasses as well as from the superclass.

If you are lucky, the remaining code in the superclass will become so simple throughout this process that you can remove the superclass from the subclasses without any further issues. Or, you will notice that keeping the superclass is not a problem any more, since you already extracted enough of the code into components you want to reuse without inheritance.

If you are unsure where to start, because you don't know if a refactoring will turn out to be simple, sometimes the best approach is to do some scratch refactoring.

Of course, your real situation might be more complicated. So learn, gather experience, and be patient, getting this right takes years. There are two books I can recommend here, maybe you find them helpful:

  • Refactoring by Fowler: describes a full catalogue of very small refactorings.

  • Working effectively with legacy code by Feathers: gives excellent advice how to deal with large chunks of badly designed code and make it more testable in smaller steps

1
  • 1
    This is also very helpful. Small step-wise or scratch refactoring is a nice idea because it then becomes something that can be progressively completed along with other work without hindering it too much. I wish I could approve multiple answers!
    – Jonathan
    Commented Feb 28, 2018 at 16:46
1

Sometimes the best two design principles are KISS* and YAGNI**. Don't feel the need to cram every known design pattern into a program that just has to print "Hello world!".

 * Keep It Simple, Stupid
 ** You Ain't Gonna Need It

Edit after question update (and mirroring what Pieter B says to some extent):

Sometimes you take an architectural decision early on which leads you to a particular design that leads to all sorts of ugliness when you try to implement it. Unfortunately, at that point, the "proper" solution is to step back and work out how you got to that position. If you can't see the answer yet, keep stepping back until you do.

But if the work to do that would be out of proportion, it takes a pragmatic decision to just come up with the least ugly workaround to the problem.

2
  • I have cleared up some of this in the edit, but in general I'm referring to exactly those problems where there isn't a simple solution that is available.
    – Jonathan
    Commented Feb 28, 2018 at 12:05
  • Ah nice yes there seems to be a consensus emerging about stepping back and reassessing the problem. This is a good idea.
    – Jonathan
    Commented Feb 28, 2018 at 16:42
1

When I'm in this situation, the first thing I do is stop. I switch to another problem and work on that for a while. Maybe an hour, maybe a day, maybe more. That's not always an option, but my subconscious will work on things while my conscious brain does something more productive. Eventually, I come back to it with fresh eyes and try again.

Another thing I do is ask someone smarter than me. This can take the form of asking on Stack Exchange, reading an article on the web on the topic, or asking a colleague who has more experience in this area. Often the method that I think is the right approach turns out to be completely wrong for what I'm attempting to do. I've mischaracterized some aspect of the problem and it doesn't actually fit the pattern I think it does. When that happens, having someone else say, "You know, this looks more like..." can be a big help.

Related to the above is design or debugging by confession. You go to a colleague and say, "I'm going to tell you the problem I'm having, and then I'm going to explain to you the solutions that I have. You point out problems in each approach and suggest other approaches." Often before the other person even speaks, as I'm explaining, I start to realize that one path that seemed equal to others is actually much better or worse than I originally thought. The resulting conversation may either reinforce that perception, or point out new things I didn't think of.

So TL;DR: Take a break, don't force it, ask for help.

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.