Framework Patterns (2019)
blog.startifact.com
blog.startifact.com
In this case, `map` is given as an example of a micro-framework. While it does illustrate the author's point, I think all it does is showcase that the separation really isn't as clean as they want it to be, and it undermines rather than reinforces their philosophy.
It's almost as if the author has defined "blue" as "any RGB colour where B>0", and then their first example is magenta. Yes, it's a nebulously defined concept (both "blue" and "framework"!), but this means that trying to impose a rigid definition on top is bound to fail.
I think this is Grade A software engineering content. Here’s a thing you do sometimes, here’s seven other ways to do it with names, code examples, trade offs and real world examples. It’s great for learning because when you come across this problem in the future you have all your tools laid out for you. I also found it personally helpful because I’m visiting Python framework interface options right now. This saved me a bunch of work.
> pytest also goes further and inspects the arguments to functions to figure out more things.
I think this pattern deserves its own category. I think of it as the Python world's variation on "dependency injection" and I really like it.
In pytest you can use argument names to request that specific test fixtures be made available to your test function: https://docs.pytest.org/en/6.2.x/fixture.html
I use it in Datasette to allow plugins to define their own view functions, which will be passed the specific objects that they declare a need for in order to process an incoming HTTP request: https://docs.datasette.io/en/stable/plugin_hooks.html#regist...
But if you have to work in a new project / code base and there is a lot of this kind of magic, then it's really really hard to figure out how things work and where you need or even can make changes to change behaviour.
I would go so far to say that this is actually an anti-pattern.
The alternative that I prefer: have a sane default that is generated somewhere and searchable for anyone who doesn't know the convention yet. And in the case of pytest, I think this is just a shortcoming of the language in spite of a better way of doing proper dependency-injection. I remember very well that angular 1.x did something very similar and it was very confusing (they called .toString on the function to find the argument-names). It also violates the general idea of being able to safely refactor a method argument name without breaking things.
I don’t know if there is a term for this concept. I’ve always seen it as some form of Dependency Injection/IoC but at the method level instead of object creation.
IIRC the Actix web framework in Rust does something similar for handler functions.
Test functions are never called explicitly and would otherwise (like unittest TestCase methods) never have any arguments, so in this context maybe it's clear that any arguments they do have must be magical.
The decorator pattern seems to fit, from my POV. The framework takes your code, creates a proxy that implements the interface, and then wraps your method in a method that does the stuff you asked for with the annotations before and after your method gets called.
A framework's restrictions can be enforced by IOC, or by integration and compatibility between its subsystems (and incompatibility with alternatives), or just by strong conventions and tutorials/documentation that stick to a beaten path, or any combination of the above. I don't think the particular mechanisms of constraint are as important as the fact that there is constraint.
GP comment says it's not what defines a framework, it just happens to be a succinct summary of most methods that frameworks use to enforce the their philosophy, which is in GP's view what defines a framework: it has opinions and philosophy about how you structure your code, and it wants you to follow them.
And with that, frameworks often become their own universe, where external components need to be "integrated" with the framework in order to enable using them pragmatically at all. So you either rely on the framework for everything, or you go looking for plugins for the framework, or if you need functionality outside it, it has to be integrated.
Inversion of control is a great principle when used with care. In the hands of amateurs, it's used to just replicate the unit version of a "god object", where your entire application becomes a unit defined by the framework.
My preference would be function based, interface based and annotation based, in that order.
It's unclear what "function based" and "annotation based" would mean though.
If you mean passing closures that implement a specific contract, that's in effect "interface based" again. And annotations seem orthogonal in terms of use cases (also full disclosure, I find about 99% of annotation use to be poorly designed and leading to unnecessary static coupling).
At some point code you’ve written is going be called by the framework and throw an exception. Recovery is often more difficult or not possible because you don’t have much knowledge of the calling frame. Are any of these approaches better or worse suited, or do they have any special requirements?
I’m thinking of among other things Java Runnables throwing exceptions and silently killing threads.
You're not supposed to throw exceptions the caller doesn't expect. What does it expect? Well it expects what's documented on the type is takes (either as checked exceptions, or by convention if that's not part of the language).
And any other unexpected errors should be of an appropriate type (Error in Java for ex.) where the framework will finalize its resource handles, and rethrow to some global handler either you or the framework defines. At which point it's in your hand.
And if your exception doesn't fit such a scenario, then probably it shouldn't have been thrown to the framework's stack frames in the first place.
I see the meaning here, but at risk of bikeshedding, I feel as if it understates the relationship between framework and application.
Rails uses the word 'scaffolding' for a bunch of the stuff it generates for you. In that sense, the framework is pretty much all of the foundation and also the load-bearing support structure for what you're actually building. All of the architecture is in place for you, essentially.
It doesn't just call your application code; it is the application.
And yeah, in that sense... programming languages are themselves frameworks over machine code.
Callbacks are usually inferior:
• The client must create lots of small functions that need to conform to some strange interface that needs some context parameter type (void * in C or complicated type hackery in other languages).
• Each of those callback functions is harder and more boilerplatey to implement because it's completely broken out of the client's control flow - there is no context around.
All this applies to inheritances / "interface" mechanisms in some languages just as well. I don't know why we still haven't abandonded this crap, it adds nothing but new words and types to achieve the same things.
What do we gain from using simple buffers?
• Better decoupling of client vs library/framework/whatever implementation: temporal decoupling. Client can decide when is the right time to take some action.
• Client can setup the necessary boilerplate in a single function (stack frame) once, and then process all messages, of all types, instead of having redundant boilerplate for each callback.
• No inconvenient context type (void * or whatever) or other conformance to any interface needed.
What can the library do when the buffer is full?
• Simple - it should back out and return to user code. The user needs to process the existing messages in the buffer first. The user should then call into the library and have it reattempt what it did last.
When do simple buffers not work?
• Only when the library needs some immediate reaction to the "event". In other words, if it is inconvenient to implement the library in an event-driven fashion and is better implemented in a completely synchronous fashion and needs feedback from the user. For example, the library might request some memory from the user that must be satisfied immediately because the implementation can't back out of the current function.
I think libraries that requires the user to make callbacks should be a rare exception, and not the norm.
[0] When I read "modern" and I am in a cynical mood, I tend to think "ignorant of the old, simple, and proven ways".
But the issues you point out around callbacks and passing context parameters apply mostly to C. In languages that support closures it's easy to give callbacks whatever context they need.
Nevertheless I recommend to never make a callback based API unless there is a clear need for callbacks. If there isn't a need, plain buffers (Maybe I should better say "queues") and decoupled processing are the right thing. Callbacks encourage synchronous processing and where they are used to only push to a buffer they are only overhead.
What's a handle? Think of it as a file descriptor. Your code doesn't store the object itself but some sort of index into some array.
I'm partial to the generational arena indices which solve the ABA problem [0] by having a handle that's composed of index and generation (both are uints). Index is the offset into an array. When you remove an element at offset nn, you put offset n on a free list and next time you insert an object, you return an index where the generation counter is incremented. If someone had a stale index (with an old generation counter) to the previously removed object, when they try to access it next time, they will get a null.
This really shines for data models where you have complex relationship. By having all data in a single centralized store and interacting with data using your indices, you can update relationships as needed
This post summarizes well why that is https://floooh.github.io/2018/06/17/handles-vs-pointers.html
Basically, if you write a framework that has callbacks, and you don't make allowance for passing such a context cookie, you're imposing a constraint on your users that you might not mean to. Maybe "re-entrant" wasn't quite the right word, but in my defense, the qsort_r() man page contains this: "In this way, the comparison function does not need to use global variables to pass through arbitrary arguments, and is therefore reentrant and safe to use in threads."
In addition to allowing one to write re-entrant callbacks, it also allows passing in arbitrary data of whatever kind, not just whatever parameters the framework author happened to think of.