130

Say I have a entity that has "type" attribute. There could be 20+ possible types.

Now I'm asked to implement something that would allow changing the type from A->B, which is the only use case.

So should I implement something that allows arbitrary changes of type as long as they are valid types? Or should I ONLY allow it to change from A->B per the requirement and reject any other type change such as B->A or A->C?

I can see the pros and cons from both sides, where a generic solution would mean less work in case a similar requirement comes up in the future, but it would also mean more chance of going wrong (though we 100% control the caller at this point).
A specific solution is less error prone, but requires more work in the future if a similar requirement arises.

I keep hearing that a good developer should try to anticipate the change and design the system so that it's easy to extend in the future, which sounds like a generic solution is the way to go?

Edit:

Adding more details to my not-so-specific example: The "generic" solution in this case requires less work than the "specific" solution, as the specific solution requires validation on the old type as well as new type, while the generic solution only need to validate the new type.

21
  • 99
    That's a interesting question. I've had a discussion about something like that with my gramps (a very, very old timer programmer) and his response was something in the lines of "the most generic thing that solves your specific problem", which boils down to a fancy "It depends". Blanket statements in Software Development rarely work - things should be taken always in a case-by-case basis.
    – T. Sar
    Nov 30, 2017 at 18:38
  • 3
    @T.Sar add this as an answer and I'll upvote :-)
    – Christophe
    Nov 30, 2017 at 18:43
  • 4
    What is a “generic solution” in your view? Also, please clarify what you mean by the “only use case”. Do you mean that only a transition from A->B is allowed or only that transition is specified, and it would not be an error condition to transition from any other state to any other state. As the developer you need to ask the author of the use case to clarify. If no other transition is allowed and your code allows it regardless of who controls the caller, your code has failed to meet the requirements. Nov 30, 2017 at 19:34
  • 5
    The principle that if X is a good idea, then only X all the time must be optimal, seems to have a particular appeal to developers (or at least a vocal group among them), but consider this: programming rules-of-thumb are like proverbs, in that you can often find a pair with opposite implications. This is a sign that you should use your judgement (as advocated by the answers here), and beware of the dogma.
    – sdenham
    Dec 1, 2017 at 2:42
  • 2
    Premature Generalization can cause as much heartburn as Premature Optimization - you wind up writing a lot of code that may never get used. Solved for the specific problem first, then generalize as the need arises.
    – John Bode
    Dec 1, 2017 at 15:34

14 Answers 14

302

My rule of thumb:

  1. the first time you encounter the problem, only solve the specific problem (this is the YAGNI principle)
  2. the second time you run into the same problem, consider generalizing the first case, if it's not a lot of work
  3. once you have three specific cases where you'd be able to use the generalized version, then you should start really planning the generalized version -- by now, you should understand the problem well enough to actually be able to generalize it.

Of course, this is a guideline and not a hard-and-fast rule: the real answer is to use your best judgement, on a case by case basis.

7
  • 1
    Can you explain what YAGNI is?
    – Bernhard
    Dec 1, 2017 at 5:09
  • 8
    @Bernhard You Aren't Gonna Need It. A very useful software design principle. Dec 1, 2017 at 8:15
  • 4
    "At thing1, thing2, think about maybe using an array. At thing1, thing2, thing3, almost certainly use thing[] array instead." is a similar rule of thumb for deciding between multiple variables or a single array.
    – Joker_vD
    Dec 1, 2017 at 23:19
  • 16
    Another way to state rule #1: "You can't generalize one thing." Dec 4, 2017 at 20:52
  • 3
    @SantiBailors: As Doc Brown says in his answer, we often vastly overestimate the amount of wasted effort we might expend by building the first one to throw away. In The Mythical Man-Month, Fred Brooks says: "Plan to throw one away -- you will, anyhow." That said: if you immediately encounter more than one use for something -- for example, by being delivered a set of requirements where you will clearly need to solve the same problem more than once -- then you already have more than one case to generalize from, and that's totally fine and not in conflict with my answer. Dec 5, 2017 at 13:12
98

A specific solution [...] requires more work in the future if similar requirement is needed

I have heard this argument several dozen times, and - to my experience - it regularly turns out to be a fallacy. If you generalize now or later, when the second similar requirement arises, the work will almost be the same in total. So there is absolutely no point to invest additional effort in generalizing, when you don't know this effort will ever pay off.

(Obviously this does not apply when a more general solution is less complicated and requires less effort than a specific one, but to my experience, these are rare cases. Such a kind of scenario was edited afterwards into the question, and it is not the one my answer is about).

