Hacker News new | past | comments | ask | show | jobs | submit login
My favorite principle for code quality (pathsensitive.com)
189 points by spenny2112 on May 22, 2018 | hide | past | favorite | 109 comments

My advice for design: Design is hard. Good design is so hard that you will almost never see it in the wild. It becomes even harder when you work with other people. Your carefully crafted ideas will be crushed under the foot of the next confident programmer who thinks they have found the holy grail of design. As more and more programmers are added, they pour in their particular favourite flavour of programming sauce. The result is swamp water.

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 like the way you think, do you write anywhere else?

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.

I have promised to, but have not followed through with that promise. One of these days maybe...

Although pracitcally what you said is true, many teams I've worked in had seniors that would design projects based on their interests. I'd like to say there is a way to design objectively based on trade-offs for the project. If you ever worked in enterprise-like "frameworks" like Magento you will see the design patterns and how they achieve certain goals in a project. E.G EAV, XML module & layout declaration for flexibility, MVVM for single responsibility controllers meant for large enterprise teams working on the same controller at one time.

Yeah. I think that kind of thing can work in certain circumstances. Definitely I've seen the dictatorial design approach be quite successful -- at least short term. It's pretty awful for the participants, though. Normally your best talent leaves. I'm less convinced about the framework approach. I've never actually seen it work well. Normally the design is just not flexible enough and something breaks pretty badly. Of course, there is a limit to the number of projects you can see in your lifetime, so I could well be missing something :-)

I can't disagree more with this post. One should always aim at trivial code, but as things get more complex, it is there the the sensibility should fire in and make you say, ok I need more abstraction now, and refactor accordingly. But even then it is important to just add the minimum amount of abstraction to generalize the problem at hand, without thinking like "but why if in the future we change things..." Unless it's very clearly going to happen soon.

There's an excellent discussion on this topic in the sample chapter, Rediscovering Simplicity[0], from Sandi Metz's book, 99 Bottles of OOP[1]. I often use it as a source for discussion with devs.

[0] https://www.sandimetz.com/99bottles/sample

[1] https://www.sandimetz.com/99bottles

I like this, though I think anyone who mentions SLoC as a metric is honor-bound to quote Dijkstra: My point today is that, if we wish to count lines of code, we should not regard them as “lines produced” but as “lines spent”: the current conventional wisdom is so foolish as to book that count on the wrong side of the ledger.

I find this true, and depressing at the same time. The amount of curation needed for a constantly evolving code base is vastly underestimated by most, even to achieve the modest design goals (IMO) that you are espousing.

Yep, you have to babysit the code forever or it dies...

I agree, generalize too early, and you are just as likely to overengineer, anticipate changes that never come (YAGNI), constrain the code in other ways that turn out to be unwieldy and incur the mental overhead of a more complex design that each new reader of the code needs to understand. Generalization should mostly happen during refactoring.

> Standard disclaimer: When reading software design advice, always imagine the examples given are 10x longer. Overengineering is bad. But, if you’re not sure whether applying a technique to your code would be overengineering, error on the side of doing it. Abstract early.

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.

Fully agree, let's not go back to instinctively applying all of the patterns from GoF. I'd also like to quibble with just this:

> 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.

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.

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.

Oh! You're a software developer at the local zoo too?

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.

I'm no expert at OOP, but never have I seen a student who telegraphs a look of comprehension after seeing an example of classes involving animals. I've seen many who get more confused.

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...

Mm. For me there are only a few reasons to use inheritance:

- 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.

I agree. Inheritence is useful in a very small amount of situations and can be the start of a long abstract chain of bullshit if other devs are allowed to build more funtionality on top...

I’ve only ever seen inheritance make sense in UI toolkits. That’s literally it.

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.”

> never have I seen a student who telegraphs a look of comprehension after seeing an example of classes involving animals

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.

> Because Customer/Vendor/Invoice was too commonplace?

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.

Once I learned about Unix, I felt that IO devices would make a better example of class inheritance. You have a base abstract class. From that, you get a block device (seekable devices with fixed-sized blocks), a character device (say, a serial port, or a keyboard), a variableblock device (like a tape drive or network, with variable sized blocks that may or may not be seekable). And then you can subclass from there. A more or less realistic example.

