this post was submitted on 18 Jul 2024
38 points (97.5% liked)

Programming

17477 readers
238 users here now

Welcome to the main community in programming.dev! Feel free to post anything relating to programming here!

Cross posting is strongly encouraged in the instance. If you feel your post or another person's post makes sense in another community cross post into it.

Hope you enjoy the instance!

Rules

Rules

  • Follow the programming.dev instance rules
  • Keep content related to programming in some way
  • If you're posting long videos try to add in some form of tldr for those who don't want to watch videos

Wormhole

Follow the wormhole through a path of communities [email protected]



founded 1 year ago
MODERATORS
 

With this post I've taken a bit more of a practical turn compared to previous Post-Architecture posts: It's more aimed at providing guidance to keep (early) architecture as simple as possible. Let me know what you think!

top 9 comments
sorted by: hot top controversial new old
[–] [email protected] 4 points 4 months ago (2 children)

@arendjr

  1. abstraction != indirection. Abstraction allows you to do a deferred architecture: store this user "somehow". Indirection is just early architecture with more steps: MyUserDatabase class is coupled to one way of doing things - it's concrete, not abstract.

  2. yet another article advocating 'pure functional' flavour, not a pure functional PL. Recommending purity in an impure language is like recommending memory safety in C. All the work is on the programmer.

[–] [email protected] 2 points 4 months ago (1 children)

To 1), that's unfortunately not entirely true. The real abstraction criticized is more like introducing a StorableEntity layer that's provided by a StorableEntityBuilderFactory. So instead of providing a compartment with a stable interface, they introduce a mess of generalizations.

Abstractions should be bulkheads, but in practice they're often more like one of those beads-on-strings door decorations.

[–] [email protected] 1 points 4 months ago (1 children)

@agressivelyPassive moving from 'storing a user in postgres' to 'storing anything in postgres' is a step up in abstraction. Same with moving to 'storing a user somewhere' or moving to 'storing anything anywhere'.

Moving from 'storing an entity' to 'storing an entity via a FacadeBuilderFactory' is not a step up in abstraction, it's only an extra indirection.

[–] [email protected] 4 points 4 months ago (1 children)

No, that's my point. Providing the builder factory as an abstract way to construct an entity, it is an abstraction. It removes you from the actual detail, that's an abstraction. But it also introduces extra complexity, which in turn negates the value of the abstraction.

In reality, the intention is an abstraction, the result is often enough a bad abstraction that introduces more complexity and adds indirection.

[–] [email protected] 0 points 4 months ago

@agressivelyPassive if you routinely call indirections abstractions, then 'premature abstraction is the root of all evil' holds. If you separate the two concepts, you might think differently.

If my team's codebase had a business logic class that had a concrete dependency on an EntityBuilderFactory, I'd vomit a little, but I wouldn't delete it (can't piss off too many people all the time). But I would route around the damage by allowing the class to depend on the EBF *or* something else.

[–] arendjr 1 points 4 months ago* (last edited 4 months ago)

I would suggest indirection is one of the forms of abstraction? Making abstractions to “defer” architecture seems quite counter-intuitive to me, since the thing you’re deferring is implementation. But the architectural part — the abstraction — gets front-loaded. The idea that an abstraction can truly abstract the “how” of user persistence is very much the kind of fallacy I warned against when it comes to leaky abstractions. Do you want to use async methods for persistence or not? Do you want to persist is batches or not? How will you be notified of completion or errors? The answers depend very much on actual architectural implementation, and if you had created an upfront abstraction you may very well find it won’t suffice if any of these variables changes. So no, I don’t think that really defers architecture at all, it merely solidifies your current assumptions about how your architecture will probably look like in the future. If any of these assumptions turn out wrong or simply undesirable later, you’ve made things harder for yourself. So if that’s the risk anyway, it’s fine to just stick with a simple concrete solution when you can.

I do agree pure functions are harder to do in impure languages, but it’s not as bad as your example of C. Rust is very much an impure language, but its borrow-checker helps you to enforce purity constraints. In fact, using pure functions there is a great way to avoid getting into fights with the borrow-checker. If you use TypeScript you can use Immer to aid you, which is also included in the Redux Toolkit.

[–] [email protected] 2 points 4 months ago (1 children)

I've always heard of your "post-architecture" referred to as "evolutionary design".

[–] arendjr 2 points 4 months ago (1 children)

Sure! The other day someone called it emergent architecture. I guess it goes by multiple names :)

[–] [email protected] 4 points 4 months ago

It goes really well with YAGNI. Also DRY without YAGNI is a recipe for premature over-architecting.

This is also one of the main benefits of TDD. There was a really good video that I can't find again of a demonstration of how TDD leads you to different solutions than you thought you use when you started. Because you code exclusively for one single requirement at a time, adding or changing just enough code to meet each new requirement without breaking the earlier tests. The design then evolves.