Jump to content

HAL: Do you ever put implementation code in abstract methods?


Recommended Posts

I have a fundamental question on HALs and abstract methods that I'd like clarified, please. In my HAL's up to this point I've been using a parent with abstract methods, implementing concrete methods in the concrete product(s) (see here for example), and using dynamic dispatch as appropriate to execute the concrete method. I think this is a standard technique and it works fine for me.

 

Now I have another layer of abstraction I want to implement, but in this case the concrete products share a lot of functionality and often I can get away with just executing the same method for each product. Should I just go ahead and implement this logic in the parent method? If I do, that means I no longer have abstract methods in my parent (right?), so am I breaking some 'good-practice' rules in OOP design? 

 

Or am I just getting bogged down in semantics? 

 

[by the way, I have loads of little niggling questions like these which I may ask as I'm going along in my OOP learning - hope you don't mind.] 

Link to post

You aren't really "breaking" any good-practice rules in OOP design. There is nothing inherently wrong with have common functionality in an abstract parent - as long as you are comfortable that this will always reasonably be the case for all child implementations. If you come up against scenarios where children are over-riding methods just to inhibit parent behavior then you are definitely seeing a code smell - your hierarchy is making assumptions about behavior that are not true for all children.

 

One of the most important guidelines for OOP design is to favor composition over inheritance for exactly this reason - inheritance enforces behavior that isn't always appropriate. The guideline encourages small, focused hierarchies of classes and then using composition where needed to add functionality without impacting the hierarchy.

 

Unfortunately OOP development in LabVIEW is cumbersome compared to other languages, so we can all be forgiven for taking a short-cut where it is simply more expedient.

 

EDIT: If you are new to OOP development, I encourage you to read through these if you haven't already:

Edited by ak_nz
  • Like 1
Link to post

I would say putting common logic in the parent is desirable behavior.   And I don’t mind if some children will want to override the default parent behavior.  For example, I have a project at the moment that uses cameras and “lockinâ€, the extraction of a periodic signal in the image.  Most concrete camera types just override the "Get Image" method, and the parent’s "Get Lockin Image†method uses "Get Imageâ€.  But  one camera model has a built-in lockin mode, so I override “Get Lockin Image†in that case.

  • Like 1
Link to post

Thanks for the replies, guys. (Thanks also for the links, ak_nz). That (mostly) clarifies things for me. ak_nz, I won't have a problem with children restricting the parent functionality if I do override the method at some point - I guess you're talking about LSP, right?

 

One final question on this: once I put code in my abstract parent method, is it technically no longer called "abstract"? I can see that it doesn't matter functionally, but I just want to get my terminology right at this early stage. 

Link to post

Thanks for the replies, guys. (Thanks also for the links, ak_nz). That (mostly) clarifies things for me. ak_nz, I won't have a problem with children restricting the parent functionality if I do override the method at some point - I guess you're talking about LSP, right?

 

One final question on this: once I put code in my abstract parent method, is it technically no longer called "abstract"? I can see that it doesn't matter functionally, but I just want to get my terminology right at this early stage. 

 

You can have an abstract class that implements some concrete logic. Since it is functionally irrelevant from a compiler perspective the important thing is to communicate the intent of the class to other developers. One technique I use in this scenario is to alter the class icon to a wire-frame cube (rather than a full cube) using GDS which indicates that the class is not intended to be instantiated but exists only to specify a common interface for concrete children and implement some common behavior. 

  • Like 1
Link to post

As ak_nz and drjdpowell have both said, it is perfectly fine to implement concrete code in an abstract class. This lets you make full use of inheritance to keep your project tidy and organized.

 

 

If you come up against scenarios where children are over-riding methods just to inhibit parent behavior then you are definitely seeing a code smell - your hierarchy is making assumptions about behavior that are not true for all children.

I would say putting common logic in the parent is desirable behavior.   And I don’t mind if some children will want to override the default parent behavior.

 