I've found Holub on Patterns to be a fantastic resource on applying GoF patterns to real world code.

Thought the exact same thing. Abstract early is not good in practice. You can't understand what problem you are solving until, as the last architect I worked with would say, n is 3. When you have three consumers of a feature, then you start to have an understanding of what the needs are.

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.

I agree. Good architecture is something that can naturally emerge. You start with a simple and less abstract solution and gradually evolve when requirements are better understood. Upfront design mostly does not work.

This boils down to code removeability. A few larger classes wit large methods is in general easier to remove/refactor then a large amount of small cohesive classes with an obtuse design concept behind it. Of all the codebases I refactored, I take the "large methods and large classes" hands down. Pulling a few classes here and there is easy, compared to first understanding a clusterf*ck of overengineered abstractions.

I worked with a domain driven setup at one point, which seemed like it was designed to sell JetBrains licenses because it became almost unbearable to try and maintain the codebase with a text editor. You would have to go through a controller, a DI container, a repository, an entity, a factory, a builder, maybe a facade, and an event bus... almost all of which were single-method classes (except for the DI boilerplate which was split between constructors and YAML files) that just called the method of the next dependent class. One line of concrete business logic hidden between half a dozen files full of architecture.

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.

This makes me think of a good thinking talk, with a quote I pull out from time to time:

> I hate code, and I want as little of it as possible in our product.


I was always disappointed I couldn't find more talks by Jack Diederich after watching this one. His approach was so practical compared to so much programming advice out there. I see now that he's got a few more talks linked from here.

I know how I'll be spending a few hours in the next few days. Thank you for reminding me of his talk!

Indeed. I am always surprised/amused by how often someone will go to the ends of the earth to criticise big up-front design and proclaim that you ain't gonna need it, but then routinely set up several layers of indirection and abstractions in the most basic code, frequently for no immediate benefit other than letting their similarly complicated automated test suite run.

As long as we acknowledge that large classes with large methods can also cause problems. Dealt with that before and it is not fun by any means. The worst offenders, though, are the projects that have a LOT of large classes with large methods, plus some crazy abstraction thrown in like a dash of pepper.

code removeability, or, write code that is easy to delete, not easy to extend: https://programmingisterrible.com/post/139222674273/write-co...

I have to agree. There are OTHER errors that can be introduced by having too many wrapper classes/methods. Clutter (code volume) also adds to causes of mistakes.

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.

> but more code == more errors in general.

you can't have bugs in code you never have to write!

To add to this, don't diminish simple and easy abstractions. There's often a tendency in software development to make use of the fanciest, most complicated tool available, rather than the most practical one. It's easy to forget that just wrapping something up in a handful of static functions and a couple data types is still abstraction, and often it's the right level of abstraction.

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.


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.

> Please take his coding advice with a grain of salt.

I think engineering advices from non-engineers should just be discarded...

I saw that as well and had the same reaction.

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.

A recent quote by Evan Czaplicki of Elm fame that I've come to like; "Abstraction is a tool, not a design goal."

Comments along this line are my favourite in our industry. Harry Roberts said, back in 2014, "Modularity, DRY, SRP, etc. is never a goal, it’s a trait. [...] but understand that they’re approaches and not achievements". This advice has stuck with me ever since.


When I read the article, I was confused about the end result, as I am fully aware of the legacy garbage from overly abstracted code base, and I was very surprised this guy seems fully confident about its approach...

This is great advice but the real problem is that so many developers never go back and change their code. They are always moving forward and never revisit their old solutions.

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 problem is of course that you may then be changing code which has been field-tested for a long time.

I agree but also think it's worth acknowledging the trade off you're making. If you never touch a piece of code, then at some point nobody remembers or is able to understand what it's doing.

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.

As long as the process had been that the only way to get a pull request for a bugfix accepted is by proving the bugfix worked with an automated regression test, then you can change the code as much as you want...

Tests lock you down to particular design by way of the interface. If you want to fundamentally redesign something, you're likely going to have to change the tests as well.

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.

Sounds like you are writing tests at the wrong level then.

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...

