5

Overview

I am tasked with designing a system that serves as an Interface between a User and one or more microcontrollers in different Variants.

enter image description here

As an example, our Microcontrollers Type 1 are milk frothers. They heat milk to a preset temperature, also they spin a little circular spring to make the milk foamy with varying speed. The speed is set by the user. Microcontroller variant A expects the parameter in terms of motor RPM as a 16-bit uint, variant B in terms of angular velocity as double precision float. Both report back the current milk temperature while frothing to my system. Sometimes I want to forward this info to another Device, say an LCD.

The Users requests from my System to frothe the milk at a certain speed, then expects a response back with some infos once everything is done. In practise, they send jsons to request a specific function with a given set of parameters, and expect a response json with another given set of parameters afterwards.

My Architecture

I decided to split the task into a layered architecture:

enter image description here

Starting from the right, here's what the individual layers do:

The Driver layer provides the low level drivers to interface the Microcontrollers and are vendor-supplied.

The VirtualTarget layer models the actual microcontroller variant and translates the requests made by the preceding layers to what the microcontroller actually expects. In our example, we would have three different implementations for this: Microcontroller B and the two variants of Type A.

The TargetAtomicFunction layer breaks down the user requests into the smallest individual functions the microcontrollers provide, e.g. MilkHeater, SpringPropeller and TemperatureReader. This layer does not know about any variants.

The UserFunction models the User story as described above and breaks it up into a sequence of TargetAtomicFunction blocks, e.g. MilkFrother

The UserInterface translates the json requests to binary data and forwards it to the appropriate UserFunction.

My Problem

I am not sure how to handle request/response parameters. The request parameters need to propagate the entire system and are mostly needed at the VirtualTarget Layer (but not all!). This is also where most response parameter values are generated (but again not all!) and they need to propagate back again.

But at the same time there is no reason for the UserFunction and TargetAtomicFunction layers to know whether the speed parameter has RPM or angular velocity as its unit.

What I came up with is a Request/Response parameter buffer that is accessed by all layers. When a new request arrives at the UserInterface level, the buffer is created with all request and response parameter fields. Then, each layer can read the request parameters it needs to operate, and post all response parameters it delivers. Then after everything is done and the response is returned, the Buffer is emptied. A warning is created if a parameter is read/posted that does not exist, or if some parameters have never been read or posted when the buffer is flushed.

As the actual parameters are known at compile time, I feel like a parameter mismatch would be reasonably easy detectable during integration testing. Yet it feels wrong to have such a component that needs to be known by the entire system.

enter image description here

Constraints

There are some constraints that might be important here:

  • I have little control over the user interface (json <-> C struct) and the request/response parameters & their datatypes
  • the System runs on a rather obscure embedded system and I have limited influence on the toolchain. Whatever solution I pursue, it will have to be implemented in plain C. No databases, no xml/json parsing,...
  • I need to compile a system configuration for Variant A and another for Variant B, it is not possible to unify them into one single configuration. Reasons are out of scope of this question (and my influence).
  • The user knows about the Variants! If he uses Variant A, he will pass the speed as RPM uint, for Variant B he passes a double with angular velocity.
  • No concurrency. There won't be another user request as long as the first one has not finished/timed out.

My Question

Given the application as described, is my proposed solution sound? Or are there better ways to handle the parameters propagating across layers?

2
  • Some sanity checks: why does the RRBuffer have to be such a generic class - why not simply use a struct for each variant which contains explicitly attributes like RPM or an angular velocity? And why is request and response mixed up in one data structure, why are they not separated?
    – Doc Brown
    Jan 12, 2023 at 11:59
  • @Doc Brown, per your first question: Under the hood, it's practically nothing more than that. But I want some access control, so that problems can be detected easier, i.e. a request parameter that was passed but never used. Per your second: The RequestResponseBuffer class might actually be two classes - I drew them as one for sake of simplicity. Just like the individual layers are not just a single class, but rather a collection of several components. Jan 12, 2023 at 12:28

2 Answers 2

6

I think the design smell is here that you are planning to use a huge global object (or component) with mutable state. That is an invitation for all kind of errors and can cause maintenance headache.

