Merge branch 'master' into v2.0v2.0

This commit is contained in:
Carson Gross 2024-01-23 12:49:02 -07:00
commit b32925937c
11 changed files with 304 additions and 54 deletions

View File

@ -10,9 +10,9 @@ jobs:
test_suite:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '20.x'
- run: npm ci

View File

@ -75,6 +75,7 @@ Thank you to all our generous <a href="https://github.com/sponsors/bigskysoftwar
text-align: center;
padding: 16px;
min-height: 100px;
border-bottom: none;
}
@media only screen and (max-width: 760px) {
@ -90,26 +91,25 @@ Thank you to all our generous <a href="https://github.com/sponsors/bigskysoftwar
<div style="overflow-x: auto">
<table id="sponsor-table">
<tr>
<td colspan="2">
<td colspan="3">
<a href="https://hydrahost.com"><img src="/img/hydra-hosting.svg" alt="The GPU Marketplace" style="width:100%;"></a>
</td>
<tr>
<tr>
<td>
<a href="https://www.jetbrains.com"><img src="/img/jetbrains.png" alt="Jetbrains" style="max-width:30%;min-width:200px;"></a>
</td>
<td>
<a href="https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next"><img src="/img/Github_Logo.png" alt="GitHub" style="max-width:30%;min-width:200px;"></a>
</td>
<tr>
<td>
<a href="https://www.commspace.co.za"><img src="/img/commspace.svg" alt="commspace" style="width:100%;max-width:400px"></a>
</td>
<td>
<a href="https://craftcms.com"><img src="/img/logo-craft-cms.svg" alt="craft cms" style="width:90%;max-width:200px"></a>
</td>
</tr>
<tr>
<td>
<a href="https://www.jetbrains.com"><img src="/img/jetbrains.png" alt="Jetbrains" style="max-width:30%;min-width:100px;"></a>
</td>
<td>
<a href="https://www.commspace.co.za"><img src="/img/commspace.svg" alt="commspace" style="min-width:200px"></a>
</td>
<td>
<a href="https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next"><img src="/img/Github_Logo.png" alt="GitHub" style="max-width:30%;min-width:100px;"></a>
</td>
</tr>
<tr>
<td>
<a href="https://craftcms.com"><img src="/img/logo-craft-cms.svg" alt="craft cms" style="width:90%;max-width:200px"></a>
</td>
<td>
<a href="https://buttercms.com/?utm_campaign=sponsorship&utm_medium=banner&utm_source=htmxhome">
<img src="/img/butter-cms.svg" alt="ButterCMS" style="width:100%;max-width:200px">
@ -130,20 +130,18 @@ Thank you to all our generous <a href="https://github.com/sponsors/bigskysoftwar
<td>
<a href="https://twitter.com/sekunho_/"><img src="/img/sekun-doggo.jpg" alt="Hiro The Doggo" style="border: 2px solid lightgray; border-radius:20px; width:100%;max-width:150px"></a>
</td>
</tr>
<tr>
<td>
<a href="https://www.dasfilter.shop/">
<img alt="Das Filter" src="/img/das-filter.png" style="width:100%;max-width:200px">
<img alt="Das Filter" src="/img/das-filter.png" style="width:100%;max-width:300px">
</a>
</td>
</tr>
<tr>
<td>
<a href="https://www.pullapprove.com/?utm_campaign=sponsorship&utm_medium=banner&utm_source=htmx">
<img src="/img/pullapprove-logo.svg" alt="PullApprove" style="width:100%;max-width:200px">
</a>
</td>
</tr>
<tr>
<td>
<a href=" https://transloadit.com/?utm_source=htmx&utm_medium=referral&utm_campaign=sponsorship&utm_content=website/">
<img alt="Transloadit" src="/img/logos-transloadit-default.svg" style="width:100%;max-width:200px">
@ -160,7 +158,11 @@ Thank you to all our generous <a href="https://github.com/sponsors/bigskysoftwar
</a>
</td>
<td>
<a href="https://bigsky.software"><img src="/img/bss.png" alt="Big Sky Software" style="width:100%;max-width:150px"></a>
<a href="https://rxdb.info/?utm_source=sponsor&utm_medium=githubsponsor&utm_campaign=githubsponsor-htmx">
<img src="/img/rxdb.svg" alt="RxDB JavaScript Database" style="width:100%;max-width:150px"></a>
</td>
<td>
<a href="https://www.ohne-makler.net/"><img src="/img/ohne-makler.svg" alt="Ohne-Makler" style="width:100%;max-width:150px"></a>
</td>
</tr>
</table>

View File

@ -5,7 +5,7 @@ title = "hx-validate"
The `hx-validate` attribute will cause an element to validate itself by way of the [HTML5 Validation API](@/docs.md#validation)
before it submits a request.
Form elements do this by default, but other elements do not.
Only `<form>` elements validate data by default, but other elements do not. Adding `hx-validate="true"` to `<input>`, `<textarea>` or `<select>` enables validation before sending requests.
## Notes

View File

@ -34,6 +34,7 @@ page_template = "essay.html"
* [Why I Tend Not To Use Content Negotiation](@/essays/why-tend-not-to-use-content-negotiation.md)
* [Template Fragments](@/essays/template-fragments.md)
* [View Transitions](@/essays/view-transitions.md)
* [Model/View/Controller](@/essays/mvc.md)
### Complexity Very Very Bad
* [The Grug Brained Developer](https://grugbrain.dev)

View File

@ -1,44 +1,65 @@
+++
title = "Complexity Budget"
date = 2020-10-29
updated = 2022-02-06
updated = 2024-01-21
[taxonomies]
author = ["Carson Gross"]
tag = ["posts"]
+++
Every application involves managing a complexity budget. A complexity budget can be defined as:
Every software project involves managing a complexity budget.
A complexity budget can be defined as:
> An explicit or implicit allocation of complexity across the entire application
"Complexity" here is defined subjectively (rather than [formally](https://en.wikipedia.org/wiki/Programming_complexity))
and in [Stewartian Terms](https://en.wikipedia.org/wiki/I_know_it_when_I_see_it): "I know it when I see it." Or, more
specifically to software development, "I know it when I *feel* it."
and in [Stewartian Terms](https://en.wikipedia.org/wiki/I_know_it_when_I_see_it): "I know it when I see it."
One of the primary jobs of an application architect is to manage a complexity budget:
Or, more specifically to software development: "I know it when I *feel* it."
One of the primary jobs of an application architect is to manage a projects complexity budget:
* Decide if a given feature is "worth it"
* Decide if a given implementation is "worth it"
* Add in appropriate system boundaries to limit complexity between component
* Etc.
* Add appropriate system boundaries to limit complexity between components
* And so on
Note that attempting to address complexity can, in fact, add more complexity. A good example of this, from experience
is [OSGi](https://en.wikipedia.org/wiki/OSGi), which when applied to an application I was working on, made things
*far more complex*, grinding development to a halt. (This is not to say OSGi is universally bad, just that in this
case, rather than boosting developer productivity, it effectively ended it.)
An infuriating aspect of complexity is that that attempts to address it can, in fact, add more complexity.
A good software architect is someone who manages their software budget effectively, either explicitly or implicitly
A good example of this from experience was when a company I worked at added [OSGi](https://en.wikipedia.org/wiki/OSGi) to the system to manage the
increasing complexity of the project. It seemed like a reasonable approach,
it offered a sophisticated [module](https://www.osgi.org/resources/what-is-osgi/) system,
it was recommended by a newly hired architect, and it even says on the "What is OSGI page":
> OSGi significantly reduces complexity in almost all aspects of development: code is easier to write and test, reuse is
> increased, build systems become significantly simpler, deployment is more manageable, bugs are detected early, and
> the runtime provides an enormous insight into what is running.
What's not to like?
Unfortunately, adding OSGi to that project effectively ground the entire project to a halt: it took a few of our best
engineers out of normal application development for over a year, and when they were done the codebase was even more
difficult to work with than when they started. Feature velocity, already teetering, collapsed.
This is not to say OSGi is universally bad. But, in this case, rather than boosting our development teams productivity,
it effectively ended it.
A good software architect is someone who manages the software budget of their project effectively, either explicitly or
implicitly.
## Complexity Growth
I assert, without evidence, that Stewartian Application Complexity grows roughly geometrically with the size of an
application. By proper factoring by experienced developers, this curve can be held down for quite some time, and this
is one major reason why many good developers are so much more productive than others.
My sense, admittedly without hard evidence, is that Stewartian Application Complexity grows roughly geometrically with
the size of an application. Proper [factoring](https://en.wikipedia.org/wiki/Decomposition_(computer_science)) by
experienced developers can hold this curve down for quite some time.
However, this doesn't change the fact that, somewhere out there, there is a Complexity Wall lurking and, if you aren't
careful you will run into it and grind development to a halt. I have had multiple experiences with this: one day,
inexplicably, development on a system that I was working on went from feeling "large, but manageable" to
"this is impossible to deal with".
However, this doesn't change the fact that, somewhere out there, there is a Complexity Wall lurking.
And, if you aren't careful, you will run headlong into it and grind your development velocity to a halt.
I have had multiple experiences with this: one day, inexplicably, development on a system that I was working on went
from feeling "large, but manageable" to "this is impossible to deal with".
## Spending Your Complexity Budget Wisely
@ -47,23 +68,24 @@ Here are some tools for managing your complexity budget:
1. Foremost: understanding that there *is* a complexity budget that needs to be managed
1. Focus your "complexity spend" on the areas where your application is adding value and/or differentiates itself
1. Saying "No" - probably the easiest, best and, also, hardest tool to use in your battle with complexity
1. Embracing [KISS](https://en.wikipedia.org/wiki/KISS_principle), even if it means admitting you are stupid (It's often very good for an organization if the senior developers can admit they are fallible)
1. Embracing [KISS](https://en.wikipedia.org/wiki/KISS_principle), even if it means admitting you are stupid (Note that it's often very good for an organization if the senior developers can admit they are fallible)
1. Proper factoring of components - this is an art: Too many components and your complexity explodes. Too few... same.
1. Choosing the proper balance of expressiveness and restrictions for a component
Unfortunately, experience shows that managing Stewartian Complexity is a subjective endeavor, and many talented and
Unfortunately, experience shows that managing Stewartian Complexity is a subjective endeavor and that many talented and
experience developers will disagree on the proper course of action at a given decision point.
None the less, by making the concept of a complexity budget explicit, these conversations can be more productive and
ultimately lead to better software outcomes.
Nonetheless, by making the concept of a complexity budget explicit in your software project, these conversations can be
more productive and ultimately lead to better software outcomes.
## A Final Note
All mature applications are complex.
Almost all mature applications are complex.
Finding a new codebase "complex" is *not* an excuse for tearing everything
apart or aggressive refactoring. We must always bear in mind [Chesterton's Fence](https://fs.blog/2020/03/chestertons-fence/).
Finding a new codebase "complex" is *not* an excuse for tearing everything apart or aggressive refactoring. We must always bear in mind [Chesterton's Fence](https://fs.blog/2020/03/chestertons-fence/).
If an application is functioning well (or even reasonably) then we should assume that the complexity budget was well
(or reasonably) managed. And we must also bear in mind that, with unfortunate frequency, attempts at addressing complexity
in existing, large applications often fail or, sadly, make things worse.
(or at least reasonably) managed.
And we must always remember that, with unfortunate frequency, big attempts at addressing complexity in existing, large
applications often fail or, sadly, make things worse.

View File

@ -10,7 +10,7 @@ One of the most common criticisms of htmx, usually from people hearing about it
>You're complaining about the complexity of modern frontend frameworks, but your solution is just another complex frontend framework.
This is an excellent objection! It's the right to question to ask about *any* third-party (3P) code that you introduce into your project. Even though you aren't writing the 3P code yourself, by including it in your project you are committed to understanding it—and refreshing that understanding if you want to upgrade it. That's a big commitment.
This is an excellent objection! It's the right question to ask about *any* third-party (3P) code that you introduce into your project. Even though you aren't writing the 3P code yourself, by including it in your project you are committed to understanding it—and refreshing that understanding if you want to upgrade it. That's a big commitment.
Let's break this criticism down into its constituent parts, and determine exactly how much htmx indulges in the harms it claims to solve.

210
www/content/essays/mvc.md Normal file
View File

@ -0,0 +1,210 @@
+++
title = "Model/View/Controller (MVC)"
date = 2024-01-16
updated = 2024-01-16
[taxonomies]
author = ["Carson Gross"]
tag = ["posts"]
+++
A common objection I see to using htmx and hypermedia is something along the lines of:
> The problem with returning HTML (and not JSON) from your server is that you'd probably also like to serve mobile
> apps and don't want to duplicate your API
I have already outlined in [another essay](@/essays/splitting-your-apis.md) that I think you should split your JSON API & your
hypermedia API up into separate components.
In that essay I explicitly recommend "duplicating" (to an extent) your API, in order to
disentangle your "churny" web application API endpoints that return HTML from your
stable, regular & expressive JSON Data API.
In looking back at conversations I've had around this idea with people, I think that I have been assuming familiarity
with a pattern that many people are not as familiar with as I am: the
[Model/View/Controller](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) (MVC)
pattern.
## An MVC Intro
I was a little shocked to discover [in a recent podcast](https://www.youtube.com/watch?v=9H5VK9vJ-aw) that many younger
web developers just don't have much experience with MVC. This is perhaps due to the Front-end/Back-end split that occurred when Single Page Applications became the norm.
MVC is a simple pattern that predates the web and can be with nearly any program that offers a graphical interface
to a user.
The rough idea is as follows:
* A "Model" layer contains your ["Domain Model"](https://en.wikipedia.org/wiki/Domain_model). This layer contains the
domain logic specific to the application. So, for example, a contact management application will have contact-related
logic in this layer. It will not have references to visual elements in it, and should be relatively "pure".
* A "View" layer contains the "view" or visual elements that are presented to the user. This layer often (although not always)
works with model values to present visual information to the user.
* Finally, a "Controller" layer, which coordinates these two layers: for example it might receive an update from a user,
update a Model and then pass the updated model to a View to display an update user interface to the user.
There are a lot of variations, but that's the idea.
Early on in web development many server side frameworks explicitly adopted the MVC pattern. The implementation
that I'm most familiar with is [Ruby On Rails](https://rubyonrails.org/), which has documentation on each of these
topics: [Models](https://guides.rubyonrails.org/active_record_basics.html) that are persisted to the database,
[Views](https://guides.rubyonrails.org/action_view_overview.html) for generating HTML views, and
[Controllers](https://guides.rubyonrails.org/action_controller_overview.html) that coordinate between the two.
The rough idea, in Rails, is:
* Models collect your application logic and database accesses
* Views take Models and generate HTML via a templating langauge ([ERB](https://github.com/ruby/erb), this is where [HTML sanitizing](https://en.wikipedia.org/wiki/HTML_sanitization) is done, btw)
* Controllers take HTTP Requests and, typically, perform some action with a Model and then pass that Model on to a
View (or redirect, etc.)
Rails has a fairly standard (although somewhat "shallow" and simplified) implementation of the MVC pattern, built on
top of the underlying HTML, HTTP Request/Response lifecycle.
### Fat Model/Skinny Controller
One concept that came up a lot in the Rails community was the notion of
["Fat Model, Skinny Controller"](https://riptutorial.com/ruby-on-rails/example/9609/fat-model--skinny-controller). The
idea here is that your Controllers should be relatively simple, only maybe invoking
a method or two on the Model and then immediately handing the result on to a View.
The Model, on the other hand, could be much "thicker" with lots of domain specific logic. (There are objections
that this leads to [God Objects](https://en.wikipedia.org/wiki/God_object), but let's set that aside for now.)
Let's keep this idea of fat model/skinny controller in mind as we work through a simple example of the MVC pattern and
why it is useful.
## An MVC-Style Web Application
For our example, let's take a look at one of my favorites: an online Contacts application. Here is a Controller method
for that application that displays a given page of Contacts by generating an HTML page:
```python
@app.route("/contacts")
def contacts():
contacts = Contact.all(page=request.args.get('page', default=0, type=int))
return render_template("index.html", contacts=contacts)
```
Here I'm using [Python](https://www.python.org/) and [Flask](https://flask.palletsprojects.com/en/3.0.x/), since I use
those in my [Hypermedia Systems](https://hypermedia.systems/) book.
Here you can see that the controller is very "thin": it simply looks up contacts via the `Contact` Model object, passing
a `page` argument in from the request.
This is very typical: the Controllers job is to map an HTTP request into some domain logic, pulling HTTP-specific
information out and turning it into data that the Model can understand, such as a page number.
The controller then hands the paged collection of contacts on to the `index.html` template, to render them to
an HTML page to send back to the user.
Now, the `Contact` Model, on the other hand, may be relatively "fat" internally: that `all()` method could have a bunch
of domain logic internally that does a database lookup, pages the data somehow, maybe applies some transformations or
business rules, etc. And that would be fine, that logic is encapsulated within the Contact model and the Controller
doesn't have to deal with it.
### Creating A JSON Data API Controller
So, if we have this relatively well-developed Contact model that encapsulates our domain, you can easly create a
_different_ API end point/Controller that does something similar, but returns a JSON document rather than an HTML
document:
```python
@app.route("/api/v1/contacts")
def contacts():
contacts = Contact.all(page=request.args.get('page', default=0, type=int))
return jsonify(contacts=contacts)
```
### But You Are Duplicating Code!
At this point, looking at these two controller functions, you may think "This is stupid, the methods are nearly identical".
And you're right, currently they are nearly identical.
But let's consider two potential additions to our system.
#### Rate Limiting Our JSON API
First, let's add rate limiting to the JSON API to prevent DDOS or badly written automated clients from swamping our
system. We'll add the [Flask-Limiter](https://flask-limiter.readthedocs.io/en/stable/) library:
```python
@app.route("/api/v1/contacts")
@limiter.limit("1 per second")
def contacts():
contacts = Contact.all(page=request.args.get('page', default=0, type=int))
return jsonify(contacts=contacts)
```
Easy.
But note: we don't want that limit applying to our web application, we just want it for our JSON Data API. And, because
we've split the two up, we can achieve that.
#### Adding A Graph To Our Web Application
Let's consider another change: we want to add a graph of the number of contacts added per day to the `index.html`
template in our HTML-based web application. It turns out that this graph is expensive to compute.
We do not want to block the rendering of the `index.html` template on the graph generation, so we will use the
[Lazy Loading](@/examples/lazy-load.md) pattern for it instead. To do this, we need to create a new endpoint, `/graph`,
that returns the HTML for that lazily loaded content:
```python
@app.route("/graph")
def graph():
graphInfo = Contact.computeGraphInfo(page=request.args.get('page', default=0, type=int))
return render_template("graph.html", info=graphInfo)
```
Note that here, again, our controller is still "thin": it just delegates out to the Model and then hands the results on
to a View.
What's easy to miss is that we've added a new endpoint to our web application HTML API, but _we haven't added it to
our JSON Data API_. So we are **not** committing to other non-web clients that this (specialized) endpoint, which
is being driven entirely by our UI needs, will be around forever.
Since we are not committing to *all* clients that this data will be available at `/graph` forever, and since we
are using [Hypermedia As The Engine of Application State](@/essays/hateoas.md) in our HTML-based web application, we are free to remove
or refactor this URL later on.
Perhaps some database optimization suddenly make the graph fast to compute and we can include it inline in the
response to `/contacts`: we can remove this end point because we have not exposed it to other clients, it's just there
to support our web application.
So, we get the [flexibility we want](@/essays/hypermedia-apis-vs-data-apis.md) for our hypermedia API, and the
[features](@/essays/hypermedia-apis-vs-data-apis.md) we want for our JSON Data API.
The most important thing to notice, in terms of MVC, is that because our domain logic has been collected in a Model,
we can vary these two APIs flexibly while still achieving a significant amount of code reuse. Yes, there was a lot
of initial similarity to the JSON and HTML controllers, but they diverged over time.
At the same time, we didn't
duplicate our Model logic: both controllers remained relatively "thin" and delegated out to our Model object to do
most of the work.
Our two APIs are decoupled, while our domain logic remains centralized.
(Note that this also gets at [why I tend not to use content negotiation](@/essays/why-tend-not-to-use-content-negotiation.md) and return HTML & JSON from the same endpoint.)
## MVC Frameworks
Many older web frameworks such as [Spring](https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/mvc.html),
[ASP.NET](https://dotnet.microsoft.com/en-us/apps/aspnet/mvc), Rails have very strong MVC concepts that allow you to split
your logic out in this manner extremely effectively.
Django has a variation on the idea called [MVT](https://www.askpython.com/django/django-mvt-architecture).
This strong support for MVC is one reason why these frameworks pair very well with htmx and those communities are excited
about it.
And, while the examples above are obviously biased towards [Object-Oriented](https://www.azquotes.com/picture-quotes/quote-object-oriented-programming-is-an-exceptionally-bad-idea-which-could-only-have-originated-edsger-dijkstra-7-85-25.jpg)
programming, the same ideas can be applied in a functional context as well.
## Conclusion
I hope that, if it is new to you, that gives you to a good feel for the concept of MVC and shows how that, by adopting that
organizational principle in your web applications, you can effectively decouple your APIs while at the same time avoiding
significant duplication of code.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

1
www/static/img/rxdb.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -11,6 +11,7 @@
border-top: 2px solid gray;
overflow: hide;
margin: 0px;
z-index: 1;
}
#demo-server-info.show {
max-height: 45vh;
@ -25,6 +26,10 @@
vertical-align: top
}
#demo-activity ol li {
list-style-position: inside;
}
#demo-canvas {
margin-bottom: 500px;
padding-top: 12px;

View File

@ -178,6 +178,14 @@ ul li {
padding: 4px;
}
ol {
margin-left: 12px;
}
ol li {
padding: 4px;
}
@media(max-width:45rem) {
#nav {
height: 0px;