1. Code to an interface, not an implementation. Think about the essence of the class, without which, it would be a different class. Then build an API for that essence, then implement the API.
2. Program into a language, not in a language. Work out first what you want to achieve, then figure the best way of achieving that in the language.
3. Tell, don't ask. Don't expect client systems to know how your system works. When the state of your domain has changed, tell other systems, don't wait for them to ask.
The "Factorio" game is one of the best explanations of good programming I've ever come across. Build components that do one thing well, then build components that use those components, and keep growing the system in that way. When you want to keep the relationship between two components more flexible, push values from the first component onto a 'conveyor belt' and have the second component pick up values off that belt. Service oriented architecture in game form.
I don't agree fully to that (1). I used to do it, because it sounds like a great idea...
However, the implementation of your use case is often too subtle to get the API right to start with, and you end up having to play around and 'adjust' your API. ie, maintaining it even tho it doesn't even exists yet!
My method is now to implement a 'dirty' integration, without any care of structure or cleanliness, then when it's in place and working only then do I sit down and look at the API I would have liked to have, and implement this by mostly refactoring what is already working.
With that method, you end up with something that is clean, and more importantly does exactly what is needed an no more. There is no bits 'that could be useful later' and are never implemented, or used, or tested, and the model fits the job.
Yeah, this really reminds me of Sandi Metz's "Duplication is less expensive than the wrong abstraction." I know we aren't talking duplication specifically here, but I've eagerly built the wrong abstraction several times and it suuuuucks.
These days I'm all for the "dirty integration" first.
If you have proper functional tests, you can replace wrong abstractions at a cost.
The real issue is code abstracted bottom-up instead of proper design. Such abstractions end up being special cases and become annoying and worthless if requirements (incl. internal ones) change.
While sometimes you can design yourself into a bad end (typically by not taking a really critical requirement into consideration), this is not what often happens.
> The real issue is code abstracted bottom-up instead of proper design.
Exactly. We're (basically) saying the same thing here. I kept my comment short-and-sweet as not to get into the nitty-gritty of everyone's different work situations/requirements/stances on different testing practices/building an app vs a library or service/programming paradigm/etc.
To expand ever so slightly: "These days I'm all about 'dirty integration' first to help inform my ultimate design."
Make it work; make it right; only if proven necessary, make it fast.
Make it work is the dirty version, always comes first without thought of proper factoring or abstractions. Once working and under test, it's much easier to then make it right, which is factoring out duplication and fixing up the code to be clean and well factored. Then and only then if something is too slow and a profiler shows a problem do you make it faster.
No, you're not understanding the context in which to apply this; this isn't something you do at the app level, it's something you do at the feature level. The app is always well structured and well factored, but when adding a feature, you start dirty, get it working, then clean it up, and then optimize it if required, then move on to the next feature. This model works for all sized projects, all the time. The point of doing things dirty is to avoid premature architecture/abstraction which is a bad habit many programmers have.
Now if you have the experience to know beforehand what abstractions you need because you've done this many times, of course you can start cleaner; you do things dirty when you're unsure of what abstractions you might need and don't want to get hung up in trying to invent an abstraction before you know you need it.
It depends on your knowledge/experience levels. A very experienced develeoper's prototype would likely be much better than a junior's production-ready version.
I think this is pretty close to "the right way" (as I see it). However, I want to add one thing:
> With that method, you end up with something that is clean, and more importantly does exactly what is needed an no more. There is no bits 'that could be useful later' and are never implemented, or used, or tested, and the model fits the job.
You want a clean abstraction that gets the job done, and cleanness is better than strict minimality. If you leave big gaps in your abstraction, then it's not a good abstraction anymore, and you'll probably have problems down the road (especially once things get transferred to a support group). It's definitely a judgement call how far you should take this, however. Generally, I want to do what I can to avoid tempting future developers to put new features at the wrong level of abstraction.
If you are writing a service or library to be consumed by other programmers, you don't have much choice but to get the interface as correct as possible up front. Once it is widely use, no matter how much you might want to get all of the consumers to use a new version that fixes design flaws, its going to be extremely difficult. So in this case it is very important to put much careful consideration into the design of the API up front.
Well, at the point where you want to make a 'public' API for any sort of service, one would hope you know enough about the problem you are trying to solve to be able to provide a nice API?
Also, for a 'public' API, I would definitely go for a bit more abstraction, and would attempt at making as future proof as practical.
Of course you can ALSO fall on the other side of the fence with that by providing an API that is so 'future proofed' that they become next to unusable without another API on top! Apple was the Grand Specialist at that in the 90's.
My reply to the post two level up was more regarding /internal/ APIs, the ones you create on top of an existing library for example, to integrate into your own systems.
There's definitely a difference between what I preach, and what I frequently do in practice. It really depends what I'm building, and how well-defined are the requirements.
To ruminate on the parallels with Factorio - if there's one thing that Factorio can really highlight, it's the cost of technical debt, and thus the importance of refactoring.
If you never "refactor" your factories in Factorio, you'll end up with an undefendable and inefficient mess. Spaghetti factories indeed. But if you stop to think, spend some time refactoring how things are working, and build to your new technologies, you'll end up with a megafactory which can help you escape with no problem.
It also highlights the importance of experience and experimentation; finding the proper ratios of furnaces to miners, maximizing the bandwidth of a conveyer belt while minimizing the stagnant materials; once you learn this, your next playthrough will be both faster and require less refactoring.
As for SOA, one thing Factorio taught me was the value of back pressure. Why spend resources producing materials which aren't being consumed? Conveyer belts and movers show the value of backpressure nicely - when the belt is full, movers stop placing items on the belt. The factories stop producing, and thus stop consuming intermediate materials... all the way back to your miners. But the moment that belt starts to clear up, the entire mechanism swings back into work, with enough queued materials to ensure that you are not suddenly starved.
The parallels with programming are a bit more indirect than with actual material production, but they do exist.
>As for SOA, one thing Factorio taught me was the value of back pressure. Why spend resources producing materials which aren't being consumed? Conveyer belts and movers show the value of backpressure nicely - when the belt is full, movers stop placing items on the belt. The factories stop producing, and thus stop consuming intermediate materials... all the way back to your miners. But the moment that belt starts to clear up, the entire mechanism swings back into work, with enough queued materials to ensure that you are not suddenly starved.
This bit of your comment reminded me of The Goal: A Process of Ongoing Improvement[1] by Eliyahu Goldratt. It's a great fictional story/book that does an excellent job of teaching about bottlenecks and the Theory of Constraints. Specifically how to identify them and eliminate them.
I'm guessing you know it, but for those that don't, The Phoenix Project is also an excellent book for understanding the similarities between manufacturing and software development.
I loved discovering the main bus strategy in factorio (everything you produce goes on a very wide main bus, so that others can easily pick it off) at the same time I was implementing an Event Bus (post events to one bus, subscribers listen and react) in some Java code. Perfect parallel, even called the same thing.
The second point sounds a lot like "fight the language", which doesn't seem right.
We had a coworker implement a custom Either object in Java instead of just using exceptions, and it made the interface significantly less ergonomic. I really like functional return values like that, but I've found that it makes things easier to use the paradigms the language provides.
It's not about fighting the language at all, it's about not letting the language dictate the business rules. The functionality exposed by the app should be the same whether I code it in JavaScript, Go or Assembly. How I implement it in those languages is a detail that comes after I've decided what to implement.
1. Code to an interface, not an implementation. Think about the essence of the class, without which, it would be a different class. Then build an API for that essence, then implement the API.
2. Program into a language, not in a language. Work out first what you want to achieve, then figure the best way of achieving that in the language.
3. Tell, don't ask. Don't expect client systems to know how your system works. When the state of your domain has changed, tell other systems, don't wait for them to ask.
The "Factorio" game is one of the best explanations of good programming I've ever come across. Build components that do one thing well, then build components that use those components, and keep growing the system in that way. When you want to keep the relationship between two components more flexible, push values from the first component onto a 'conveyor belt' and have the second component pick up values off that belt. Service oriented architecture in game form.