Jump to content

Keeping Interfaces Simple When Functionality Requirements Change?


Recommended Posts

Hi guys,

I thought I'd throw this out there to see if anyone has any opinions on a hypothetical (which cartoons my current situation).

 

Say I have some process, implementing some functionality, for example:

 

A signal generator which can generate triangles, sines, ramps and steps.

 

Now I want the process to also generate band limited white noise.

The problem is, the parameters to the generator don't accomodate the idea of a frequency cut-off.

 

The ways I can see of dealing with this are:

  1. Extend the interface to include a frequency cut-off parameter (redundant for 90% of messages.)
  2. Break down the interface and expose different sub-interfaces (pass complexity on to calling code...)

Any other ways?

 

Thanks in advance for your insights!

Link to comment

I would choose option 2, because I think it's misleading to make it possible to set meaningless properties on a class just to make it fit the interface. Is it that much additional complexity for the calling code? You know which specific child you have when you instantiate it, it's not that much complexity to set some class-specific properties at the same time. If you want to change it further down the line, yes, there's a bit of complexity in checking if that property is valid for that class, but let's say you're presenting this to the user - would you want the user to be able to change a property that didn't have any effect? So you'd need to do that check anyway, or confuse the user when they change that property and it doesn't do anything because that child class doesn't use it (which I'd consider a poor UI choice).

Link to comment

Hmmm, what if the UI doesn't expose that choice unless the user selects the White Noise option?

That's exactly the point I was trying to make, although perhaps I didn't explain it well. A proper user interface should display only the options that are relevant for that choice. In the case of a collection of generic generator classes, somewhere you'll need to check the exact type of the child class to determine the correct options. At that point, you've determined the correct child class, so you can cast the generic item to the specific child class and access any class-specific properties.

Link to comment

Personally?

 

Option 3: Define each signal generation method into a separate class, several of which (corresponding to each type of measurement the device supports) are contained in the parent.  Then move the UI (it can be a simple FP with controls on it) into the object in question and tell the appropriate object from the parent to place their UI in a subpanel of your choice (This can be a much simpler approach than it sounds).

 

That was each and every object only exposes the parameters which are important for it's operation and extensibility is much easier in future.

 

Shane

Link to comment

The ways I can see of dealing with this are:

  1. Extend the interface to include a frequency cut-off parameter (redundant for 90% of messages.)
  2. Break down the interface and expose different sub-interfaces (pass complexity on to calling code...)

Any other ways?

 

 

Search Liskov substitution principle (abbreviated LSP in OO circles). Your #1 choice above is a violation of LSP, and therefore probably not a good idea.

 

---

 

Using your case study of a signal generator configuration, let's consider our design dilemma centers around the ConPane of "Configure Signal Generator.vi", which is intended to be an abstract method of parent "Signal Generator.lvclass" marked as "Must Override" for implementation by all subclasses. As you point out, this method requires different arguments (input parameters) that are specific to the subclass.

 

This dilemma has at least 3 resolutions that are correct:

  • Use dynamic typing rather that static typing for input/output arguments. JSON is a good choice in 2014 that provides both strong typing and dynamic typing (as opposed to weak typing and static typing). XML is your second choice. INI is a suboptimal choice. Your Own Clever Protocol (YOCP) is a significantly suboptimal choice. LvVariant... exists as a choice (with editorial value judgement reserved at this time).
  • Use dependency injection (DI) and/or constructor injection (constructor is sometimes abbreviated ctor). But! If the constructed object you are injecting can have purely by-val semantics with no by-ref or active object requirements, this could be a code smell. Specifically, since it represents an "OO purist" idiom of circumventing dynamic typing, which represents far less syntax and bloat. (Plus, joke's still on you! You've just deferred and complicated the exact same problem, abstracting the problem of concrete input parameters to the Configuration class constructors rather than implementation methods. Said another way -- object configuration is probably not best solved with DI in LabVIEW.)
  • Acknowledge that "Configure Signal Generator.vi" is an inappropriate abstract method of the supertype, but should instead be reserved as concrete methods of subclasses that "look and feel like they ought to conform to an inheritance relationship but just don't because they don't really IRL and i accept this".