We seem to be talking at cross-purposes here. I agree that tests enforce backwards compatibility (or compatibility in general) but they also enforce a particular design. Fundamentally redesigning something generally involves breaking the testa and 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.

This assumes that if you rewrite a piece of code then the possible bugs in the new code are similar to the bugs in the old code. And that is in general not true.

This seems to be industry and not developer driven, business thinks it's like building a bridge and the manpower (and money) is only needed at the build stage, not the maintenance stage. After it is built money can only be directed into adding features and fixing bugs, not to improving code. This applies at both the macro (project/product) and micro (class, module) level.

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.

And the sad thing is, it's the opposite of what they teach you in school.

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.

Thank you for saying it, cannot agree more. Premature derivation of abstractions so often leads to code that is later difficult to modify and understand and leads to a vicious circle of adding more abstraction and complexity.

It's a 'frggin stats page where a developer forgot to remove a call.

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.

I agree. Developers should never build anything more complicated than it has to be at the moment.

Simplicity is key to happiness and productivity.

We shouldn't go too far with this line of thinking either.

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.

Whatever version is in the documentation people read is the default. If it's not compatible to use v2 when expecting results from v1, you can't change the behavior for no version specified anyway.

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.

Versions, timestamps and unique IDs are definitely base design requirements on anything.

Fully agree. KISS

I love articles like these because every time I'm naïvely expecting to learn something new, but more often than not I'm left unsatisfied.

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.

I believe the code presented would have benefited from a lambda-based API approach instead of an object-oriented one. In Javascript:

  // 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 agree. Using the class approach usually leads to complex class hierarchies and a lot of made up concepts for simple things which would be better solved if you could pass functions around easily (lambdas) and had generic data types (like javascript objects). But java does not have these easily available.

I think the OPs problems are mainly coming from the culture/design of Java.

Yes, 90% of abstraction is best done by organizing files and functions, NOT by creating classes.

Unfortunately these are largely the same thing if you're stuck with Java <= 8

This is similar to what I was thinking, but in python and remaining more object-oriented, its API would look something like this: (with essentially the same invalidation logic in the init)

   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.

> The Embedded Design principle, which I briefly introduced in a previous post, states that you should always code in a way that makes the design apparent. It’s like pixie dust in its ability to make code more readable, loosely coupled, and impervious to bugs.

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.

> 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.

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.

Cleanroom has a lot of evidence that process (especially around code inspection/review) does improve quality more than TDD or other methods of detailed design, most people are just unwilling to take the time for detailed process for quality IMO.

That's frequently a reasonable trade off. A lot of businesses need a proof of concept to test product market fit. IMHO TDD can often be a massive waste of time if that is the case.

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.

> 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.

You need to get your domain concepts right, and to let your code reflect these concepts. Then, fewer bugs occur because mapping a business request to code becomes a simpler task. Also, it becomes easier to understand and fix bugs, because again the mental mapping from customer-observable behavior to code is easier.

Still, I agree with the advice to not introduce these abstractions early, because you will get them wrong.

The experience of fixing a bug somewhere in a Ball of Mud is profoundly different than fixing it in better factored code.

I agree with you.

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.

Yes, you can't prove a negative. I'm pretty sure it was a joke.

> “There’s a bug over there because you re-used lastCachedTime. Otherwise, looks good. Ship it!”

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;

He does point out every good programer knows they should have one if check for all three: " Wrapping Up . Many programmers will get a sense that something was off about the first example. Every programmer I’ve shown this example to knew they should merge the if-statements and factor out the prints. "

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.

I feel like this is a long-winded way of saying what Linus already has famously said:

"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.


"Rule of Representation: Fold knowledge into data so program logic can be stupid and robust."


"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."

This was a pretty bad article on achieving code quality.

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.

Hmm.. Grrumble. I think he has the tail of something.... but hasn't stated it well.

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.

I prefer a more focused version of this principle: you should organize your data in a way that makes the design apparent. The code will follow later.

This principle still works with the author's example. Most of the refactoring he did boiled down to modeling the data more accurately.

Algorithms + Data Structures = Program

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.