When the "second similar case" shows up, then it is time to start thinking about generalizing. It will be much easier to generalize correctly then, because this second requirement gives you a scenario where you can validate if you made the right things generic. When trying to generalize just for one case, you are shooting in the dark. Chances are high you overgeneralize certain things which don't need to be generalized, and miss other parts that should. And when then a second case arises and you realize you generalized the wrong things, then you have much more work to do to fix this.

So I recommend to delay any temptation for doing things "just in case". This approach will only lead to more work and maintenance efforts when you missed the occasion for generalizing three, four or more times and then have a pile of similar-looking (so duplicate) code to maintain.

10
  • 2
    To me this answer doesn't make sense in OP's context. He's saying he'll spend less time generalizing now, and less time in the future, unless in the future there is a need for the implementation to be specific. You're arguing against "investing additional effort in generalizing", whereas OP will invest additional effort only if they don't generalize. (think) .... weird :$
    – msb
    Nov 30, 2017 at 22:14
  • 2
    @msb: that context was added after I wrote my answer and goes contrary to what the OP was saying before, especially in the part I cited. See my edit.
    – Doc Brown
    Nov 30, 2017 at 22:25
  • 2
    Also, the generalized solution will be more complicated, so by the time you have to adapt it for the second case, it will take much longer to understand it again.
    – AndreKR
    Dec 1, 2017 at 9:36
  • 4
    Doc, an aside, but never would have guessed you were a non-native speaker. Your answers are always well written and well argued.
    – user949300
    Dec 1, 2017 at 23:27
  • 2
    I'll imagine your accent when I'm reading through your future answers. :-)
    – user949300
    Dec 2, 2017 at 2:36
66

TL;DR: it depends on what you're trying to solve.

I've had a similar conversation with my Gramps about this, while we were talking about how Func and Action in C# are awesome. My Gramps is a very old timer programmer, that's been around source codes since software was run on computers that took a whole room.

He changed techs several times in his life. He wrote code in C, COBOL, Pascal, BASIC, Fortran, Smalltalk, Java and eventually started C# as a hobby. I learned how to program with him, sitting on his lap while I was but a devling, carving away my first lines of code on the blue editor of IBM's SideKick. By the time I was 20, I already had spent more time coding than playing outside.

Those are a bit of my memories, so excuse me if I'm not exactly practical while retelling them. I'm somewhat fond of those moments.

That's what he said to me:


"Should we go for the generalization of a problem, or solve it in the specific scope, do you ask? Well, that's a... question."

Gramps took a pause to think about it for a brief moment, while fixing the position of his glasses on his face. He was playing a match-3 game on his computer, while listening to a Deep Purple's LP on his old sound system.

"Well, that would depend on what problem you're trying to solve", he told me. "It is tempting to believe that a single, holy solution for all design choices exist, but there isn't one. Software Architecture is like cheese, you see."

"...Cheese, Gramps?"

"Doesn't matter what you think about your favorite, there will always be someone that thinks it is smelly".

I blinked in confusion for a moment, but before I could say anything Gramps went on.

"When you're building a car, how do you pick the material for a part?"

"I... I guess it depends on the costs involved and what the part should do, I suppose."

"It depends on the problem that part is trying to solve. You won't make a tire made of steel, or a windshield made of leather. You pick the material that best solves the problem you have at hand. Now, what is a generic solution? Or a specific one? To what problem, to what use case? Should you go with a full functional approach, to give maximum flexibility to a code that will be used only once? Should you write a very specialized, fragile code to a part of your system that will see lots and lots of uses, and possibly lots of changes? Design choices like those are like the materials you pick for a part in a car or the shape of the Lego brick you pick to build a little house. What Lego brick is the best one?"

The elderly programmer reached for a little Lego train model that he has on his table before continuing.

"You can only answer that if you know for what you need that brick. How the hell you'll know if the specific solution is better than the generic one, or vice versa, if you don't even know what problem you're trying to solve? You can't see past a choice that you don't understand."

"..Did you just quote The Matrix?"

"What?"

"Nothing, go on."

"Well, suppose you're trying to build something to the National Invoice System. You know how that hellish API and its thirty thousand lines XML file look like from the inside. How would a 'generic' solution for creating that file would even look like? The file is full of optional parameters, full of cases that only very specific branches of business should use. For most cases, you can safely ignore them. You don't need to create a generic invoice system if the only thing you'll ever sell is shoes. Just create a system for selling shoes and make it be the best darned shoe-selling invoice system out there. Now, if you had to create a invoice system for any type of client, on a more broad application - to be resold as a independent, generic sales system, for example - now it is interesting to implement those options that are only used for Gas, food or alcohol. Now those are possible use cases. Before they were just some hypothetical Don't use cases, and you don't want to implement Don't use cases. Don't use is the little brother of Don't need."

