Reusable Software? Just Don't Write Generic Code
josdejong.com
josdejong.com
The original formulation may have been a real problem at the time it was made, but in our time running into ugly code due to misguided optimization seems pretty rare. Code that attempts - and almost always fails - to be generic, and instead ends up overcomplicated and awkward to use, on the other hand...
It's not just 3rd party code either, in my own teams it's not rare I find myself arguing for KISS and YAGNI. Sometimes I prevail, sometimes the premature generalizers do.
When I'm tempted to make something generic, it's most of times because I've built a too-focused monolithic solution to my problem. Genericity should be built bottom-up from the start by progressing up in small layers with controlled (and meaningful) interface surface reduction.
Genericity is attained by composition.
Of course, the vast majority of these in-house application frameworks ended up being used for precisely one application or are overtaken by frameworks that succeed in the wider market.
1. Particularly the enterprise Java world in the bad old days.
...and then someone comes up with the idea of building a framework for building frameworks to do X: http://discuss.joelonsoftware.com/default.asp?joel.3.219431....
NB I did comment on that thread on Joel On Software.... :-)
I think the difference now is that some high quality fairly generic framework have emerged (there was nothing like Rails or Django back in 2000 as far as I am aware). Why build that part when someone else has done the hard work for you? Its well tested, documented, and there is a large knowledge base out there. (I dislike it when I hear about in house frameworks when I am in a job interview, as those will generally not be as well documented, and only a few "in house rockstars" will really understand them).
Time goes by pretty quickly, things change, and so much code is being made obsolete every year. Remember, it's easier to write code than to read it. Thus, write code that is readable and that does the job, not code that does something abstract and "lead the way for similar cases".
To me inheritance and object oriented programming should let people make libraries, but not make end user applications. Application are imperative and follow scenarios and use case, they're not OOP. Most programmers should just use OOP and inheritance when they use libraries, not in other cases. You don't need to make a library very often, thus, don't try to use inheritance or to write a library.
Re-use is only one aspect of generic code. The other, no less important aspect, is a reduction of possible implementations, which results in less bug-prone, easier to read code.
Whilst inheritance is a problem, it seems to me we've been preferring composition over inheritance for a long time now. Single inheritance is mentioned as if it is a bad thing, but I'm not sure anyone that has seen (or worse, tried to implement) multi-inheritance in C++ would agree. Finally, I'm not convinced having a interface for a single concrete class is a bad thing. Sure, it adds some boilerplate (Java's main problem) but it forces the developer to think about the public API and makes testing easier.
Generic programming actually reduces assumptions, is less bug prone, and is (probably) not what the author was thinking of.
How does Haskell deal with refs?
Can you provide an example of semi unification where Haskell would be not so great?
I have been recognizing this cycle more and more, I think it's almost a fundamental law of software engineering -- the war between flexibility and over-engineering/over-abstraction.
Reusable code isn't generic, reusable code is focused and built out of (re)composable parts. Reusability IS composability.
I think you've got the right idea, but it still doesn't always make it easy.
The places I have found the most success in my design, even looking back, are when I constructed a library for a domain I was already well familiar with (often having experienced how another library handled it), and where I had the time to do it slowly and carefully, deliberately considering each design choice and the simplest possible way to achieve each desired path while still allowing for flexible reconfiguration. When working with other developers, this often, as well, involves some initial disagreement about the best solution that accomplishes without adding unneeded complexity. And sometimes even I start getting impatient with the slowness of the deliberate process and consensus building -- it's easier to do by myself (which takes even more time).
These are rare circumstances that allow for such slow coding though, in the pressure to get things out quick and get them used and then quickly iterate. We have a (hard-won, reaction to other harmful development ideologies) overall ethos in our industry(ies) of getting things out quickly without needing to get them perfect or fully understand where it will lead you, and then iterate based on feedback. That works pretty well for UI, although it can still contain the same pitfalls. It is even harder for infrastructural architecture though, it's probably still possible as long as you are careful and deliberate with your iterations (although knowing when to break backwards compat is still a trick), but, anyway, I don't think it's easy.
Converging towards the simplest representation of a problem's solution takes a strong ability to let loose on your mental pictures in order to escape local maxima, and time. What we do is essentially rewiring our brains around a problem. In this respect we are bound by biological processes. There is no shortcut. Every path that looks like a shortcut will lead to conceptual pollution that will only grow like the square of the team size and seed what we use to call technical debt.
It requires a very thorough understanding of the domain you are working to create a solution in. And time and effort.
The actual context most software engineers find themselves in does not usually encourage (or even allow?) that kind of development.
Creating quality software really is more 'expensive'.
Personally it reminds me of Ubuntu, which was a stripped down, fast spin off of Debian back in the day. It started getting popular, and more and more stuff got added, now it is at least as bloated as Red Hat or the other distros, which it had been compared to when it first started getting attention.
The tension I think I've seen through experience is between maximizing flexiblity and simplicity. Maximizing flexibility without extreme care can often lead to non-simple, over-engineered, over-abstracted, complexity.
Software environments and frameworks need to be generic because they have to support a very wide range of software behaviors on the layer above.
Another point; people tend to think that the terms 'monolithic' and 'modular' are mutually exclusive when describing software - But it's not the case.
Most operating systems are monolithic by nature, but many of these OSes are also modular in the sense that you can replace or customize various parts of them without breaking the system.
The Linux kernel does a lot of different things - It's monolithic by design. It worked out in this case - Otherwise we'd all be using MINIX. Monolithic isn't always a bad thing.
I've found that my most reusable code is composed of pure functions. The imperative stuff is always changing depending on the circumstances but my functions are a solid base upon which I build.
http://blog.ircmaxell.com/2014/10/an-open-letter-to-php-fig....
The generic code rule : "Everything should be made as generic as possible, but not more generic than necessary". That means that every time we see a pattern in the code, we can try to write a generic version. Be that we should never try to do a generic version of something if we don't need it somewhere in the code.
This rule respect, IMHO, both DRY and YAGNI principle.
Phrasing it as "should be made as generic as possible" is just what leads us down the primrose path to over-engineered complexity, no?
Abstraction maybe doesn't at first seem to use like it should be the enemy of simplicity -- certainly we can think of cases where abstraction leads to beautiful simplicity, and that's in fact a large part of what makes software engineering work, and what we enjoy about it.
But in actual life, the pursuit of "as generic as possible" (even if qualified by 'not more generic than necessary") seems to often be the primary motivation leading to conflict with "as simple as possible but not simpler."
> should be made as generic as possible
To:
> should be made no more generic/abstract than actually neccesary.
You've introduced a subjective judgment call. The result is a tautology, since we could rephrase it to:
> should be made no more generic/abstract than it should
Whether or not we agree with the quotes from Einstein or the parent, at least they're (mostly) objective, and tell us how to proceed: if anyone finds a way to simplify or generalise a result (without sacrificing correctness), they should do it.
I suppose the corresponding operationalization to the way I've phrased it is: Don't add genericness unless you are unable to find a way to proceed with what is needed without it.
I think (and I think the OP thinks) the problem we have more often is people adding too much abstraction, not too little. If so, we need a rule that discourages, rather than encourages, the problem.
The issue isn't the concept and understanding it, the issue is that it's hardly ever straightforward to figure out if you need something, or if refactoring your copy/pasted code so you're not repeating yourself is worth the effort, given there are other things that need to be done. The hard part about software is balancing the tradeoffs between time, money, technical challenges, product challenges, not the ground rules.
In essence, they're both partially to blame. There are times when generic code is unneeded, and other times when it will get used in the future. The trick is to know the difference, before it's too late and you have X different variations of similar functionality.
Back "in the day", devs and managers used to call this job security, and was a preferred outcome for most of the people I worked with.
But, unfortunately, this was the mindset in corporate IT back 10-15 years ago, and in fact the last onsite contract job I worked a few months ago seemed to contain this attitude, so I was thinking in some ways, things are still the same.
I couldn't really stand it back then, which is why I moved to contracting/consulting.
There is no doubt that due to the pace of technological change nowadays, the idea that devs and managers can hide behind a bloated solution and rake in profit! are pretty much long gone.
Not exactly. Maximizing genericity leads to a turing-complete language. An API is a set of restrictions you place over a language, completely generalyzing it leads to no API at all.
IDE support for this is great so you do not have to type it twice.
For example, (barring side-effects for simplicity) there is one and only one possible implementation for a function with signature:
f: T -> T
Where T is a generic type. Contrast this with: g: Int -> Int
which has many implementations, many of them possibly not what you intended. If what you intended was the identity function for integers, better use f, because it has fewer possible implementations than g. It's even easier for the reader to understand: since f doesn't mention integers, there's no point in checking whether it does any operation with them. Of course this is a trivial example, but the same point applies to more complex functions.