There are more incorrect resolutions. Here are some obvious ones where I have tried and failed:

  • Bloat the superclass with subtype-specific methods. Why is this wrong? This violates LSP, enables (encourages?) incorrect usage of subclasses, and throws run-time errors rather than compile-time errors.
  • Bloat ancestor methods with descendent-specific parameters (input and output). Why is this wrong? This violates LSP, enables (encourages?) incorrect usage of subclasses, and throws run-time errors rather than compile-time errors.
  • Inject Signal Generator Configuration.lvclass into the Signal Generator.lvclass "ctor" method (LabVIEW does not have canonical object constructors, hence air quotes around "ctor"). Why is this wrong? I was once excited to have "discovered" this solution, and gleefully coding the Configuration classes, only to realize, on completion of this dual hierarchy of redundancy, the exact same problem existed with the ctors of the Configuration classes.
  • Use #1 or #2 "correct" solution above, when I should have chosen #3. After doing this many times, I realized eventually LVOOP might not contain sufficiently-expressive constructs to represent object models of my application domains. Mixins (or traits), available in modern OO languages, might be a solution, and a construct we could consider rooting for in LVOOP.

YMMV! Hope this helps. All bolded terms are searchable.

  • Like 1
Link to comment

This dilemma has at least 3 resolutions that are correct:

  • Use dynamic typing rather that static typing for input/output arguments. JSON is a good choice in 2014 that provides both strong typing and dynamic typing (as opposed to weak typing and static typing). XML is your second choice. INI is a suboptimal choice. Your Own Clever Protocol (YOCP) is a significantly suboptimal choice. LvVariant... exists as a choice (with editorial value judgement reserved at this time).
  • Use dependency injection (DI) and/or constructor injection (constructor is sometimes abbreviated ctor). But! If the constructed object you are injecting can have purely by-val semantics with no by-ref or active object requirements, this could be a code smell. Specifically, since it represents an "OO purist" idiom of circumventing dynamic typing, which represents far less syntax and bloat. (Plus, joke's still on you! You've just deferred and complicated the exact same problem, abstracting the problem of concrete input parameters to the Configuration class constructors rather than implementation methods. Said another way -- object configuration is probably not best solved with DI in LabVIEW.)
  • Acknowledge that "Configure Signal Generator.vi" is an inappropriate abstract method of the supertype, but should instead be reserved as concrete methods of subclasses that "look and feel like they ought to conform to an inheritance relationship but just don't because they don't really IRL and i accept this".