Gramps put the lego train back on its place and turned back to his match-3 game.

"So, to be able to pick a generic or a specific solution for a given problem you first need to understand what the hell that problem is. Otherwise you're just guessing, and guessing is the job of managers, not programmers. As almost everything in IT, it depends."


So, there you have it. "It depends". That's probably the most powerful two-word expression when thinking about software design.

14
  • 15
    I'm sure there's something useful in that story but it was too painful to read, sorry Dec 1, 2017 at 10:47
  • 28
    Your first post was a short fun anecdote - and of course it's a matter of opinion - but unless I really like someones writing style I find long stories like this really self-indulgent and a bit inappropriate outside of a personal blog Dec 1, 2017 at 11:05
  • 16
    @T.Sar don't listen to the haters, your post is a gem of useful advice from a guru. +1
    – LLlAMnYP
    Dec 1, 2017 at 12:27
  • 7
    The "software architecture is like cheese" part was just pure awesome. Indeed, this answer is a gem! Dec 1, 2017 at 13:10
  • 3
    Maybe this answer is like cheese too? ;) But please, "SmallTalk" should be "Smalltalk", and yes, I'm being that guy, sorry.
    – fede s.
    Dec 1, 2017 at 14:30
14

Primarily, you should try to anticipate whether it is likely that such a change will occur - not just a remote possibility somewhere down the line.

If not, it's usually better to go for the simple solution now and extend it later. It's quite possible that you'll have a much clearer picture of what is needed then.

13

If you're working in domain that's new to you, than the Rule of three that Daniel Pryden mentioned definitely should be applied. After all, how are you supposed to build useful abstractions if you're a novice in that area? People are often self-assured in their ability to catch abstractions, though it's seldom the case. To my experience, premature abstraction is no less of an evil than code duplication. Wrong abstractions are really painful to comprehend. Sometimes even more painful to refactor.

There is a book addressing my point about an unknown area developer is working in. Is consists of specific domains with extracted useful abstractions.

