7 ms·
> group related concepts together > The hardest part of this process is deciding what “related concepts” mean. The article talks about "readability", but arg
by shoo 3y ago
> group related concepts together
> The hardest part of this process is deciding what “related concepts” mean.
The article talks about "readability", but arguably the unnamed hard problem it is dancing around is how to structure an application or system by decomposing it into modules.
I'd argue the baseline reasonable approach to structuring applications or systems is the one given in Parnas' 1972 paper "On the Criteria To Be Used in Decomposing Systems into Modules":
> We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others.
http://sunnyday.mit.edu/16.355/parnas-criteria.html http://sunnyday.mit.edu/16.355/parnas-criteria.html
Parnas' criterion embeds the understanding that code and systems are not static but need to evolve over time as requirements change or decisions are made, and that different decompositions can be inferior or superior to accommodating that change.
"Don't repeat yourself" refactoring rules of thumb can give poor results if blindly applied. Suppose two sections of application logic just so happen to look similar at this moment in time and get refactored to "remove the duplication" coupling them together, when the two sections of code are subject to different constraints and reasons for change, and will need to evolve separately.
- TeMPOraL 3y agoPeople forget that readability isn't a function of specific program - there is no one optimal readability. On the contrary, it's a function of the program and the goals of the reader. So after fixing and DRYing all the generally bad/inefficient decision, what is readable code becomes solely the issue of why you're reading it - trying to debug or add an entirely new feature will have opposite readability criteria to extending some high-level feature. Even in the best case, readability just becomes a Pareto frontier[0], given by expressive limits of the dominant programming paradigm - same single plaintext source code for all. There's only so much complexity, so much cross-cutting concerns, we can cram into the same piece of plaintext, until something gives, until the same code is beatiful to you one week, and incomprehensible the next week, with the only thing that changed is the type of work you're doing on it. So, beyond evolving over time, I'd also consider the orthogonal aspect of different decompositions being good for different purposes, and that you can't have it all and work on the same, single, high-level plaintext code. EDIT: And I believe the solution to this, the step forward beyond the Pareto frontier, is what 'valty described here: https://news.ycombinator.com/item?id=39426895 https://news.ycombinator.com/item?id=39426895 - not coding directly in the same plaintext, but treating the single-source-of-truth code as a database, which you query and update through views/lenses that best fit whatever work you're doing at the moment. -- [0] - https://en.wikipedia.org/wiki/Pareto_front https://en.wikipedia.org/wiki/Pareto_front
- UweSchmidt 3y agoIf it's not too much trouble, could you create a minimal demonstration of a simple piece of code, structured for various goals - easy to extend, easy to debug etc.? I can't defend my code form the best-practice-people with a Pareto Front wikipedia article.
- ryandv 3y agoThere's two aspects to GP's comment. > Even in the best case, readability just becomes a Pareto frontier[0], given by expressive limits of the dominant programming paradigm > People forget that readability isn't a function of specific program - there is no one optimal readability. On the contrary, it's a function of the program and the goals of the reader. The original Wiki at c2 has a great example [0] comparing the expressive capabilities of functional vs. object-oriented programming, and their suitability towards certain goals - namely, either extending the set of operations the system supports, or expanding the number of data (sub-)types the system models. In spite of Turing equivalence, some paradigms (even if only in terms of readability, dev ergonomics etc.) are better suited for expressing certain classes of problems than others, which may introduce unnecessary friction trying to solve problems that are mismatched with the paradigm's approach to structuring and decomposing problems (e.g. into compositions of functions, or compositions of objects). [0] https://wiki.c2.com/?ExpressionProblem https://wiki.c2.com/?ExpressionProblem
- pjc50 3y agoI'm not sure this is even possible - all the value comes when the underlying application is no longer simple. If you try to make a minimal example it just looks over-engineered, like some sort of "enterprise hello world" joke.
- actionfromafar 3y agoWith enough work I think it is definitely possible, but your comment made realise it would be pretty large, almost a research project. One could start from a real case in a real company, document it thorougly, including different stakeholders. Then rewrite the code to fit this organisation at this point in time. Then, make a two or three fictional changes to the organisation and circumstances, snapshot these (or follow an organisation longitudinally for long enough that this occurs naturally) and for each snapshot, rewrite and redocument the code to fit those circumstance. From that, one could "dumb down" the whole thing until it stops making sense and see how simple one could make it. Probably there are people smart and seasoned enough to write an entirely fictional account of all of this, and still have it make sense - that's not me though.
- calvinv 3y agoI haven't worked on a project where we've know all our problems up front and most of the time the complexity is added to cater for "flexibility" but that rarely ends up being a useful implementation for what we actually needed. It's great to hide this from other areas but you will need to work on it and it will impact how the software is architected
- lupusreal 3y agoIn my experience, most programmers go way too hard with anticipating future changes and end up creating systems with entirely too much abstraction. Most of the time those changes never occur and the result is a codebase which has been obfuscated with excessive abstraction that bogs down anybody trying to maintain it. Future changes end up needing different abstractions than the ones which were preemptively created, and as the programmer is adding new abstractions to cover their present need they also create new premature abstractions, thinking they're saving themselves future trouble. The cycle then continues. Better to KISS and leave abstraction for the future when it actually becomes necessary. If you start out with code that is only as complex as it needs to be in that moment, then it will generally be much easier to change it in the future.
- jpc0 3y ago> Better to KISS and leave abstraction for the future when it actually becomes necessary. I love this statement and for me personally is also perfectly encompassed in YAGNI. You ain't gonna need it. Until it's proven you need that piece of code, don't write it.
- surprisetalk 3y agoAuthor here :) Wasn't familiar with Parnas' criteria, thanks for sharing. I do something similar in a different way, which I call "IKEA-oriented development". IME, semi-disposable code is very easy to change over time as mental models and product goals evolve: [1] https://taylor.town/ikea-oriented-development https://taylor.town/ikea-oriented-development
- shoo 3y agoThank you for the post, and the link to this second one as well. Re: "IKEA-oriented development", you make a very good point about the cost of change. I think the semi-disposable code idea overlaps comments from folks elsewhere in this discussion thread talking about the horror of codebases that introduced premature abstractions to cope with expected future changes that then never actually appeared ("YAGNI" is indeed a good rule of thumb). Your point about "make experimentation effortless" is a good one. The highest productivity environment I worked with that supported rapid experimentation was a small business' monorepo codebase with good test coverage and rapid feedback from CI, where the library code was only used internally by the company's software products (i.e. all the abstractions were implementation details, not part of any external interface). Over time we'd learn that some of our early ideas for abstractions in the internal libraries were flawed, but because these abstractions were internal, and we had confidence in the automated test coverage, it was possible to make quite large scale improvements to abstractions rapidly with confidence as we learned more. The kind of environment that really bogs down experimentation and impedes change and improvements to abstractions is where an initial idea for an abstraction is resourced with its own development team and turned into a production service, and then another half a dozen internal company services start depending on it. Then it's very easy to end up in situations where everyone becomes aware that the abstraction is flawed, but improving it is less "one developer goes dark for a week or two and emerges with a 50-patch PR that atomically replaces the flawed v1 abstraction with v2 while passing all test suites in all projects that depend upon it" and more "project managers, product owners and enterprise architects compare roadmaps for the next few quarters to figure out how many years it might be until a prototype of the v2 abstraction can be ready for manual testing in the integrated test environment". Maybe in the worst case there's some initial decomposition of the system that is flawed, then an org chart is spun up defining teams that own components matching the flawed system decomposition, so refactoring to improve the decomposition would also require refactoring the org chart to change people's teams. Then instead of having colleagues indifferent to or supporting a purely technical refactor, people will resist it to avoid change to their roles!
- BenoitEssiambre 3y agoThat's interesting. Knowing when to decompose systems into modules indeed seems to be key. This is a complex problem because, I think, the choice of the optimal model depends on the uncertainty you have about the reality behind the data, about what you know and don't know about the domain you are modeling. But there might be optimal solutions rooted in information theory and Basyesian probabilities that you can strive to approach while programming. This is about avoiding over-fitting or under-fitting your domain knowledge. Theoretically speaking, finding the right Bayesian fit optimizes for future evolution of the code and how it generalizes into the unknown, how correct your software will be when faced with things you haven't specifically designed for. More here: https://benoitessiambre.com/abstract.html https://benoitessiambre.com/abstract.html If I were to add something to abstract.html blog post, it would be something about Dependency Length Minimization ( https://www.pnas.org/doi/full/10.1073/pnas.1502134112#:~:text=sentences%20according%20to https://www.pnas.org/doi/full/10.1073/pnas.1502134112#:~:tex... ) which has important information theoretic ramifications (for example, files with shortened dependencies tend to compress better and LLMs became much better when they solved for managing dependencies with their "attention" mechanism). When an abstraction breaks out a piece of code to enable reuse, the reduction in redundancy should be weighted against the stretching of dependencies to decide whether the abstraction is warranted. The original article acknowledges this by mentioning "locality". Other things to take into account is how tests fit into all this. Again more here: https://benoitessiambre.com/abstract.html https://benoitessiambre.com/abstract.html
- shoo 3y agoYour linked blog post "Abstraction and the Bayesian Occam's Razor" is very interesting. I'll play back my understanding to you, to see if I'm approximately following and summarising your thesis. Context: When programming we attempt to design an effective abstraction that models some domain. When designing this abstraction, there are trade-offs between reducing the amount of code required, enabling reuse, reducing coupling, flexibility to accommodate future use cases. Key Problem: How do we design an abstraction for our domain model? Claim 1: Apply the "Minimum Description Length" (MDL) model selection principle: prefer a domain model embedded in the shortest program able to recreate the dataset of domain knowledge. Applying MDL model selection will result in an abstraction for the domain model that is both smaller -- giving less code to maintain -- and more likely to generalize to future unknown use cases. Complication: Applying the MDL model selection principle relies on having access to a dataset of domain knowledge. We can think of this dataset of domain knowledge as a list of (situation, expected behaviour) pairs -- c.f. a labelled supervised learning dataset, or a gigantic list of requirements. Unfortunately, in typical software projects, no such explicit dataset cataloguing the requirements or expected behaviour in each situation exists. Claim 2: We can use the automated test suite as a proxy for the dataset of domain knowledge. When designing our abstraction we should prefer a domain model where the combined size of the logic for the domain model and the size of the corresponding test suite* is minimal. * with the important caveat that "just cutting out the tests, or removing other safeties like strict types doesn't give you a lower MDL, in that case, you're missing the descriptions of important parts of your data or knowledge".
- Izkata 3y ago> "Don't repeat yourself" refactoring rules of thumb can give poor results if blindly applied. Suppose two sections of application logic just so happen to look similar at this moment in time and get refactored to "remove the duplication" coupling them together, when the two sections of code are subject to different constraints and reasons for change, and will need to evolve separately. I'm fairly sure I remember reading somewhere that that piece of advice was originally meant for data/values/configuration, not code, and that applying it to code is itself a mistake that keeps getting repeated.
- aranchelk 3y agoRegardless of who said it and what they meant, I don’t want more code to write tests for, more pages to read through when stuff breaks, more material for new engineers to learn. You can always start copy-pasting and make a mess later - less true in the reverse. Like almost anything it can be taken too far or misapplied. In the quoted example, when I have multiple occurrences of related business logic, I build a vocabulary of reusable sub elements - find the joints and carve, don’t build a giant mutant.