"Rules" and "best practices" are established to guide you down a tried-and-true path, to maximize your chances of succeeding the first time you try. However, a set of rules will never cover all possibilities so simply following rules will not guarantee you success.

 

As the software designer/architect, it is your job to decide whether or not it makes sense to follow a rule in a particular part of your project. Referring to the two quotes above, I'd say: "It's ok to inhibit parent behaviour if 2 out of 20 children need to do it, but not if 18 out of 20 need to do it. If 10 out of 20 need to do it, well... make a judgement call."

 

 

once I put code in my abstract parent method, is it technically no longer called "abstract"? I can see that it doesn't matter functionally, but I just want to get my terminology right at this early stage. 

 

I'll answer your question in multiple levels.

 

Level 1: The definition of "Abstract"

In OOP, an abstract method is defined as a method which has no implementation. Its purpose is to define an interface for children to follow, and it is the children's job to provide the implementation.

 

An abstract class is a class that contains one or more abstract methods. An abstract class can contain concrete methods too.

 

So yes, if you put code in your parent method, then the parent method is concrete, not abstract. However, this in itself doesn't make the parent class non-abstract.

 

 

Level 2: The (lack of) necessity of "Abstract"

From your original question, I gather that you're wondering if you need to keep your parent "abstract" in order to abide by some set of best practices, and your latest post suggests that you're no longer worried about this.

 

To reaffirm: Many examples of inheritance show you an abstract common ancestor implemented by multiple children. However, there are no rules that say you must have an abstract common ancestor. You are free to have a fully concrete (and usable) class as your common ancestor if it makes sense to do so.

 

 

Level 3: The (lack of the) concept of "Abstract" in LabVIEW

Since you're interested in formal definitions and terminologies, I'll throw this in: LabVIEW does not support abstract methods in the traditional sense (yet?).

 

As I said in Level 1, an abstract method has no implementation by definition. However, what people commonly refer to as "abstract VI"s do in fact have an implementation: This implementation simply does nothing. "Having no implementation" is not the same as "Having an implementation that does nothing". LabVIEW doesn't let you create a VI with no implementation.

 

In other languages, your program cannot be compiled if you inherit from an abstract class but don't provide implementations for all of its abstract methods. This can be a useful way to remind developers to implement them. LabVIEW doesn't do this -- a LabVIEW program will happily run even if you call an "abstract VI". To work around this, what I did in a recent project was to implement pop-up dialogs in my "abstract VIs". This way, I'll get a message at runtime if I forget to implement something in a new child class a few months down the track.

 

EDIT: LabVIEW programmers fulfil the purpose of abstract functions by doing 2 things: (i) Have a VI that does nothing, and/or (ii) select the "Require descendent classes to override this dynamic dispatch VI" option in the class properties dialog.

 

See this post for more: http://stackoverflow.com/questions/391483/what-is-the-difference-between-an-abstract-function-and-a-virtual-function (Note that NI explicitly rejected the term "virtual" because it's confusing to talk about non-virtual-Virtual-Instruments, so they picked "dynamic dispatch" instead)

 

At the end of the day, knowing terminologies is useful (and important!) when discussing concepts, but it shouldn't affect how you design your project.

 

All the best!

Edited by JKSH
  • Like 1
Link to post

 To work around this, what I did in a recent project was to implement pop-up dialogs in my "abstract VIs". This way, I'll get a message at runtime if I forget to implement something in a new child class a few months down the track.

 

I've found this to be critical, even for functions where I require overrides. There are situations (for example if you are debugging and add a conditional disable but forget to wire things through, or if you do something similar with a for loop that runs 0 times) where a class wire can get invalidated. If the wire type is that of the parent class (common), you can still get an instance of that parent class and you will never know whats going on unless you throw an error. Its pretty horrifying to realize you've spent half a day debugging a billion reentrant VIs on cRIO targets just because you dropped down one disable structure and forgot to wire through the enabled case.

 