I'll offer my 2c here.

 

  • As a general solution to the problem, point 1 is simply not going to cut the mustard.  As I understand it, part of the problem here is the presentation of the values on the UI and how to handle the different modes of user entered data.  Try telling a user to enter data via String/variant and you might as well just quit on the spot.  This also pushes a LOT of errors from compile time to run-time which is not good.
  • The option of dependency injection works absolutely fine IF you are using the objects own methods to interface with the stored configuration data.  By implementing a simple UI for each (type of) class, you can keep the UI interfaces for user data entry completely seperate from each other with zero JSON, XML or the like in sight (unless that's actually your underlying data of course :P ).  Granted the problem of programatically setting the values of the sub-objects remains but simply pushing the UI work into the objects themselves DOES significantly improve the ability to decouple the various input methods for users at least without creating any new burdens.
  • This is simply the same as hard-coding each and every subclass functionality into your code and has almost zero expandability in the future.

As a rule, you should be asking your objects to do things for you instead of telling them what to do.  As such, I find the implementation of the UI interface (for this specific example) into the subobjects themselves the most logical, most flexible and safest method.  Even the required knowledge of JSON formatting or Variants so that the object of choice will understand it is, for me, not a definition of an interface, it's broken encapsulation.  Wiring up a strictly-typed terminal is always better.  If and only if it is impossible to get what you want with strict typing then consider going down the dynamic typing road.

Edited by shoneill
  • Like 1
Link to comment

Option 3: Define each signal generation method into a separate class, several of which (corresponding to each type of measurement the device supports) are contained in the parent.  Then move the UI (it can be a simple FP with controls on it) into the object in question and tell the appropriate object from the parent to place their UI in a subpanel of your choice (This can be a much simpler approach than it sounds).

 

 

^^this is more or less what we've been doing with the configuration editor framework (http://www.ni.com/example/51881/en/) and its pretty effective -- CEF is a hierarchy, not containment, but the implementation is close enough. We've also found that for deployed systems (ie RT) we end up making 3 classes for every code module. The first class is the editor which contains all the UI related stuff which causes headaches if its anywhere near an RT system. The second is a config object responsible for ensuring the configuration is always a valid one as well as doing the to/from string, and the idea is that this class can be loaded into any target without heartache. The third is a runtime object which does whatever is necessary at runtime, but could cause headaches if its loaded up into memory on a windows machine. Using all three is kind of annoying (some boilerplate necessary) but theres a definite separation of responsibilities and for us its had a net positive effect.

 

The other thing I've done with the above is to make a single UI responsible for a whole family of classes, like the filtering case here. Basically I store the extra parameters as key value pairs, and the various classes altogether implement an extra three methods. One says "what kvpairs do you need to run correctly", one says "does this kvpair look OK to you" (validation), and the third says "OK heres the kvpair, run with it" (runtime interpretation). This has worked pretty well for optional parameters like timeouts and such, which you wouldn't want to make a whole new UI for but are still valuable to expose. Also this obviously only works well if you (a) know good default values and (b) have some mechanism for a user configuring the system so that the computer doesn't have to be smart.

Link to comment

Lots of fantastic info, thanks guys!

 

@Shoneill

 

How does your proposed solution deal with the potential situation where the process (Signal Generator) is running remotely from the control software (UI). Over TCP or whatever. I can't see a graceful way of passing a UI type object around. If the UI is a part of the object, then you have a static dependency between the control UI (which contains the subpanel) and the process code don't you?

Link to comment

I've made some things like this. Usually I've used a tab control with the common controls floating over and the custom controls on their designated pages. The tab change event would sometimes hide or re-caption certain common controls. If you want another function, I'd make another tab page fill out the captions and visible flags.

 

You need to send that over TCPIP? no prob, just send the function and a flattened cluster of the page contents or even send each individual control on value change. Not infinitely expandable or perfectly decoupled but it got the job done.

Link to comment

@AlexA,

 

no, there is no static link between the VI "hosting" the UI and the UI itself, that's the point.  The VI with the subpanel (not part of any of the classes but needing to display their UI) passes the subpanel on to the object and asks it to embed itself there (which it would then do).

 

How the rest is done depends a lot on your requirements.  If the call to the UI is blocking, then it can all be handled within the single VI which also embeds the FP in the subpanel with the entered values simply contained within the object returned from the VI call.  This really is the super simplest approach.  The VI to place the UI int he subpanel does so and waits for the stop button to be pressed after which it retrieves the current parameter set and updates the object accordingly.  You have a single VI call with an object input and output (along with a subpanel input) and the new values are simply part of the output object.

 

If you need an asynchronous approach, then your UI will most likely need to expose a user event where each changed set of object parameters gets sent back to the host until the UI is closed.  But again all communications are done on the API of the parent since we no longer have any need (for this functionality at least) to know which child we0re currently working with.  It's enough that the child knows what kind of object it is and acts accordingly).

 

The "Host" VI only ever sees a parent object since this is the interface we've programmed to.  There's absolutely no knowledge of how the UI looks except for the fact that at some stage it's embedded in the subpanel.  Each object has it's own UI handler.  There is no showing or hiding of controls, no dynamic parsing.  Each object operates in its own world and the UI can be appropriately designed and encapsulated for that specific object's requirements.

 

Shane.

 

PS The TCP part is a red herring, it makes no difference whatsoever.  How would you send the parameters anyway?  That's a problem for your object model but is not affected by the UI aspect at all.

Edited by shoneill
Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.