I also try to stress this principle. Data and its structure is the most important part, and bring accurate and explicit, lets the author communicate their design better than mountains of doc.

Or inversely: if the data presented are complex and convoluted, you will need mountains of docs to try explain its design.

The post indirectly suggest creating domain specific language for solving dashboard presentation problem. In my opinion this shifts engineers focus to solve problems with-in provided domain specific language, which in the end resolves in job security, as non-senior engineers will not "dare" to touch "the framework". Whole situation, in my opinion, results in "everything working on paper" (aka tests, good design, etc) while in reality nothing works and as project progresses engineers get a feeling that "nothing can be done". I would just remove "numWords = countWords();" line and move on. There is no need to invent domain specific language when just programming language is sufficient. I would love engineers to be more reasonable and aware of DSL's trade off.

Robert Piersig illustrates these higher-level concepts, which he calls forms,

Pretty sure that's attributed to Plato. Well I guess he called them είδε but he couldn't wait around for English to get invented.

I think that was one of the great things about Zen. It was an unreliable narrator who had megalomania. At one point, he acknowledges that most of what he is stating is pulled from older greek philosophy that he didn't bother to learn properly but then continues to claim to invent new philosophy through the story.

I get the feeling that most fans of Zen didn't interpret the narrator as unreliable :-/

I agree with the general sentiment, but suggest exercising caution: Every problem can be solved by an additional layer of abstraction, except the problem of too many layers of abstraction!

The version i like goes something like this :

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.

Is that a quote from someone? It nicely sums up my naive experience of trying to "clean up" my code

"Abstract early" is just plain bad advice because you usually don't know the full problem up front. Better advice would be to write code that lends itself resilient to refactoring (i.e. it is easy to refactor and won't break in unexpected ways when you do so)

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.

Ask yourself what assumptions you are baking into the design. Can you make them more explicit? How likely are they to change? Where multiple approaches are on the table, favour the ones with the fewest assumptions. Be especially critical where cardinality is concerned. It's much easier to go from n to n+1 than from 1 to 2.

I find it extremely disconcerting that so many software engineers don't follow this. You're not there to figure out some clever way to do something that you made up. You're first and foremost there to figure out the essence of the thing itself and to find where the design exists naturally. Only then can you really start to design things well IMO.

This is a very complicated solution for a problem the compiler would warn you about (unused variable).

How does the final example solve the daily slowdowns?

Abstract Early? That's kind of Anti-KISS . It's better to start simple and abstract when desired.

It wastes a lot of time when I try to abstract early. You can't solve a problem without all the variables.

I feel like there must be an equation such as:

Peter Principle + Embedded Design Principle = Conway's Law

More often than not AwesomeSauce will end up SpectacularFailure, thus making the exercise in divining the future use case of 30 lines of code pointless

seems the article ends up with what was first done decades ago in PHP - separation of presentation and business logic. Btw i don't understand all this animosity toward PHP, especially given that pretty much all the modern web development is basically PHP-like, in spirit if not in actual implementation.

> i don't understand all this animosity toward PHP

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.

> 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.

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.

For what use case is PHP better than other languages?

Rapid development of performant get/post web interfaces. Ruby is nice but runs a lot slower. Python is better in a general-purpose sense, but every file will have a preamble of includes for things that are just built-in to PHP.

As a general purpose language, PHP is weak. As a web-form DSL, it's great!

Most PHP is actually the opposite... Many PHP scripts, especially those from decades ago, are a mix of HTML, database calls, and business logic all in the same file.

And now we have JSX and this is considered good again.

Jumble code and markup together in PHP, and you've only got one language to put on your resume.

Jumble code and markup together in JSX, then you can put JavaScript, ECMAScript, React/Vue, npm, node.js, babel, and possibly webpack on it.

Can you explain what you mean by "pretty much all the modern web development is basically PHP-like"?

I'm not necessarily disagreeing. I just don't really get what you mean by that.

Just give yourself time to write and maintain PHP code for over one year and you will find the answer. PHP does not have a debugger nor a test runner nor a profiler built in. Someone will say use xdebug and phpunit and etc.. well I want tools that ship with the language and are not slow like phpunit...

Applications are open for YC Winter 2022

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact