Hacker News new | ask | show | jobs
by contingencies 2039 days ago
I don't personally feel any version of code presented is good. At a high level, none of those magic values should be in the code. The whole thing should probably be in a database.

In terms of your algorithm, you would want to decouple your ingredient prep from your cooking algorithm. Otherwise, if prepping ingredients takes longer because you buy a new prep tool, your food winds up over or under-cooked. Secondly, you want to decouple your cooking algorithm from your equipment model. Otherwise, every time you upgrade your oven you need to rewrite every recipe. But this is all a digression.

In future if you want to make a point about software, I would recommend using either English or real code and not a stressed analogy to a novel domain. But in general, it seems you are still learning the craft. It's really great that you are thinking about the evolution of a codebase over time as this is a key area that people earlier on in their career miss, and IMHO one of the greatest learning experiences for a programmer is maintaining non-trivial system over an extended period as the environment and requirements change.

Oh, and check out https://web.archive.org/web/20021105191447/http://anthus.com... (1985).

7 comments

I disagree with you. Introducing a database too early is overengineering. My first principle is KISS. I have encountered code that was similar to the example: it was the handling of messages received from automatas in a nuclear plant. I have done some refactoring so that the code had a structure closer to the specification (we had very good specifications). It was quite similar to the third example with recipes.
I tend to think people underestimate the hidden complexity of sequential programming. Each statement has a potential, opaque effect on the complete state.

To prove anything in the OP solution would be extremely complex. To extract new knowledge and abstract the solution in the future would be nearly impossible without a complete rewrite.

For example: what if we need logging? Timing of the steps taken? A list of dish washing tasks generated? Parallelism in the tasks, given an extra cook? Exception control? Unit testing of the dough?

Adding an extra recipe is not the only possible new requirement you can have. Anticipating and preparing the right abstractions, that’s what good software engineering is about.

"Anticipating and preparing the right abstractions, that’s what good software engineering is about."

I find this a rather surprising statement. It sounds almost like something from some weird parallel universe. In my universe I generally take for granted that anticipating abstractions is not something that actually works.

Let me give an example. Some years ago I was working on some code that was writing and reading data from a database. A colleague said that we need validation so every field that can be written to the database needs to have the ability to have a validation. So, optionally a validate function can be attached to each and every database field. I was against it at the time because as I stated I do not believe in anticipated abstractions. But the colleague was convinced that it was necessary and wrote this. A few years later indeed validations had been added but literally all validations where about properties that a set of fields together should have and literally none of them were about single fields. At some point I just deleted the single field validator. It had been there for years but it never was anything besides completely useless.

To me anticipating abstractions is a recipe for all kinds of over-engineering. The need for abstractions arises as more requirements need to be fulfilled and when writing those one should probably think about what is likely going to be desired in the future but anticipating them before it is needed is something I have given up a long time ago.

I absolutely agree with this. I always like to say, the best way to make code extensible in future is to make it do its current job as simply and clearly as possible.

If you leave a hook in for some feature you "know" is coming you'll only screw it up - either that feature won't be needed or it'll look different than you expect. Your field validator is a great specific example, I think I'll use that one in future.

(A corollary of this though is that you have to be happy to ruthlessly refactor existing code when a new requirement actually does enter the scene, because of course that requirement was deliberately not prepared for in the existing code.)

A hook is not an abstraction. A validation function is not an abstraction.

An abstraction would be to say: "These programming statements are actually objects." Or, the time and ordering constraints implied in this program should be explicit.

In addition, I was talking about anticipation and preparation of abstraction. Not the actual abstraction.

> A hook is not an abstraction.

By "hook" I didn't mean anything specific, like a React Hook. What I meant was adding some extra abstraction that has no purpose except for some future feature. For example, taking a class that works perfectly well on its own (let's say Rectangle in a drawing program) and separating it into a base and derived class (let's say Shape and Rectangle) in anticipation of a feature in a future release (next time we're going to add a Pentagon class too and that'll need to derive from Shape). If you don't consider that change to be adding an abstraction, then I suppose we just have different ideas of what an abstraction is.

> In addition, I was talking about anticipation and preparation of abstraction.

I wonder if you've just used a word that doesn't reflect what you really mean. Maybe you just meant designing and creating an appropriate abstraction for the current requirements? "Anticipate" would be the wrong word for that.

"Anticipate" literally means making an educated guess about some future event before information about it becomes available. Most of us have experienced other devs aniticipating future requirements, and creating abstractions in response to that anticipation, and the inevitable negative fallout of that. So seeing "anticipation ... of abstraction" is bound to generate a negative emotional response.

Programming in my world is about anticipating abstractions, without actually abstracting. We write code for it to be changed. We know to a certain extend how the specifications, design and code are likely to change over time. We anticipate the kind of abstractions needed to accommodate that change.

This in turn influences the choice of programming language, unit testing, naming, modularization, infrastructure...

Just as a simple counter example: I write my code so I minimize the amount of state and provide good type information. The advantage is that future (re-)composition in other abstractions of the code will continue to work.

If your familiar enough with the problem, you can usually predict fundamental requirements and design in anticipation of them.
Boy, who has time to be familiar with a problem anymore? I used to be familiar with problems, now I just slug through problems created by other people.
How do you have their problems become your own?
I think there is an important difference between anticipating abstractions and actually abstracting.

In your example, you were actually implementing an abstraction and anticipating a future use. That is something completely different.

What I am trying to bring across is: there are multiple 'simplest' solutions. It helps to know how, in the future, you are expecting to abstract away from that solution to newer 'simplest' solutions.

For example, one might want to use a FP-ish approach, because simplest solutions within that space tend to abstract better than an OOP approach. Or in the recipe example, we could have modeled the steps as objects with dependencies. Given the right programming language, that solution might be as simple as the OP, but provide many more extension points.

This is the essence behind YAGNI. I think it's something everybody disbelieves to begin with and only learns through suffering the pain of their own mistakes.

At least, I've really struggled to convince people that it's true until reality hits them in the face.

I completely agree. One thing I would add that you shouldn't anticipate abstractions, but find them through business domain analysis. Exactly like OP missed that there's ingredients, recipe and equipment.
Thanks. I intentionally stayed outside of the business domain, since I believe it is a subset of the problem domain. But, then again, I agree the orientation should be around the added value and as such, most likely, the business domain.
Programmers write code. Good programmers think about data movement
Good programmers know how to "grow" software. Starting with the right level of simplicity, and adding complexity when needed.
Honestly in the real world you would generally already know if the need expressed is meant to be a web service (for instance) and if it requires a database - and in those cases, most framework already propose an sqlite / fully-fleged database in the same code.

If we already know that a database might be required later then using something like sqlite straight is smart move that would allow you to write code that is instantly going to work when you actually need something fully fledged instead of re-writing it.

Knowing if you need a database or not is simply one of the first things you know, and writing code before setting that up is a waste of time or a learning process.

It is not whether you need a database or not. It is "how configurable/dynamic things should be". More configurability usually means more power to your users and more pain to maintain the software. So I usually go to the least amount of configurability and then add things when required. The same goes for abstractions (you often need more of them when things becomes more configurable)
I've been writing code for more than twenty years now and I had the opportunity to try many different styles.

It is, unfortunately, very easy to write difficult to read code, especially with good intentions and principles.

In practice, I've found that the most important principle is Locality, that is avoiding nested indirections and unecessary abstractions.

I completely agree with the author of the article here, the simple and dumb recipe with constants local values is both easy to read and easy to maintain.

It might seems like duplication but the complexity has to live somewhere and it is more manageable when it is not scattered.

I tend to agree with you, however, there's sometimes so much complexity that you really do have to abstract it to manage sanity.

The issue I often see is that when abstractions are created, the thought process or design of those abstractions aren't well explained. When you create abstractions, more verbose documentation and design is needed to share the ideas. Anyone using this in the future needs to understand your abstractions and there's a cognitive cost of dealing with it.

You reduce this cost when you explain everything well, give examples, show use cases, etc. When you don't provide this sort of verbose documentation, you might as well have made it a large sequential program because it likely would have been easier for the next person to understand.

Yes, this is often more difficult than it seems, this is typically something that requires experience and wisdom.

When unsure, it is still better to write less abstract code and somewhat messy code than falling into the over-engineering trap.

It will cost less to fix and it will also cost less to write in the first place.

> In terms of your algorithm, you would want to decouple your ingredient prep from your cooking algorithm. Otherwise, if prepping ingredients takes longer because you buy a new prep tool, your food winds up over or under-cooked. Secondly, you want to decouple your cooking algorithm from your equipment model. Otherwise, every time you upgrade your oven you need to rewrite every recipe.

The author chose a simplified example to demonstrate a point.

Also, without a proven a need, building for these considerations would result in an over-engineered solution.

"The author chose a simplified example to demonstrate a point."

Shrug. I too can make any point if I get to choose my own contrived examples. And when a bad example is chosen, like here, any reactions will devolve into bikeshedding about the appropriateness of that example (as is clearly seen in this thread).

Well, that’s programming: it’s not about proving one point. It’s about proving every possible point. The level of ability to abstract should be much higher than the ability to add an extra recipe.
> It’s about proving every possible point.

Writing maintainable software is about solving the problem you actually have as simply as possible. Introduce abstractions only when you need to.

This! I know that it's often tempting to demonstrate your abilities in the software you create. But introducing abstractions and DRY code is only necessary, when you have a complex problem with nany repeating parts that would become unmaintainable otherwise. If that is not the case, keep it simple. This is similar to premature optimization.
That's exactly the point I want to bring across. As simple as possible, but not simpler. However, solving one problem, but causing dozens down the line is not engineering, even though your problem is 'the simplest'.

You see, in the 'problem solution lattice' there are multiple bottoms [1] (most simple solution). Knowing which 'simplest solution' is the best, depends on your knowledge of the problem, your anticipation on how the problem might be extended in the future, your array of possible solutions, etc. etc.

For example, the original solution to the recipe problem in OP uses a very sequential and object oriented approach. That solution is one of the infinite number of solutions that fixes the problem. The extension of the problem (an extra recipe) plots a graph upwards through the latice towards another solution that fixes both the original problem, as well as the new problem. The number of steps required (the distance) is dependent on choice of the original solution. And since there are multiple bottoms, we could have a solution that is not able to be further simplified, even though there is a parallel bottom, that has a much shorter distance to the second solution.

[1] https://en.wikipedia.org/wiki/Lattice_(order)

> But in general, it seems you are still learning the craft.

This is a completely unnecessary personal attack on OP.

We are all learning the craft.
I feel that you are missing the forest for the trees here
I believe you have missed the point because you think this is literally about running recipes to bake cookies.

The code is the data here. Imagine instead of a program to make cookies it is two different scheduling algorithms for an operating system.

At this point I'd only put things in a database if they're actually runtime dynamic - recipes would be, I guess. I'm just bitter / venting because the codebase I inherited has a ton of form definitions in a database.