My favorite principle for code quality
pathsensitive.com
pathsensitive.com
Good design is not about the code. Good design is about the people who are going to work with that code. Understand your audience. Get to know what they like and what they don't like. Write your code in a way that will delight your audience. Give them what they expect to see.
Of course, this is exceptionally shallow advice because no programmer can actually be so selfless. If you write code that pleases your audience you will usually have to do so at the expense of yourself. It is not possible to write good code that you dislike -- at least not long term.
So the trick to good design is always putting a bit of yourself in the code and always putting a bit of your colleagues in the code. It's striking that balance where you are satisfied, but also where they are satisfied. This requires working in small increments, and sharing what you are doing. It requires watching what your colleagues are doing and looking for opportunities to borrow. It requires questioning, evaluating, praising, explaining and coaching.
And if you are working by yourself: do whatever the hell you want. Why are you so worried about what other people think? If you are trying to improve what you are doing, don't do so by reading about the opinions of others. Read code, not blog posts. Write code, not design diagrams. Work slowly, try out ideas you've seen in other code and evaluate their success. When you get the chance, work with others to broaden your horizons.
I'm only in my first year of designing and writing code professionally, much of what you point out about this being 1. difficult, and 2. a social/human endeavor rings true.
Please, please, please absolutely disregard this advice. More errors, pain and suffering come from early abstraction and poor understanding then not enough abstraction.
Build simple, extend as needed. It's a 'frggin stats page where a developer forgot to remove a call. It could of been written much cleaner initially, but breaking it into a bunch of classes at the initial stage is fruitless abstraction.
The rationale was that the abstraction was necessary to make things easier to replace if they weren't needed but it was a false assertion on two levels (and it almost always is):
- that kind of replacement is unlikely to happen in the short/mid-term, and if it does it won't be in a way you anticipated.
- the simple version could be deleted and rewritten in less time than it would take to fix your highly abstracted/decoupled/meticulously architected integration
Of course, simple isn't easy and this approach to abstraction (where you take classes/methods longer than x lines and extract them into more classes and methods) is very easy to achieve... at a great cost.
> I hate code, and I want as little of it as possible in our product.
I know how I'll be spending a few hours in the next few days. Thank you for reminding me of his talk!
I've been on the wrong end of those abstractions and it can (and often does) end up resulting in a lot of pain for everyone involved.
https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstracti... is pretty relevant.
And a call that any decent set of tools would surely have highlighted as redundant at that.
I'm all for being clear about the design of a program, but I don't think the example here is convincing, and I agree with the parent that over-engineering can itself damaging.
From the author's page:
> I work at MIT trying to make program transformation and synthesis tools easier to build
So, the author works in an academic environment. He doesn't have to deal with real-world code, budgets, bugs, teammates, etc. Please take his coding advice with a grain of salt.
I think engineering advices from non-engineers should just be discarded...
I don't know what particular mistakes could come about in this case, but more code == more errors in general. Wrapping stuff into mini abstractions is not always an improvement.
As a rule of thumb, if some code pattern repeats 5 or more times, an abstraction wrapper is probably justified. Between 2 and 4 is a situational judgement, but lean toward skipping the wrapper. KISS.
you can't have bugs in code you never have to write!
To put it in another anecdote. I dealt with a SOAP api for a successful company that was purposefully designed to have no versioning. This meant that every decision that went into the API had to live forever and be backwards compatible with every decision that had ever come before.
This leads to the form of architecture I find to be the least useful. One where the architect tries to anticipate and solve all future problems. In my experience, this never leads to software that can grow or evolve. It's fine if you are solving a known problem and can set known boundaries around which your solution will never be applied. I, personally, have never encountered problems like that in the field.
Simplicity is key to happiness and productivity.
When things are immutable design early.
Consider an api. You can build a basic api without a version parameter when you release. When you need to change the api and keep backwards compability you introduce a version parameter. You are forever stuck with the first version being the default.
More important for an API is building in a way to signal users that the version they're using will be or has been discontinued, and a way for the users to test that.
> When reading software design advice, always imagine the examples given are 10x longer.
How about: if you're peddling software design advice, take some time to make (or find) a realistic example so I don't have to imagine so hard. Even if it's in a toy-problem type of scenario the code can be realistic. Working Effectively With Legacy Code has spoiled me by setting such a higher standard than most peddlers, since it actually presents real code (sometimes 3 pages of it at once) in real languages (Java and C++) using styles and highlighting problems I actually see in legacy codebases with those languages, and how to address them.
Somewhere around 1989 I swore that the next author of an OOP book that used animals as class examples was going to get an angry personal visit from me.
"Suppose you have an Animal class. We subclass to a Cat, and add a Meow method..." If it wasn't animals, it was cars: "we'll subclass Car to create a Ford class, add a Horn property..."
Because Customer/Vendor/Invoice was too commonplace? Between Customer classes and Animal classes, I'll give you a hint as to which I've created more instances of.
Humming birds, I tell ya thank goodness for multiple inheritance! I was able to inherit from both birds and bees. It saved me a ton of work. There was the time I used the flying mixin on sharks though, that was such a mess to cleanup.
Classes are a neat way of hiding implementation detail behind a mini-api that has reasonable code-hygiene benefits and works well in a team setting. None of these things have anything in common with meowing cats.
There was a classic on HN a while ago [1] illustrating how blindly guessing an OOP hierarchy for a problem isn't going to help. Throw that at a learner without thinking too hard, and they are going to question what the benefits of OOP even are - it doesn't always simplify a problem.
[1] https://www.quora.com/Is-abstraction-overrated-in-programmin...
- Common operations between classes, operating on common data, but requiring an external API (so composition is a pain because you would have to proxy those actions to the member.)
- Restricting/specifying the types of objects you can store in a container if you are programming in a language/codebase that cares about that (incl. the C++ "definitely has the vtable I want".)
And maybe that's it? I guess all the taxonomy talk might be useful in the first hour of learning about inheritance, but after that I think the analogy should give way to more concrete "what are the code and data doing?" angle.
It’s like if someone took the cascading idea of CSS and decided “this works so well for UI styling, let’s build a language paradigm out of it and convince people they need to express every problem in terms of it.”
I think it's a classic case of teaching people the answer before the question.
Animal examples explain what types and subtypes are, but the thing that warrants explaining is why and when it's useful to separate things into types and subtypes, and animal examples are terrible for that. If someone asks "should Cat and Dog inherit from Quadruped? Mammal? Pet?" there's no useful answer.
I don't know which I hate more. Animals and cars, or this. And don't get me started on portfolios and stocks.
Some of us learned programming having absolutely zero clue what an invoice is. Hell, I remember being sad that all this fun knowledge is introduced with these weird, boring examples from bankers' world, as if written for PHBs.
The point is, I guess, no examples are ideal. That said, animal examples probably deserve a special place in hell, as they mess up not just with your understanding of OOP, but also biology.
And only by practice, and making the mistake enough, did I reached the same conclusion as you.
Although there is a balance, as sometime you know your future requirements, and making an abstraction right away may save you some time. It's hard to teach experience and the ability to evaluate something vaguely.
I constantly re-writing and re-organization code as the problem changes but I feel like that's the exception rather than the norm. We need to teach that change is good and a normal part of the process.
The power of automated testing is really to ensure that nothing changes, which is great when doing bug fixes, but not so great for actually evolving software.
Ultimately it just becomes easier to add new code than it is to ever change a design that is already in place.
I was talking about a regression test for a fix for a bug found in production. For a typical backend, write such tests against the publicly exposed API.
If you end up breaking tests as you refactor it means that you have broken backwards compatability for others who use the API...
To put it another way, if you never break your tests you can only ever fix bugs or add more code -- you can never remove code or redesign.
I think this is worse than changing code that has been field tested but admit I am biased because I work on a large project where we are beginning to run into this problem.
We've got a weird situation where the best (best potential) devs are not financially incentivized to reach that potential, the money is in being a locust, showing up, devouring all local resources and then moving on to the next green field.
Design patterns are a great thing to understand but they can be dangerous when used incorrectly. I've had to deal with overly design patterned code and it can be a nightmare. You go searching and searching for the "sharp tip of the spear" where you can actually get something done and then you find out that you need some sort of special object. Then you find out that the object doesn't have a constructor it has to come from a factory method. And you can't just call that factory method with parameters, oh no, you need to pass in some special property bag object, and so on.
Don't worry about trying to show off, worry about making your life easier. The ideal situation is that if you want to do some new thing X that is similar to but slightly different from other stuff your system does then you will not only have a good idea of how to do that with the existing primitives, utility methods, abstractions, etc. but it will also be a fairly straightforward task to add that functionality.
Ask yourself these questions about your code base:
- how easy is it to read the code and figure what it does?
- how easy is it to figure out how to use it?
- how confident can you be that the code is correct just by looking at it?
- how difficult is it to maintain and add new functionality to?
- how easy is it to test?
Let those guide your designs.
It's always the same pattern and it's like the programmer's version of the one weird trick. Show ten lines of (contrived) code and talk about its potential to be flawed, and then 'fix' it by explaining 50+ lines of code that has to be abbreviated or split into pieces to keep the article flowing. What quality actually means in this context is unclear, but it feels like tidying your bedroom as a child: hide the clutter in various places to give the appearance of cleanliness, while actually creating more of a mess for the next cleanup in the process.
And like many programming maxims, advising to abstract early is just as bad as advising to not abstract at all. Better abstraction will become more apparent as you see how your system fits together and how parts of it can be lifted up to a higher level, and this kind of intuition will only come with patience and experience.
// global, since the cached values were apparently global in the original
let cache = {};
let lastCachedTime = 0;
// Utility for fetching potentially cached values, or computing them
// if there's no valid cache entry.
let cached = (id, recompute) => {
// I don't love this policy, but thankfully it's all in one place.
if (lastCachedTime <= lastMidnight() || lastCachedTime <= lastNoon() {
cache = {};
lastCachedTime = Time.now();
}
if (!(id in cache)) {
cache[id] = recompute();
}
return cache[id];
};
// now we get to have a nicely factored, simple, conceptually independent,
// declarative code for the dashboard.
function displayDashboard() {
print("Total Users: " + cached("numContent", => countUsers());
print("Articles written: " + cached("numArticles", => countArticles());
print("Words written: " + cached("numWords", => countWords());
}
This achieves the same "Embedded Design" goal. If you squint, it's actually virtually the same as the DashboardStatComputation/DashboardStat/Dashboard the author comes up with, where each lambda in the displayDashboard function is corresponds to a DashboardStatComputation.I prefer this lambda-based design because it reifies the "form" into a single, callable, function: cached(). If you want to use the idea of cached(), you don't have to subclass anything or know about its implementation details— just call it.
(PS. the code's even nicer in CoffeeScript)
I think the OPs problems are mainly coming from the culture/design of Java.
cache = GroupedCache(key="dashboard_stats")
print("Total users: {}".format(cache.get(countUsers))
print("Articles written: {}".format(cache.get(countArticles))
print("Words written: {}".format(cache.get(countWords))
To me this would make it more apparent when reading the code that these should all be cached together (your functional one, with a different cache strategy, makes it look like it's acceptable for them to be cached for 1 hour, but offset from 30 minutes from each other due to external circumstances), and while pretty irrelevant for this particular example, would avoid the miniscule race condition if the clock ticks over midnight between Total Users and Articles Written.I'm all for good software design, but claiming a design process can eliminate bugs is more fantasy than reality. Bugs typically come from unforeseen requirements or application states.
Yes, planning reduces bugs, but bugs are still inevitable no matter what software design process we use.
Issues with communication were directly attributable to ~1/3 of the bugs in software I've worked on. These bugs are unfixable with any process as they are caused by not stating a requirement/feature/bug in a way that is understood perfectly by the developer writing the code.
In my experience, that's only true code that's relatively well-engineered to begin with. Unfortunately, there are lots of bugs in real-world shipping software that have nothing to do with an unforeseeable environment, but are only the result of unnecessarily complex designs that even the original programmer hadn't completely thought through. So while good design might not eliminate bugs, bad (or no) design can definitely create them.
There's weirdly not a lot of good advice out there on sensible steps to reasonably transform a rushed proof of concept in to a production quality app. Mostly it all starts with the assumption that you're building something from scratch.
Cleanroom is actually completely reasonable for this - what you must accept is that transforming a rushed proof concept into a production quality application is starting from scratch in terms of ascii in github. It's the experience of those that worked on it that is valuable to save.
Still, I agree with the advice to not introduce these abstractions early, because you will get them wrong.
The fixed code still has a bug. It assumes successive calls to lastMidnight() within the method will always return the same date. But presumably, if partway through the method it ticks over to midnight, that would not be the case. So some stats would be updated during one call, and the rest would be updated during the next call.
That would violate this requirement:
> They had previously decided all stats should refresh simultaneously;
Which corresponds to the one if part of the story: " if (lastCachedTime <= lastMidnight() || lastCachedTime <= lastNoon()) { numUsers = countUsers(); numArticles = countArticles(); numWords = countWords(); lastCachedTime = Time.now(); } "
So having the multiple ifs being bad even before a second cache refresh is added is part of the story.
"Bad programmers worry about the code. Good programmers worry about data structures and their relationships."
The author refactors some code to revolve around a "DashboardStat" and explains it came from "thinking about how the code is derived from the design." Personally, I think it came from putting the data & its structure first.
gives some of the genealogy of the idea.
http://www.faqs.org/docs/artu/
"Rule of Representation: Fold knowledge into data so program logic can be stupid and robust."
https://users.ece.utexas.edu/~adnan/pike.html
"Rule 5. Data dominates. If you've chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Data structures, not algorithms, are central to programming."
First, there was a bit of bashing of design patterns and refactorings and says we can forget all that in order to make it sound like he was going to drop some magic fundamental wisdom. Which he then says is "Embedded Design Principle".
Then he proceeds to describe an evolution of a piece of software into a bit of a mess, then proceeds to solve it by "AND here is a design that solves it". No method or process for getting to that design, just "here it is!" .... drop the mic, exit stage left. So basically his advice is dont write crappy code, some how magic up a design that might or might not be robust for the future that reveals your design.
Looking at some of his other stuff, he uses a similar "strawman" ish approach to first highlight some technique like (TDD), come up with some crappy code, and use it as some kind of justification for what he thinks.
Funny thing is, the stuff he trivialized actually are exact micro techniques that allow one to create code thats shows its design.
However, for this article, there is a simpler idea that is useful when tackling code like this ( I first came across it in martin fowlers UML Distilled ) and that was the idea of "Levels of Perspective". Which there is Conceptual, Specification, and Implmentation. Since his code seems to have purposely mushed all those things together, its an easy way to look at this code and make it better. I won't go into the exact details, but given a piece of crappy code either in the beginning stages or even the end stage and start decomposing it, first seperating specification and implementation, then as one gains insight, seperating things into composable conceptual ideas. To achieve this, you want to use things like TDD, Refactoring, Patterns, but, and the real trick, only as is demanded to shape the code into a simple design that is modular and composable.
Personally I keep coming across code where the "design" as indicated by names and comments indicate one thing.... but the fact of the code indicates another design.
Now if this indicates a bug, ie. Incorrect behaviour, yup, fix it.
But usually it has been "tested into the shape of a product". ie. The names and the comments and the data structures indicated first, draft, incorrekt, thoughts... and the reality of the code _is_ the correct behavior.
Usually at that point I refactor to reveal what the design really is, down at the mathematics of the code, not what the designer thought it was.
This principle still works with the author's example. Most of the refactoring he did boiled down to modeling the data more accurately.
Or inversely: if the data presented are complex and convoluted, you will need mountains of docs to try explain its design.
This was something that was stressed by my mentor, who was a mathematician turned programmer and a big fan of Niklaus Wirth. It has proven valuable to me on numerous occasions and informs design.
https://en.m.wikipedia.org/wiki/Algorithms_%2B_Data_Structur...
Pretty sure that's attributed to Plato. Well I guess he called them είδε but he couldn't wait around for English to get invented.
Any problem of abstraction (in programming) can be solved by adding a layer of indirection. Any problem of performance can be solved by removing a layer of indirection.
Additionally, the authors "better" design now abstracts the primary performance problem out of sight of the developer. The main problem I see is that they are making multiple passes through the data to extract different metrics. By abstracting early they miss the opportunity to improve the performance by calculating all the metrics in one pass. The new design has hidden the root cause of the problem and prevented a developer from fixing the core problem without chucking out the entire design.
It wastes a lot of time when I try to abstract early. You can't solve a problem without all the variables.
Peter Principle + Embedded Design Principle = Conway's Law
Jumble code and markup together in JSX, then you can put JavaScript, ECMAScript, React/Vue, npm, node.js, babel, and possibly webpack on it.
I'm not necessarily disagreeing. I just don't really get what you mean by that.
Most PHP code is terrible. That's not really PHPs fault - it's capable of producing very nice programs with beautiful behavior.
As a language, it suffers a bit from being older and it was internally very inconsistent for a long time. For a long while, it's package management and practices were way behind the curve. Many people who program in PHP learned it as a first language, hacked together programs in it, and probably used hack together libraries and tooling.
PHP can be used with grace, but most people don't have that experience with it.
As a general purpose language, PHP is weak. As a web-form DSL, it's great!
This describes me very well, and i don't think i'm alone. When i look back at my(very early, first language) PHP code it looks horrible. A lot of it is just copy pasted code from random websites telling me that that it does what i wanted it to. Which is a pretty good description of me at a young age trying/learning to program by internet. I don't use PHP today, and i really don't want to, but i suspect that is in some part due to me struggling with stuff i never understood when i was doing it and developing a distaste for it looking back at my bad code from that time without realizing it could be done differently/in a way fairly close to what i do in my current work-language. I just wanted to make my WoW dkp page work and searched the internet when wondering how to and ended up writing really bad code. It still worked pretty good and i had fun doing it but i never considered writing PHP again once i moved on.