5 ms·
I agree with you except on the topic of abstractions. Everything in software is an abstraction. Source code is an abstract representation of the running progra
by didibus 4y ago
I agree with you except on the topic of abstractions.
Everything in software is an abstraction. Source code is an abstract representation of the running program. Functions are abstract representations of various execution that differ only in their parameters. Classes are abstract representations of stateful concepts of various instantiations that vary only in the information each instance contains. Etc.
You cannot "not use" abstractions when you program, you can simply design poor abstractions or design good abstractions. And a lot of people underestimate the second degree impact of even the smallest of abstraction design choices, like what is and isn't factored into its own function, what parameters the function takes and return, what classes we have, how big they become, etc.
And while saying no to new use cases that are almost same but slightly different is a good way to avoid needing to abstract more things (or avoid needing more code in general), and good engineers push back when necessary here, you will eventually have to deliver what the users want, and you cannot always convince them that all users want the same exact behavior as all other users, there will sooner or later be good business justifications for it.
That's when if you don't introduce good abstractions to tackle the problem, you'll end up in an unmaintainable mess which will turn your velocity to a crawl.
But it's important to understand abstractions have to be designed in tandem between product and tech, because how the user feature is exposed is also an abstraction, both the feature abstraction and the tech abstractions have to be well designed and must help each other out to remain manageable and coherent and work well with other features.
In my opinion this is where really great engineers distinguish themselves, it's the ones who can figure out the good way to abstract that delivers that business international expansion where each region has slight variations yet feature parity can be maintained accross region, as an example of when you won't be able to push back and the abstractions will become critical to success. Normal engineers brute force their way through this slowly, each new region taking longer to expand too then the previous took, and with new features being impossible to launch globally anymore. While great engineers figure out the right set of abstractions alongside the product to enable fast expansions with judicious configuration/plugins to handle per-region variations, etc. They invest in tooling, frameworks and platforms that create those kind of abstractions, and just have the good instinct around balancing tradeoffs to deliver simple yet powerful abstractions that actually makes you faster and the system more maintainable.
Bottom line, I find abstractions are critical to success, saying to avoid them I believe is misleading, you cannot avoid them, you need to get good at them instead.
- portsnadaptors 4y ago> And a lot of people underestimate the second degree impact of even the smallest of abstraction design choices This is one of the hardest lessons I've had to learn as an engineer. Building extensible abstractions upfront is extremely difficult to achieve without building a lot of abstractions upfront that aren't extensible, and learning why. > you cannot avoid them, you need to get good at them instead I'm working on it!
- KronisLV 4y ago> You cannot "not use" abstractions when you program, you can simply design poor abstractions or design good abstractions While I agree with the comment as a whole, I think a part of the argument is about not introducing unnecessary abstractions which might couple code that might be better off remaining separate. As another commenter put it: > The best codebases I’ve seen have an abstraction layer at the foundation and the rest is just dumb repetitive code built on top of that layer. https://news.ycombinator.com/item?id=33569197 https://news.ycombinator.com/item?id=33569197 Suppose that you're building a system for supporting a sales process and keeping track of bunches of data. And based on some BPMN diagrams, you might have multiple different sales processes to keep track of: A, B, C. At a surface level, all of them deal with some company receiving money in exchange for goods and services, but the particulars of each process (the steps involved, the documents, the state transitions etc.) are going to be different. What's worse, they're going to change with time and evolve separately. So, the aforementioned and boring architecture would be a bit like (which might get created across 5 years, as new processes need to be supported): SalesProcessAResource/View (REST/UI) <--> SalesProcessAService <--> SalesProcessARepository <--> sales_process_a_schema SalesProcessBResource/View (REST/UI) <--> SalesProcessBService <--> SalesProcessBRepository <--> sales_process_b_schema SalesProcessCResource/View (REST/UI) <--> SalesProcessCService <--> SalesProcessCRepository <--> sales_process_c_schema Each process might have a separate schema with different tables, different services and repositories and resources for them (not unlike microservices, but assume that this is within a single codebase, maybe modules). There would certainly be overlap which wouldn't be ideal and in the first iterations those might seem somewhat similar/redundant, but this way you can evolve each part separately. For example, if your sales process A now needs to have products have groups in front of them, or even products that can only be bought as package deals/sets, then you can change all of the layers: SalesProcessAResource/View (REST/UI) <--> SalesProcessAService <--> SalesProcessARepository <--> sales_process_a_schema And then there is basically no risk of you ruining or breaking the other sales processes, because they're all decoupled and are handled differently. Now, the alternative to something like that is: SalesProcessCommonResource/View (REST/UI) <--> SalesProcessCommonService <--> SalesProcessCommonRepository <--> sales_process_common_schema I've often seen is developers trying to create "the one solution to rule them all", which leads to code that's really abstracted, maybe the SalesProcessCommonService has some abstract methods that are implemented by separate services, which makes your code paths harder to track, maybe there are now enums all over the place or your code is full of if/else or switch constructions. Worse yet, your schema might look like swiss cheese, some of the columns always being empty because the data normalization isn't up to par, or perhaps going in the opposite direction and needing to query 10 different tables to get the data that you need, or worse yet "classifier systems" that I've seen a lot of in my country, basically the worst of OTLT/EAV patterns: https://tonyandrews.blogspot.com/2004/10/otlt-and-eav-two-big-design-mistakes.html https://tonyandrews.blogspot.com/2004/10/otlt-and-eav-two-bi... I've personally seen many similar outcomes in projects, all the way to needing to introduce some data in the middle for one business case, and creating faux records there because the schema doesn't work otherwise, for example: SELECT ... FROM sales_processes INNER JOIN sales_sets ON sales_processes.id = sales_sets.sales_process_id INNER JOIN sales_products ON sales_sets.id = sales_products.sales_set_id AND sales_sets.code = 'NO_GROUP_FOR_PROCESS_ONLY_LINK' WHERE sales_processes.code = 'SALES_PROCESS_A' or something like that. Of course, one can come up with the right abstractions for something like that, or extract common parts of code, but only as long as they're not logically decoupled and the code is only incidentally the same (or close to it). Also, in some cases you might need a time machine to see what the requirements will be in a few years, otherwise you risk digging yourself a hole by over-abstracting things and going off in the wrong direction, to the point where you'll have thousands of person-hours sunk into an inadequate implementation, or at least one that becomes inadequate in the light of changing requirements. That said, there are definitely easy cases where abstractions are of use without giving it too much thought. Think along the lines of code that is either purely functional or close to it, "utilities" that you can use as necessary with simple function calls or maybe injected dependencies, versus something that makes you restructure your entire code because someone went on a power trip after reading an OOP book.