[HN Gopher] Framework Patterns (2019) ___________________________________________________________________ Framework Patterns (2019) Author : rbanffy Score : 145 points Date : 2021-08-07 13:51 UTC (9 hours ago) (HTM) web link (blog.startifact.com) (TXT) w3m dump (blog.startifact.com) | travisjungroth wrote: | I've come to like a cleaner separation, so I would reach for | things in the order of function, interface and then subclassing | as the complexity demands it. The downside is I've had a hard | time with type hinting and enforcement on callbacks in Python. | Ideally I'd like to register a function with a decorator (like in | "language integrated registration" or "language integrated | declaration") and get highlighting/prompting in PyCharm. | jstimpfle wrote: | My very first choice, which is typically missed by "modern" [0] | languages and approaches, would be to put the stuff in a | buffer, and have the client read it out when it is ready. | | 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". | ptx wrote: | An example of this would be a pull parser for XML, right? A | callback-based API can be built on top of the pull-based one | if desired, but not the other way around, as far as I can | tell, without using a separate thread. So the pull-based | approach (i.e. putting stuff in a buffer) is more flexible. | | 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. | travisjungroth wrote: | I don't find your first two points convincing. The structure | of the data put in the buffer is equivalent to the interface | of the function. The buffer does encourage you to use data | instead of classes. I want _more_ guidance to my users about | the contract that they 're expected to fulfill and I don't | think buffers help with that problem. I do appreciate the | temporal aspect you're talking about. I'd be all about if I | was Erlang, but I don't think it's worth it for my purpose in | Python. | [deleted] | brundolf wrote: | I think the distinction between frameworks and libraries is much | more fuzzy and philosophical than what's presented here. A | framework says "Here's how we're going to do things. I'll allow | you to extend and build out in certain directions, but you have | to use my channels." A library says "Here are some pieces, I | don't know or care what you're going to do with them, figure it | out." A framework is the Apple philosophy applied to code. | | 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. | config_yml wrote: | I'm not sure who put it this succinctly, but I remember it | generalized this way: a framework calls your code, but you call | the library's code. | paozac wrote: | I knew it as the Hollywood Principle: "Don't Call Us, We'll | Call You" | BulgarianIdiot wrote: | A framework calls your code, but in practice the term is | loaded with a set of independent characteristics, such as it | being the frame of your entire application, not just an | aspect of it (libraries can also "call you" in some cases, no | one forbids a library from taking in a callback function or | an object). | | 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. | garethrowlands wrote: | You don't need a framework of any kind to do inversion of | control though. | brundolf wrote: | This is how the OP put it, and I'm pushing back against this | definition. | Banana699 wrote: | This is inversion-of-control principle, popularized (but not | coined) by Martin Fowler in an article of the same name. | | 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. | simonw wrote: | Under "convention over configuration" is this bit: | | > 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... | vbsteven wrote: | The Spring framework (Java) does the same thing for controller | methods. If you need an authenticated user, or a model object, | or a path variable, just add it to the method signature and the | framework will provide it. | | 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. | idiocratic wrote: | The problem with doing this in Python is that there is no | typing or interfaces to help you. Basing it purely on naming | of arguments breaks the assumption that argument names are | local to the function/method. I find this extremely confusing | to reason about, let alone the possible unwanted side effects | if some developer isn't aware that a name is magical. It's | also very non Pythonic for good reasons. | ptx wrote: | I feel the same way (and mostly use unittest instead) but | maybe this technique makes sense as long as its use is | limited to test 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. | simonw wrote: | It can actually play really well with Python's optional | typing. I should add that to the implementation in | Datasette! | EdwardDiego wrote: | I've always just called it "spooooky annotation magic". | | 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. | johnday wrote: | I have to say, any definition of "framework" that claims the | general concept of higher-order functions is a subset of | frameworks, does not seem particularly useful in the day-to-day. | | 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. | kaycebasques wrote: | The use of map threw me off, too. The author mentions React as | an example of that pattern. Not sure why they didn't use that. | Express.js comes to mind, too, as a very grokkable example. | (Edit: or perhaps the author considers Express.js an example of | an imperative registration API?) | travisjungroth wrote: | Swap out "framework" for "inversion of control" in your head | and you might find the article more useful. It's nice seeing | these options listed out. | mirekrusin wrote: | Still plenty missing, delegate a'la macOS/iOS, | singleton/global/envs, explicit parameters/context object, | multimethods a'la clojure/miltiple dispatch a'la julia, | functors a'la ocaml, plugins/convention-based-autoloading, | even monkey patching a'la RoR/active-stuff-style if a form of | configruation/inversion of control, probably more. | johnday wrote: | I agree - the author's use of "framework" here betrays the | article a bit, and "inversion of control" is a much more | accurate portrayal of what they're actually getting at. Of | course this is a natural side-effect of what happens when | terms are born without proper definitions. | | 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. | travisjungroth wrote: | I'd care more about the misnaming if it was frameworks | versus anything else, but it's not. It's frameworks vs | frameworks. You could call it X and say all the code | examples are members of X and I'd find the article just as | valuable. | | 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. | JackFr wrote: | Not a bad article overall but I wish the author had spent some | time talking about error and exception handling. | | 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. | BulgarianIdiot wrote: | I don't think there's anything specific to frameworks about | exception handling. | | 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. | adamnemecek wrote: | I think that a pattern that doesn't get nowhere near enough | attention is the handle [0] (as opposed to objects/pointers) | pattern. | | 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 | | [0] https://en.wikipedia.org/wiki/Handle_(computing) | | [1] https://en.wikipedia.org/wiki/ABA_problem | darepublic wrote: | This was helpful to me, gave it a read and may revisit in the | future. I'm always a bit muddled in my comprehension of formal | coding theory. | smcameron wrote: | The section on callbacks should probably mention that it's best | to allow some sort of context cookie to be passed along to the | callback. This allows callbacks to be re-entrant and to target | any side effects to some particular context. Compare, e.g. | qsort() to qsort_r(). | BulgarianIdiot wrote: | Functions are re-entrant unless they refer to _and modify_ | state outside themselves. If you mean recursive calls, that | happens rarely in frameworks which deal with a very 'flat' | processing pipeline. | smcameron wrote: | No, I don't mean recursive. I mean the callback might need to | read (or write) some state that's different than the state | used by other, concurrent instances of the callback, or other | arbitrary threads, so the framework should provide a means | for it to get at that state that doesn't rely on say, global | variables, so that multiple instances of the callback may be | running concurrently with different state. Typically this is | done with a cookie (in C, a "void *" parameter is generally | used for this.) | | 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. | ptx wrote: | This isn't a problem in any of the languages the article | mentions (Python, Ruby, JavaScript, Java) so that might be | why the author didn't bring it up. Callbacks in these | languages are objects that can carry along their own data. | SinParadise wrote: | This puts into words why I hate subclass based frameworks and get | footgunned by some convention-over-configuration frameworks. | | My preference would be function based, interface based and | annotation based, in that order. | BulgarianIdiot wrote: | Interfaces instead of subclassing is clear as an alternative | and it has the benefit of allowing multiple inheritance. | | 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). | jayd16 wrote: | The first pattern in the article is function based. | Annotations based is called "language integrated | registration" in the article. | ljm wrote: | > A software framework is code that calls your (application) | code. | | 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. | yuchi wrote: | I may have given a too cursory read on this article but it seems | to be confusing the higher level concept of frameworks with the | lower level concept of inversion-of-control. While IOC is indeed | the usual foundation for frameworks, and thus also all | programming approaches to IOC, a framework does not differentiate | itself from competitors by those, but through a wide range of | capabilities offered to users. | [deleted] | [deleted] ___________________________________________________________________ (page generated 2021-08-07 23:00 UTC)