Instead, I would recommend you pass the request data explicitly from one layer to the next, using local values or immutable data structures. Of course, you can group them together to one or more general input data structure, and pass pointers to avoid copying overhead. But things initialized in the UserInterface must not be changed during their way down through the other layers.

For the response data, I would go the same route. A response object (or record) created in the VirtualTarget layer might be propagated in immutable form back to the outer layers. If you think the outer layers must add some information to a response, pass a modified copy of the response, or chain the different response parts together.

As for your programming language, C should be perfectly sufficient here. If you use simple structs, you may simply treat them as immutable by convention. If you need a complex data structure, implement them as a C module with functions for constructing a parameter record, for copying and freeing it, and for accessing it in a read-only manner.

2
  • Thanks for your answer. So let's say if I modified the RequestResponseBuffer in my design, so that one buffer is initially instantiated by UserInterface layer and then passed through the layer with read-only access to the parameters, and a different buffer that is instantiated by VirtualTarget layer that allows appending, but not modifying response parameters passed back through the layers to the user interface - would that be in the spirit of your last paragraph? Jan 16, 2023 at 14:38
  • 1
    @UnbescholtenerBuerger: yes, I think so. Components which are known to many parts of your system are a lot easier to reason about when they stay immutable.
    – Doc Brown
    Jan 16, 2023 at 15:16
2

There are some cases where you might want to pass around a global "context" object, for example if you are implementing a pipeline. In this pattern, different software modules represent different steps in a chain of ordered events. Each module, in its turn, has a chance to read and write to the context, which in your case would include the request and response to the end user. This pattern works well to support a sequence.

Your design is not a sequence. It is a solution stack. The modules don't execute one after the other. Rather, each layer is an abstraction for the benefit of the layer above it. This is a common and excellent pattern for separation of concerns.

In this sort of arrangement, each layer must provide its own interface to the one above it, independently of any of the other layers, and each layer must consume the interface of the one below it, which is also independent. This means copying, or mapping, the inputs of one layer to the next, and then mapping the outputs on the way back up. Sometimes it seems a bit tedious to code-- it easier just to pass along one big object that everyone can access-- but it has to be done.

If instead you decide it is "easier" to share global state, you will will find that certain problems come up more, e.g. code fragility, design rigidity, difficulty unit testing, bugs regarding sequential coupling or invalid states, branch conflicts in source control, a larger QA "matrix of pain," etc. In the end, it is hardly "easier." It is harder. Don't do that unless you have to.

12
  • Thanks for your answer. I try to wrap my head around how your suggestion could be implemented. It sounds like instead of just knowing a robust global object - the RequestResponseBuffer (I think it is robust because it has no dependencies and just operates on a well-defined set of primitive types) - now each layer has to know something about all the underlying ones (not just the one immediately below). Jan 16, 2023 at 15:15
  • Also, each UserInterface class needs to know the exact layout of request parameters of each layer component below it instead of stupidly mapping the incoming json to the requestresponse buffer. Is this really an improvement? Or am I understanding you wrong? Jan 16, 2023 at 15:15
  • Interesting interpretation. To me it is very obvious that a layer knows only about the layer immediately below it. Why do you think it would have to know about all the underlying ones too?
    – John Wu
    Jan 16, 2023 at 18:34
  • Probably I'm just dense and missing the obvious here... but the only way I could imagine implementing your answer without each layer knowing everything below itself would be to have the input parameters wrapped like an onion by the first layer (which still needs to know all underlying specifics), then pass it down the chain. Each layer consumes his parameterset and passes the unwrapped rest on to the next one. This causes me a lot more immediate headaches than my own solution. Jan 16, 2023 at 21:01
  • "To me it is very obvious that a layer knows only about the layer immediately below it." - em, no. There are layered architectures which are designed as you describe, and there are layererd architectures where layers just form a hierarchy, and access can be done from any higher level to any lower level. Both are well-know architectural options, none of them is "more correct" or "the only real kind of layered design".
    – Doc Brown
    Jan 16, 2023 at 22:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.