[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)