4
  • The question is clear about a concrete problem. Nov 30, 2017 at 19:28
  • 4
    The answer is generic enough to address this concrete problem. And the question is not specific enough to be given a concrete receipt: as you can see, no domain details are mentioned in a question. Even class names are not specified (A and B don't count). Nov 30, 2017 at 19:35
  • 7
    "Should a stackoverflow answer be as generic as possible or as specific as possible?" Dec 1, 2017 at 1:59
  • @LyndonWhite should a stackoverflow question be as generic as possible (too broad!) or as specific as possible (whatever that failure is called!)? Ha.
    – user251748
    Dec 1, 2017 at 16:51
4

Given the nature of the body of your question, assuming I understood it correctly, I actually see this as a design question of central system features more than a question about generic vs. specific solutions.

And when it comes to central system features and capabilities, the most reliable ones are the ones that aren't there. It pays to err on the side of minimalism, especially considering that it's generally easier to add functionality centrally, long desired, than it is to remove problematic functionality, long undesired, with numerous dependencies because it has made working with the system so much more difficult than it needs to be while raising endless design questions with each new feature.

In fact, lacking a strong anticipation of whether this will be needed frequently in the future, I would seek to avoid looking at this as a type replacement from A to B if possible, and instead just seek it as a way to transform A's state . For example, set some fields in A to make it morph and appear like B to the user without actually changing into a different 'type' of thing -- you could potentially make A store B privately using composition and call functions in B when A's state is set to indicate that it should mimic B to simplify the implementation if possible. That should be a very simple and minimally invasive solution.

So anyway, echoing many others, I'd suggest to err on the side of avoiding generic solutions in this case, but more so because I assume this is in consideration of adding a very bold capability to the central system, and there I'd suggest to err on the side of leaving it out, especially for now.

1
  • "you miss all the bugs you don't code." (from basketball poster: you miss all the shots you don't take)
    – user251748
    Dec 1, 2017 at 16:53
3

It's difficult to give a generic answer to this specific problem ;-)

The more generic it is, the more time you'll gain for future changes. For example for this reason many game programmes use the entity component pattern instead of building a very elaborate but rigid type system of the characters and objects in the game.

On the other hand, making something generic requires an up-front time and effort investment in design which is much higher than for something very specific. This bears the risk of over-engineering, and even of getting lost in potential future requirements.

It's always worth to see if there's a natural generalisation that will get you a breakthrough. However, in the end, it's be a question of balance, between the effort that you can spend now and the effort that you might need in the future.

3
  1. Hybrid. This doesn't have to be an either/or question. You could design the API for general type conversions while implementing only the specific conversion you need right now. (Just make sure that if someone calls your general API with an unsupported conversion, it fails with a "not-supported" error status.)

  2. Testing. For the A->B conversion, I'm going to have to write one (or a small number) of tests. For a generic x->y conversion, I might have to write a whole matrix of tests. That's substantially more work, even if all the conversions share a single generic implementation.

    If, on the other hand, there turns out to be a generic way to test all possible conversions, then there isn't much more work and I might be inclined to go to a generic solution sooner.

  3. Coupling. A converter from A to B might necessarily need to know implementation details about A's and B's (tight coupling). If A's and B's are still evolving, this means I might have to keep revisiting the converter (and its tests), which sucks, but at least it's limited to A's and B's.

    If I'd gone with a generic solution that needs access to the details of all types, then even as C's and D's evolve, I may need to keep tweaking the generic converter (and a bunch of tests), which can slow me down even though nobody yet needs to convert to a C or a D.

    If both the generic and the specific conversions can be implemented in a way that's only loosely coupled to the details of the types, then I wouldn't worry about this. If one of those can be done in a loosely coupled way but the other requires tight coupling, then that's a strong argument for the loosely-coupled approach right out of the gate.

2
  • 2
    Testing is a good point. It's a big advantage if you design the generic solutions based on concepts from category theory, because then you often get free theorems which prove that the only possible implementation for a signature (that the compiler's type checker accepts) is the correct one, or at least that if the algorithm works for any particular type it must also work for all other types. Nov 30, 2017 at 23:13
  • I'd guess that there is a domain-specific reason why only one type conversion has been asked for, meaning that most type conversions would be invalid actions in the problem domain, and for that reason, should not be supported by the app until they are officially allowed. This answers comes closest to that argumentation. Dec 1, 2017 at 10:25
3

Should the solution be as generic as possible or as specific as possible?

That's not an answerable question.

The best you can reasonably get is a few heuristics to decide how general or specific to make a given solution. Working through something like the process below, usually the first-order approximation is correct (or good enough). When it isn't, the reason is likely to be too domain-specific to be usefully covered in detail here.

  1. First-order approximation: the usual YAGNI rule of three as described by Daniel Pryden, Doc Brown, et al.

    This is a generally-useful heuristic because it's probably the best you can do that doesn't depend on domain and other variables.

    So, the initial presumption is: we do the most specific thing.

  2. Second-order approximation: based on your expert knowledge of the solution domain, you say

    The "generic" solution in this case requires less work than the "specific" solution

    so we might re-interpret YAGNI as recommending we avoid unnecessary work, rather than avoiding unnecessary generality. So, we might modify our initial presumption and instead do the easiest thing.

    However if your solution domain knowledge indicate that the easiest solution is likely to open up a lot of bugs, or be hard to adequately test, or cause any other problem, then being easier to code isn't necessarily a good enough reason to change our original choice.

  3. Third-order approximation: does your problem domain knowledge suggest the easiest solution is actually correct, or are you allowing lots of transitions you know to be meaningless or wrong?

    If the easy-but-generic solution looks problematic, or you're not confident in your ability to judge these risks, doing the extra work and staying with your initial guess is probably better.

  4. Fourth-order approximation: does your knowledge of customer behaviour, or how this feature relates to others, or project management priorities, or ... any other not-strictly technical considerations modify your current working decision?

2

This is not an easy question to answer with a simple answer. Many answers have given heuristics built around a rule of 3, or something similar. Stepping beyond such rules of thumb is difficult.

To really really answer your question, you have to consider that your job is most likely not to implement something that changes A->B. If you're a contractor, maybe that's the requirement, but if you're an employee, you were hired on to do many many smaller tasks for the company. Changing A->B is only one of those tasks. Your company is going to care about how well the future changes can be made as well, even if that is not stated in the request. To find "TheBestImplementation(tm)," you have to look at the larger picture of what you're really being asked to do, and then use that to interpret the small request you've been given to change A->B.

If you are a low level entry programmer fresh out of college, it's often advisable to do exactly what you were told to do. If you were hired on as a software architect with 15 years experience, it's typically advisable to think about big picture things. Every actual job is going to fall somewhere between "do exactly what the narrow task is" and "think about the big picture." You'll feel out where your job fits in that spectrum if you talk to people enough and do enough work for them.

I can give a few concrete examples where your question has a definitive answer based on context. Consider the case where you are writing safety-critical software. This means you have a test team stood up to ensure the product performs as promised. Some of these test teams are required to test every single possible path through the code. If you talk with them, you may find that if you generalize the behavior, you'll add $30,000 to their testing costs because they'll have to test all of those extra paths. In that case, do not add generalized functionality, even if you have to duplicate work 7 or 8 times because of it. Save the company money and do exactly what the request stated.

On the other side, consider that you're making an API to permit customers to access data in a database program that your company makes. A customer requests to permit changes A->B. APIs typically have an aspect of golden handcuffs to them: once you add functionality to an API, you're typically not supposed to remove that functionality (until the next major version number). Many of your customers may not be willing to pay for the cost of upgrading to the next major version number, so you may be stuck with whatever solution you choose for a long time. In this case, I highly recommend creating the generic solution from the start. You really don't want to develop a bad API full of one-off behaviors.

1

Hmm... not much context to go on for an answer... echoing earlier answers, "It depends".

In a sense, you have to fall back on your experience. If not yours, then someone more senior in the domain. You could quibble about what the acceptance criteria state. If it's something along the lines of 'the user should be able to change the type from "A" to "B"' versus 'the user should be able to change the type from it's current value to any allowed alternate value'.

Frequently acceptance criteria are subject to interpretation, but good QA staff can write criteria appropriate to the task at hand, minimizing the interpretation needed.

Are there domain restrictions that don't allow changing from "A" to "C" or any other option, but only "A" to "B"? Or is this just a narrowly spec'ed requirement that isn't "forward thinking"?

If the general case were more difficult I'd go ask before starting the work, but in your case if I could 'predict' that other 'type' change requests would be coming in the future, I'd be tempted to: a) write something reusable for the general case, and b) wrap it in a conditional that only allows A -> B for now.

