Community Context Exercise/Learning Discussion

Yes - but at the same time I think the sometimes obsessive concern with extensibility and reuse is what has gotten the practice of OO into trouble.

Seems to be a direct consequence of Chapter 13: Programming Erlang 2e:

In Erlang we have a large number of processes at our disposal, so the failure of any individual process is not so important. We usually write only a small amount of defensive code and instead concentrate on writing corrective code.

The Erlang philosophy for building fault-tolerant software can be summed up in two easy-to-remember phrases: “Let some other process fix the error” and “Let it crash.”

This seems to give rise to a tenet of relying on a set of well-defined, pre-selected contingencies whenever a “you can’t get there from here” situation arises - rather than expending effort on trying to micro manage the error response based on a plethora of detailed error information that may or may not be available.

An error occurs:

  • let’s give up OR
  • let’s try again again later OR
  • let’s try something entirely different

You can’t get any more loosely coupled than simply responding to the presence of the error while not depending on any of the details about the error.

And once even a single client depends on that field you’ll break it when you need to take it away again or move it, i.e. by offering too much detail you are limiting your own potential for future change without breaking things outside of your own boundary. Too much detail can be just as damaging as too little - possibly more so.

1 Like

@peerreynders
What I’m missing from your remarks is a differentiation between errors a user cannot directly act on (your car example) and ones, where the user can indeed act on. If there are components of your system failing to work as expected you can easily return a generic error and log the rest. But if things depend on the user you might as well tell them what is wrong with their side of it and not just “sorry, couldn’t work with that, please fix it on your own”. E.g. I like it when my printer tells me about the empty paper cartridge without me having to diagnose, why it won’t print. Same for empty inks. And at best my printer should tell me both issues at once, because then I don’t have to walk to my printer twice.

Lets address this specific example first. One thing you should notice is the impact that the error had on your objective. You started:

  • I want to print this.

error happens

  • I want to fix the printer.

Printing something and diagnosing printer problems are separate concerns - as per ISP that would benefit from separate interfaces - irrespective of the fact that they relate to the same physical object in the tangible world.

Different clients have different needs but they should all aim to minimize dependencies - to the benefit of both parties, the clients and the provider.

Some clients just want to print - if it fails they either give up or just try again later in case the problem was fixed. These type of clients should only be dependent on the printing interface - and to keep things simple, errors should be kept simple; printing either works or it doesn’t.

In some cases the change in objective is permanent. A periodic job prints a work schedule on paper for a high-EM environment. The job can’t fix the printer but it can do it’s best to speed things along. Now becoming dependent on the diagnostic interface becomes a deliberate choice. Rather than just sending a “failed to print” email to the admin, relevant information is gathered from the diagnostic interface to be included in the email.

As far as design goes it would be just as legitimate to avoid the dependency on the diagnostic interface and decide that it is good enough to stick to “failed to print”. In most cases the operator has a separate detailed console or dashboard to the printer anyway (and has to go to the printer to actually fix it), so the effort of duplicating the printer status in the email could be viewed as a form of “waste for the sake perceived convenience”.

Finally the case that you probably had in mind was that the new objective is ancillary to the original objective. Again the diagnostic interface can be used to aggregate the necessary information. But you should’t blindly send all available information but instead a priori determine what information is actually relevant to the end user and just send that. (And your original objective is still nixed if it turns out that you didn’t stock up on ink/toner - but that’s on you).

Now developers like interfaces where their favourite IDE can just latch onto those capabilities so that they can just look at the list of what’s available, pick and choose stuff and move on. However if that diagnostic interface was designed to minimize dependencies there will likely be only one single capability - something that accepts a diagnostic query. The good news is that the intent is to support older queries indefinitely while adding more advanced capabilities later - but that proprietary query language is just a different form of tight coupling - so probably need to put a façade around that.

In the “real” world printing often goes to a queue. That’s nice and asynchronous - i.e. loosely coupled. But by its generic self there’s a problem - all you know is that the job went onto the queue - so there is no way to find out that it printed or whether there is a problem. So you need some sort of a backchannel to get that information back. But how much error information do you send back? All of it? There could be a lot. And the more you share the more likely it becomes that you’ll run into versioning problems over time for those who actually use it. And if you only send some of it, how is that configured? Centrally for all users? Individually for each request, i.e. “if there is an error then send me this information”. Again the solution is to keep errors as simple as possible and to add diagnostic capabilities (e.g. another queue) for those who actually need it.

The problem with harmonized types (in this case the error that is returned) is that complexity is inflicted unnecessarily on the simple cases. That is an acceptable tradeoff if only 5-15% of your cases are simplistic while the remainder is inherently complex. But when 85% of your cases are simplistic you just have to admit that you are dealing with two separate things and only inflict the complexity on the cases that actually need it.

So while it may make sense for Ecto.Changeset to offer structured error information that is so complex that you may need to use Ecto.Changeset.traverse_errors/2 to go through it all, that doesn’t automatically imply that a client of a context capability needs that same level of detail.

Even in the case of UI validation a simple “UI field”/“Field Error” key-value structure is probably enough - functional programming is all about data transformation - so transforming the error information to specifications of the context’s client before it passes through the boundary shouldn’t be an issue. And if it’s about function pipelines - what if stages produce similar looking errors but for some reason we need to know which particular stage produced the error. I guess we’ll have to write wrapper functions that add stage specific tags to the error tuple’s reason information.

So:

  • Contexts have boundaries.
  • Boundaries are supposed to (sometimes reverse but often) minimize dependencies. Dependencies are typically minimized via loose coupling.

Loose coupling leads to a specific type of relationship within and outside of the context boundary, with emphasis on reducing dependencies between the context boundary, the context implementation, and the context’s clients.

Loose coupling promotes the independent design and evolution of the context logic and implementation while still providing interoperability with its clients that have come to rely on the context capabilities. There are numerous types of coupling involved in the design of a context, each of which can impact the content and granularity of its boundary. Achieving the appropriate level of coupling requires that practical considerations be balanced against various context design preferences.

(Text liberally “repurposed and rephrased” from here).

I’m not exactly sure how this particular talk has managed to fly under the radar:

ElixirConf 2016 - Selling Food With Elixir by Chris Bell

Does this talk mention contexts? Not exactly. But for me it manages to check some of the boxes on the road towards contexts.

  • De-emphasizes the Rails way of doing things
  • De-emphasizes the database as the core of the application, relegating it somewhat to data backup and bootstrapping support.
  • Breaks the application down into distinct areas of domain concerns:
  • Store Availability
  • Order Scheduler
  • Order Tracker

Are those “areas of domain concerns” contexts? Hard to tell because there isn’t enough detail to judge the level of autonomy and decoupling that these “areas” have. And more to the point - for the time being the product has succeeded with the level of separation that it is currently implementing. Meanwhile some boundaries have already been defined - future development may discover that the boundaries have to be shifted or that they need to become more decoupled (fyi, the current version is running on a single node).

5 Likes