I like this. Now I have a fancy sounding name that I can throw around to counter some of the other fancy ideas that lead us to scatter logic all over the place.
In my opinion, which often seems like a minority opinion on the teams I've worked in, these questions should be (made) easy to answer:
- Where is this thing defined? For example a method. Surprisingly often it's hidden in a concern, behind a wall of indirections, or even generated by some clever metaprogramming.
- What happens when this thing runs? Hidden side-effects, nested method calls 17 layers deep, operator overloading, and interfaces can all make this harder.
- Why did this code run? Seems like it should be an easy thing to answer, but imagine that the stack trace is truncated because of some creative use of exception handling as control flow, that the call was made from a different process, or that it's in a callback that's been passed around like a hot potato.
I have a hunch that these kind of problems often pops up when you treat state and logic as two separate things. Then the way your code is organized won't match how state is managed. State transitions can happen anywhere, and the customer won't be reassured to know you used a great design pattern if it still takes you a week to figure out why their account balance turned negative, why their bill was sent twice, or why some users suddenly have access to things they shouldn't.
Some solutions I can think of:
- State machines, since they make state transitions very explicit.
- Functional programming to some extent. Immutability and pure functions might force you to handle state explicitly. It's still easy to make the logic messy though.
- I believe reactive programming is mainly an attempt to solve this? Are there any good implementations for other uses than front-end apps? Does it go by a different name?
- Hexagonal architecture?
What are some nice solutions that you've seen to these things? Patterns that take you in the direction of making critical things easy or even trivial to understand.
>htmx, for example, allows you to place many attributes on parent elements in a DOM and avoid repeating these attributes on children. This is a violation of LoB, in favor of DRY, and such tradeoffs need to be made judiciously by developers.
This is correct but this has little to do with DRY. This is more about making connections clear, which parents tinkering with children in non-explicit ways does not.
>The primary feature for easy maintenance is locality: Locality is that characteristic of source code that enables a programmer to understand that source by looking at only a small portion of it.
Richard's quote at the top of the article was not about deleting or inlining all abstractions so that the code is all in front of the programmers face, it was about making connections or dependencies clear. DRY flat out helps with "Locality of Behavior" by giving the programmer confidence that if they change so-and-so variable or function it will be updated properly throughout the code base. Non-DRY code would require a programmer to search the entire codebase to make sure their change is propagated properly. Not very "local". It also lets the maintainer focus on the LOCAL code instead of wadding through 60 lines of boilerplate such as "setup sql connection".
Basically please avoid conflating DRY with syntactical duplication.
Here we have two buttons that both target the div `someDiv`. We are repeating that information across both buttons. In an OO language we might be tempted to create a base class or dependency inject this behavior to avoid this repetition, ie to keep things DRY. In htmx, we would hoist this to the parent div:
This eliminates the code duplication on the two buttons, we are no longer repeating ourselves, but at the cost of sacrificing some locality of behavior. Here, the decrease in LoB is relatively minor, but the further you move from the buttons the more serious the violation is.
I think the concepts hold up, when considered in this light.
That would be called implicit structural coupling for the benefit of reducing syntactic boilerplate. And as you note, sometimes it can be a good trade.
Much like functions that have to be called in a specific order are called "temporarily coupled"
It's ironic to see essay about LoB from htmx author who made attribute inheritance as default framework behavior. Basically you can guarantee nothing by looking at piece of htmx code. It can do different things depending on place in DOM tree.
I view the issue pragmatically: I think developers should consider LoB strongly when building systems and when using htmx, but I also recognize there are other design considerations (principally DRY). I can definitely see both sides of the argument, however, and would encourage people not to abuse attribute inheritance, just like I would encourage people not to abuse generics in OO languages.
Every few years the tide appears to turn and what was old becomes new again. In the early 2000s web standards were all about separating logic from presentation, eliminating all inline code (can cause messy delayed browser rendering), etc. Recently SPA not only reversed that but made it fashionable. The reality is you forget your own code within a few months and when you have a team they need to be able to contribute to your code over time, so most code is not “fully known” when modified. The way a lot of projects go south is by becoming “hairy” - you don’t know all code that affects a module or a piece of logic, complexity gets added, functionality gets replicated, conflicted, etc. The key to surviving this is organization and documentation of the code under the assumption that nobody knows the code and needs to onboard quickly. If the code has a lot of abstractions - they will be forgotten, so you’d better have tooling to surface or explain them when it matters. If the code is self-explanatory (partly why I like Go), you have a lot less headaches.
The locality of behavior can break down easily when functional complexity increases. Because front end and backend and user experience and state and appearance are caked on together, any custom aspect to either of them will force you to segment out code into more files or sections. So, what if we assume that is bound to happen and plan for it instead. What if there is a way to visualize all dependencies, possibly with a tool? Tracing, logs, flame graphs, maybe more? Still not ideal as they don’t fully capture all “ends” and “sides” of that hairdo.
the crucial distinction here is between definition and invocation or usage:
it is fine to have complex behavior defined elsewhere, but then, as much as is possible, the usage of this behavior should be clear on the element using it
using the functions as a familiar example: it's not necessary that functions be defined inline to satisfy LoB, but it is necessary that the invocation of the function be colocated on the element on which an event is triggering the function
I usually follow this locality of behaviour in the following situation: imagine you have several features that all of them require an object of type A, another of type B and one of type C. Usually, specially in languages with packages, it is very common to put all A objects inside a typea package, all B inside typeb and all C in typec. I prefer to put app A, B and C objects of a single feature in the same package (or if possible in the same file). This is not always desirable, depends on the context and the number of features, but I prefer it.
A different example, instead of:
var a = init()
var b = init()
var c = init()
do(a)
do(b)
do(c)
I prefer
var a=init()
do(a)
var b=init()
do(b)
var c=init()
do(c)
Well, up until now it hasn't been called anything formal, which is why I'm trying to give developers some language to (however imperfectly) describe this approach. :)
Isn't that roughly the same as literate programming, where all code related to one piece of functionality is grouped together, so that it can be read linearly as one block?
Totally agree with the stance, but it's pie in the sky stuff when it comes to real world, often imperfect, codebases.
Take for instance a piece of code I was reviewing earlier today. A class in one package referenced a function in a database manager class, which itself referenced a function in a DAO, which in turn contained some SQL. Just to figure out what was going on I had to read, parse and follow through on each. And this is the recommended design pattern too!
Exactly this! I like the concept a lot but perhaps an event handler being declared in-line in the component template isn't the best example of when this concept becomes really useful?
Event handlers in the HTML template look great for toy examples like <button hx-get="/clicked">Click Me</button>.
Personally, I think any reference to code within the HTML is bad - especially anything referencing code like the “clicked” identifier. It starts to look like line noise when there are many event handlers, and also I prefer my HTML to be as pure as possible (which makes validation and editing easy). Instead code and events within the component JavaScript/TypeScript should reference named nodes within the HTML template - nodes named by purpose.
I wrote a frontend framework doing this, and it worked really freaking well and components were a pleasure to code. The template didn’t need any tricky parsing, instead it was just boring HTML. Never mixing HTML and strings avoided most XSS opportunities. Most of the DOM is inherently secure.
The idea of HTMX is to mostly eliminate JS, and move everything into either HTML or the server. It's kind of like old fashioned <form>s, but for all elements and for more dynamic pages.
This allows the frontend to be simpler, while the backend is ironically also less complex because the only special feature you need is support for sending partial HTML (fragments). This as opposed to needing to embed your entire frontend framework in your backend to allow your website to load in a reasonable time, run in browsers with JS disabled, be easier to crawl, etc.
That said, do you have a link to your frontend framework? I always enjoy seeing different approaches to the same or similar problems.
Svelte fits in nicely in this respect. I like being able to see all of the elements composing a component or view in one file (or as much as is reasonable).
In my opinion, which often seems like a minority opinion on the teams I've worked in, these questions should be (made) easy to answer:
- Where is this thing defined? For example a method. Surprisingly often it's hidden in a concern, behind a wall of indirections, or even generated by some clever metaprogramming.
- What happens when this thing runs? Hidden side-effects, nested method calls 17 layers deep, operator overloading, and interfaces can all make this harder.
- Why did this code run? Seems like it should be an easy thing to answer, but imagine that the stack trace is truncated because of some creative use of exception handling as control flow, that the call was made from a different process, or that it's in a callback that's been passed around like a hot potato.
I have a hunch that these kind of problems often pops up when you treat state and logic as two separate things. Then the way your code is organized won't match how state is managed. State transitions can happen anywhere, and the customer won't be reassured to know you used a great design pattern if it still takes you a week to figure out why their account balance turned negative, why their bill was sent twice, or why some users suddenly have access to things they shouldn't.
Some solutions I can think of:
- State machines, since they make state transitions very explicit.
- Functional programming to some extent. Immutability and pure functions might force you to handle state explicitly. It's still easy to make the logic messy though.
- I believe reactive programming is mainly an attempt to solve this? Are there any good implementations for other uses than front-end apps? Does it go by a different name?
- Hexagonal architecture?
What are some nice solutions that you've seen to these things? Patterns that take you in the direction of making critical things easy or even trivial to understand.