Easy enough to verify for the current case in automated testing, and easy enough to open up to other options later if/when different use cases arise.

1

For me, a guideline I established a while ago is: "For hypothetical requirements only write hypothetical code." That is - if you anticipate additional requirements, you should think a little about how to implement them, and structure your current code so that it doesn't block that way.

But don't write actual code for these now - just think a little about what you'd do. Otherwise you'll usually make things unnecessarily complex, and will probably be annoyed later when actual requirements come that differ from what you anticipated.

Four your example: if you have all the usages of the convert method under your control, you can just call it convertAToB for now and plan to use a "rename method" refactoring in the IDE to rename it if you need more general functionality later. However, if the conversion method is part of a public API, this could be quite different: being that specific would block the generalization later, since it's hard to rename things in that case.

0

I keep hearing that a good developer should try to anticipate the change and design the system so that it's easy to extend in the future,

In principle, yes. But this does not necessarily lead to generic solutions.

There are two types of topics in software development, as far as I'm concerned, where you should anticipate future change:

  • Libraries intended to be used by 3rd parties, and
  • overall software architecture.

The first case is solved by watching your cohesion/coupling, dependency injection or whatever. The second case is on a more abstract level, for example picking a service oriented architecture instead of a big monolothic blob of code for a large application.

In your case, you are asking for a specific solution for a specific problem, which has no foreseeable impact whatsoever on the future. In this case, YAGNI and DRY are good mottos to shoot for:

  • YAGNI (you ain't gonna need it) tells you to implement the absolutely minimum, basic stuff that you need and use right now. It means to implement the minimum that makes your current test suite turn from red to green, if you should use TDD/BDD/FDD style development. Not a single line more.
  • DRY (don't repeat yourself) means if you come about a similar problem again, then you take a good hard look at whether you need a generic solution.

Combined with other modern practices (like good test coverage to be able to refactor safely) this means that you end up with quickly written, lean, mean code that grows as needed.

which sounds like a generic solution is the way to go?

No, it sounds like you should have programming environments, languages, and tooling that make it easy and fun to refactor when you need it. Generic solutions don't provide that; they decouple the application from the actual domain.

Take a look at modern ORMs or MVC frameworks, for example Ruby on Rails; on the application level, all focus is on doing non-generic work. The rails libraries themselves are obviously nearly 100% generic, but the domain code (which your question is about) should be doing minimal shenanigans in that aspect.

0

A different way to think about the problem is to consider what makes sense.

For example, there was application I was developing that had there sections which did almost the same thing but had inconsistent permission rules. As there was no reason to keep them different, when I refactored that section, I made them all do permissions the same way. The made the overall code smaller, simpler and the interface was more consistent.

When management decided to allow other people access to a feature, we were able to do it by just changing a flag.

Obviousily it makes sense to make the specific type conversion. Does it also make sense to make addintional type conversions?

Keep in mind, if the general solution is faster to implement, then the apecific case is easy too, just check that it is the only type conversion you are allowing.

If the application is in a highly regulated area (a medical or financial application) try to involve more people in you design.

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