tl;dr: throw errors in parent functions which shouldn't be called. Its a good idea.

Link to post

As ak_nz and drjdpowell have both said, it is perfectly fine to implement concrete code in an abstract class. This lets you make full use of inheritance to keep your project tidy and organized.

 

 

 

"Rules" and "best practices" are established to guide you down a tried-and-true path, to maximize your chances of succeeding the first time you try. However, a set of rules will never cover all possibilities so simply following rules will not guarantee you success.

 

As the software designer/architect, it is your job to decide whether or not it makes sense to follow a rule in a particular part of your project. Referring to the two quotes above, I'd say: "It's ok to inhibit parent behaviour if 2 out of 20 children need to do it, but not if 18 out of 20 need to do it. If 10 out of 20 need to do it, well... make a judgement call."

 

Yes, this is an interesting design problem that inevitably ends with a different answer for each developer - what percentage deviation from LSP are you comfortable with? It is a slippery slope - so be careful. And document the limitations so that the next developer to follow you understands the trade-offs you made - they might follow your example in a way you didn't expect. As the quote goes:

 

“Code as if the next guy to maintain your code is a homicidal maniac who knows where you live.â€
 

 

 

tl;dr: throw errors in parent functions which shouldn't be called. Its a good idea.

 

I do this for hierarchies where a child class doesn't support a particular function - the default parent behavior is to generate an "Unsupported Operation" error. Still I personally only do this if I think the number of operations or number of classes in the hierarchy won't really increase (ie. percentage deviation from LSP is small). If I find that I have multiple levels of undo in multiple operations in a hierarchy I wonder how maintainable this codebase will be by the worst critic - another independent developer.

Edited by ak_nz
Link to post

Thank for the further insights, in particular JKSH for your post. I think your link to stackoverflow.com summarizes it for me as regards my OP:

 

An abstract function can have no functionality. You're basically saying, any child class MUST give their own version of this method, however it's too general to even try to implement in the parent class.

A virtual function, is basically saying look, here's the functionality that may or may not be good enough for the child class. So if it is good enough, use this method, if not, then override me, and provide your own functionality.

 

I'm a little bit wary about the comments regarding children inhibiting parent behavior, as I thought that definitely was bad practice. But I haven't come across any situation where I need to do that so I won't worry about that for now anyway. Thanks also for the insights in throwing exceptions when a method won't be over-ridden. That's something I was thinking about, so you've saved me a question. Cheers, all.

