06-06-2013 06:12 AM - edited 06-06-2013 06:34 AM
Ah! I actually might have a solution:
It looks like I can use a polymorphic VI in a parent class to simulate an abstract VI. That is, I'll create polymorphic VIs for GetTemperature, GetPressure, etc, and register implementing vis (which have to be static and uniquely named) from the child classes. The object wired to the selector pane picks the correct instance of the polymorphic vi, and wiring an unimplemented object to the selector pane breaks the vi since there is no matching instance.
I suppose with this I can't do anything that requires casting--so no looping over an array of different temperature sensors--but I'd prefer compile-time error handling over looping.
EDIT: Actually, some languages (Java, I believe) have a set of methods marked as iterable. I think I can make a parallel tree of dynamic dispatch vis called GetTemperatureIter.vi that would wrap the static implementations.
It's all a bit of a kludge and it's more work for me, but if I'm not missing something, this should fulfill the requirements for the users. I'll still miss some of the bookkeeping stuff like require override.
06-06-2013 07:17 AM
Ah: discovered a trick!
It looks like I can make an abstract method by including a dummy polymorphic VI in the abstract VI, where the polymorphic VI only has an implementation involving the abstract class itself.
I have a class called Sensor. All of my devices will be children of this class; Sensor will never be used directly. Sensor has a VI called GetTemperature.vi. There's no functionality in this VI; it's just there to be overridden. If anything ever tries to call Sensor's GetTemperature.vi it means the actual device doesn't do temperatures, and I want that call to break at compile time.
So, I created a polymorphic VI called _dummy.vi and a single instance called _dummyinstance.vi, which has as input a Sensor. In GetTemperature.vi (and GetPressure.vi, etc) I wire the dynamic dispatch (object) input to _dummy.vi's input. LabVIEW's happy with this.
Now if GetTemperature.vi is invoked on a class that didn't override GetTemperature.vi, the class won't have an implementation in the polymorphic, and LabVIEW will reject the attempt at compile time.
Unless I'm missing something, this is good enough for me.
06-06-2013 07:39 AM
Ah: I was wrong! 🙂
Nevermind. I tested that trick with the wrong class. It doesn't actually work when I sat down to actually do it with the real devices since (obviously) the thing inside the Sensor vi will always actually be seen as a Sensor no matter where the child class came from.
06-06-2013 08:44 AM
I don't know if this trick will work.
1. Create Device parent class
2. Create children from this parent class for the different class
3. Create a temperature class with Device object as private data
4. When you instantiate the temperature class insert the correct children into the Device object
5. Get Temperatur on the Temperature class invokes the Device Get temperature method.
06-06-2013 08:50 AM
@crcarlin wrote:
There are a few drawbacks to that, Intaris.
Firstly, I disagree that the requirement is silly. To me it's natural, reasonable, and intuitive to insist on one object per device, both for user reasons and for OO design principles reasons, so any additional hoops end up seeming silly to me... which is how we got here.
But you still have only one object per device. The single object contains other objects which represent WHAT the object can do. These have more than one object, but the device itself has a single object.
@crcarlin wrote:
Secondly, when you unpack the individual behaviors from the instrument with your one extra vi, suddenly you're faced with more than one wire representing the object. Sure, we can have references inside the behavior so that there's really only one device object, but now we've (1) broken the encapsulation because the user needs to know the implementation is protecting him, and (2) thrown a curve ball to good programming because my hammering in of a "Don't branch wires!" mantra will seem to have a giant exception.
Your encapsulation is broken only to the extent of "This device can measure Temperature" which is already something the user should know if he's planning on calling the "Get Temperature" VI. This is not exposing the implementation, ti's exposing the iterfaces, the functionality of the device which is NOT against OO principles. I refuse to see this as breaking encapsulation.
@crcarlin wrote:
So yeah, I find this approach of creating sub-objects based on bits of behavior instead of is-a relationships to be the silly requirement, and it's a requirement being imposed by LabVIEW instead of any notion of good design or user want.
Try it. It's more scaleable and better to maintain than multiple inheritance.
@crcarlin wrote:
There's no need for explicit casting at all for this. Whether it's done through inheritance or interfaces, everything can be clear at compile time through normal most-recently-overridden and most-overlap patterns: add a temp-pressure meter to an array of pressure-voltage meters and the output is an array of pressure meters. Try to call GetVoltage.vi on that array's members and get a compiletime error since a member of the array doesn't implement that, which caused it to be an array of only pressure meters in the first place. Natural, easy, and consistent.
Well, that kind of assumes a multiple-inheritance or interface solution. (By the way, I don't buy the "quickly lead to name collision argument. I've read it; I get it; I don't think it's a big deal to decree that one may not use two interfaces/parents that overlap in a name. We're used to such restriction elsewhere in LV.)
Another solution, sort of a compromise one, woud be to create real abstract methods: allow a vi to exist broken in the inheritance tree so long as there aren't any "call parent" calls. I'm not sure this would even have to be explicitly marked as abstract. Then I'd just have one Meter class full of broken VIs, with children overriding with functional ones.
Well that's certainly a valid input but things being as they are, name conflicts are a REAL problem if you start thinking of going down the multiple inheritance route. The multiple parents could never share a common ancestor which makes using common logging classes or actor work really really cumbersome. Trust me, it's a solution which gets out of hand really quickly and will grind progress to a stop, resulting in incredible amounts of duplicated code once complexity ramps up.
OTOH, do I believe the LV implementation of OOP is perfect? No way. There are lots of things I'd like to see changed. But your adamant claim that allowing for multiple wires exposing aspects of a devices functionality to be against "OOP design principles" doesn't gel with me. Which principles exactly does it compromise?
06-06-2013 09:25 AM - edited 06-06-2013 09:29 AM
Intaris, one of the funny things is that we apparently each claim our proposal to be the one with a single wire while claiming the other leads to branches.
One thing I can clarify is this: my users very honestly don't always know the capabilities of the device they're looking to use. There's nothing I can do about that. The users have neither time or interest to learn the devices inside and out or to learn LVOOP to any depth (that's why they hire folks like me!). They WILL attempt GetTemperature on a thing that doesn't read temperature, so the question is how to deal with that with a minimum of (quite literal) explosions.
By using objects I can solve a lot of these issues and protect the users from themselves. I want to entirely hide the implementation; I don't want them to know anything about references, or whether there's something sitting in private data, or anything like that.
If at any point there are two objects pointing to the same device--as there would be if there were classes for each capability with a reference in their private data--then suddenly the user DOES need to know how that multiplexing is handled inside my objects. That's no good; the black box is opened.
An object on the block diagram that can be attached to any method it implements or swapped out with anything else implementing the same method is what's needed here. Anything else and we get explosions and expensive downtime.
It's far preferable for LabVIEW to refuse to connect an object to a method it doesn't support--detecting at compiletime--because that means there won't be a surprise runtime error a week down the road when some user didn't know that meter doesn't do temperature. That shouldn't be a hard request: I know at compiletime precisely which methods the thing implements, and even LabVIEW knows, but there seems to be no way to get LabVIEW to use that foreknowledge to throw an error.
I still have a suspicion that using a polymorphic VI can provide the bridge I need. Maybe I'll try again tomorrow.
06-06-2013 02:06 PM - edited 06-06-2013 02:10 PM
I don't think I claimed your solution led to any wire branching. If I did it was in error. Either way I believe it's irrelevant to the real problem.
@crcarlin wrote:
It's far preferable for LabVIEW to refuse to connect an object to a method it doesn't support--detecting at compiletime--because that means there won't be a surprise runtime error a week down the road when some user didn't know that meter doesn't do temperature. That shouldn't be a hard request: I know at compiletime precisely which methods the thing implements, and even LabVIEW knows, but there seems to be no way to get LabVIEW to use that foreknowledge to throw an error.
An object which does not measure temperature should NOT HAVE a "Get Temperature" method. This is proper OOP design. Having dummy methods which may or may not work is code smell and is a sure sign of a bad OOP architecture. You cannot have a class with a broken method without breaking the whole class. Either a class is executable or not. Having PARTS of a class broken is something which doesn't make sense in LabVIEW. Your wish for a compile-time error based on this action is simply covering up for bad OOP design.
In the absence of interfaces and multiple inheritance, the only proper OOP method of implementing what you want is to create an aggregation object which will actually do what you want. There need be NO WIRE BRANCHES unless the customer wants to read Temperature and Pressure simultaneously. This simultaneous situation also happens to require wire branching in your solution also unless you want to muddy the water even more and introduce Read Pressure AND Temperature methods.
I'm not sure at this stage if you've ever tried the aggregation method. It gives you basically all of what you're looking for: Compile-time error checking, inability to call Methods which don't exist and actually gives the user MORE information about the CAPABILITIES of the device because if the device doesn't have a Temperature module, guess what, it can't read temperature. How simple and intuitive is that?
I have the feeling that you are so caught up in your feelings regarding multiple inheritance / interfaces / LAbVIEW problems that you're not actually paying attention to what people are trying to tell you. I certainly don't think Polymorphic VIs are going to give you what you want. Been there, done that. Doesn't work the way you think it will.
TRY making an aggregate object and post code so that we can see what it is you have objections with. Just create a dummy class and then we can continue the discussion on a less idealistic level.
06-06-2013 02:34 PM - edited 06-06-2013 02:57 PM
Keep in mind, Intaris, I've also been down these roads. I'm not just blowing wind here. Certainly I took time to go through every pattern mentioned at https://decibel.ni.com/content/docs/DOC-2875 (including aggregation) and found none of them workable, for one reason or another. I'll take a second look at aggregation, but.... been there done that.
As I said from the beginning, I'm willing to accept that LabVIEW has no solution to this. What my users are asking for is entirely reasonable and intuitive, but if it can't be done in LabVIEW then that's that. It's BECAUSE I agree with you so strongly about bad design of half-broken classes that I came here in the first place to see how other people are dealing with my circumstances.
Edit: Looked back and verified that yes, you were suggesting using a class method to pull a module that can be itself asked for the actual measurement, right? The users flatly reject that. They are to interface with one object that represents one instrument, not a handful of objects that often seem to split arbitrarily between parts of programming. I'm entirely on their side with this one for a number of practical (and, sure, ideological) reasons.
If you say it's the best LabVIEW can do, then again, that's fine, but it means the half-broken classes are the best solution LabVIEW has to this problem.
06-07-2013 01:21 AM
It's a lost case. You are unable to implement exactly what you want in LabVIEW for a number of practical reasons.
Try C#.
06-07-2013 02:48 AM
@crcarlin wrote:
The container class pattern prevents both the bookkeeping of inheritance and the functionality of dynamic dispatch, while the "share object between classes" pattern prevents dynamic dispatch, requires a lot more programming, and lets users commit what should be compile-time erros.
If this statement is true of the aggregation solutions you have tried than you're doing it wrong.
I use aggregates all the time and have absolutely no problem using dynamic dispatch. I don't see where one prohibits the other. It also requires no more programming, so I don't know where that claim is coming from either. If done properly it will also PREVENT run-time errors, instead imemdiately letting the user know that the function is not supported.
Again, my request is to show us soem code because my feeling is that you simply have not implemented a proper design here. None of these "problems" are inherent to LabVIEW. If you are finding these limitations to be real then you have created them yourself.