Link to post

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.

  • Similar Content

    • By Ryan Vallieu
      I have seemingly found an issue with the shipping example code for Nested Malleable VIs.  Another user has verified that he saw the same behavior in 2019.
       
      I am working through the examples and the presentation from NIWeek 2019.  In running the Lesson 2b code (C:\Program Files (x86)\National Instruments\LabVIEW 2019\examples\Malleable VIs\Nested Malleable VIs) I found the Equals.vi in the class was not being leveraged and the search failed.  When I went to my LabVIEW 2018 machine and ran the Lesson 2b.vi the code worked to find the element by correctly leveraging the in-class Equals.vi.
      One difference I see is that in the 2018 example the Equal.vi is in the example folder with the code, and in 2019 the Equal.vi has been moved to VI.lib - otherwise the code looks to be the same.  The Equals.vi code looks identical, and the calling VIM look identical.  I posted on the LabVIEW NI.com forum here: 
      https://forums.ni.com/t5/LabVIEW/LabVIEW-2019-Malleable-VIs-Shipping-Examples-Lesson-2b-Nested/m-p/3966044/highlight/false#M1129678
       
      I am trying to determine what may have broken or changed between the implementation in 2018 and 2019, visually the code looks the same.
    • By Voklaif
      Hello all,
      I am programming with LabVIEW for around 2 years and was recently stumbled upon LVOOP.
      I am required to write a communication protocol to work with a micro-controller, which later will be also used for ATP and debug purposes.
      I want to build the program "correctly" from the beginning so it will be maintainable and flexible to additions and changes.
      My natural way of building a program would have been a queued state machine, with several loops, each loop is in charge of a different module (one for GUI obviously), but as I stated in the beginning, I want to use LVOOP.
      Does anyone have a LVOOP project I can use as reference? I've searched online and found some nice examples, but they are small and teach you the basic stuff.
      For me it's important to see the how to use the project tree wisely, where to place the classes, see the managing loop and to learn as much as possible before I create one of my own.
      Thanks in advance,
      Voklaif
    • By GregFreeman
      I have an array of classes, let's call the object TestPass, of size 1 (but it is an array because it can scale out to multiple test passes). In this class, there is one other nested class which is not too complex, then various numeric and string fields to hold some private data. There is also an array of clusters. In this cluster there is a string, two XY pair clusters, and an integer. Not very confusing.
      This array of clusters gets fairly large, however, upwards of 80-100k elements. What I am finding is when I index the array of pass classes it is crazy slow. On the order of 30 ms. Doesn't seem like much, but we are indexing the array in our method to "Get Current Pass" which is used in various places throughout our code. This is adding potentially hours to our test time over the 80k devices we are testing. 
      So, I started digging. When I flatten the class to a string and get the length, it's 3 mb. But, when I run the function with the profiler is is allocating close to 20 mb of memory!
      My gut feel was that the string is causing the issues. So I removed the string from the cluster and the index time went to 0 ms. 
      Luckily we can normalize a bit and pull the strings out of the cluster since a lot of them are duplicates. But it makes our data model a bit uglier. 
      Has anyone seen these kind of performance issues before? I saw them in 2013 and 2017.
    • By ted Francis
      I am new to LVOOP and have jsut started writing my first LVOOP program which I have attached.
      I would appreciate greatly help with the question I have
      Thank you in advance 
      Ted
      This vi will perform two tasks 
      1.Generating Report data sheet for metrology 
      2. updating the scales in a MAX .nce file
      1. Metrology will input calibration information into the tables on the tabs
      Metrology will then click "Update Tables" then "Create Report ( create report section of code is not yet written
      Update Tables will write all information entered in the tabs to class varaibles and will also delete current Max informatiomn
      2. Metrology will click "Load NCE Scale"
      vi will prompt for nce file to load and then once file is selected, display existing scales for two channels (Current Motor 1 and 
      Current Motor 2)
      Metrology will then click "Update Scales"  the program will replace the existing scales with those entered in Step 5.14 and 5.15
      from the tables on the tab
      Question 1.  Steps 5.14 and 5.15 are needed by both classes ( Table Variable and MAX) - what is the best way to share this information
       
      CAT0000032 Class Version.zip
    • By shoneill
      I was browing through the actor framework discussions on the NI site yesterday and I came across a statement by AQ.
      Never inherit a concrete class from another concrete class
      I had to think about that for a second.  The more I think about it, the more I realise that all of the LVOOP software I have been writing more or less adheres to this idea.  But I had never seen it stated so succinctly before.  Now that may just be down to me being a bit slow and all, but in the muddy and murky world of "correct" in OOP-land, this seems to be a pretty good rult to hold on to.
      Are there others which can help wannabe plebs like me grasp the correct notions a bit better?  How about only ever calling concrete methods from within the owning class, never from without?  I'm learning for a long time now, but somehow, my expectations of LVOOP and the reality always seem a little disconnected.  AQs statement above helped crystallise out some things which, up to that point, had been a bit nebulous in my mind.  Well, I say I'm learning..... I'm certainly using my brain to investigate the subject, whether or not I'm actually LEARNING is a matter for discussion... The older I get, the less sure I am that I've actually properly grasped something.  The old grey cells just seem to get more sceptical with time.  Maybe that in itself is learning...
×
×
  • Create New...

Important Information

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