JKISoftware / JKI-State-Machine-Objects

Object-oriented framework for LabVIEW based on the JKI State Machine
BSD 3-Clause "New" or "Revised" License
96 stars 55 forks source link

[Discussion] about notation for composition #15

Closed francois-normandin closed 6 years ago

francois-normandin commented 8 years ago

Currently, the composition tree is flattened using the dot notation. This is a thread to discuss whether this is an optimal pattern for SMOs.

For example, an instrument of "Instrument.ModelA" class that depends on a "Tool.ModelB" object would create two objects with default names (Qualified Names):

Unique names for SMOs currently remove invalid characters, including dots, to allow dot notation to be used in the flattening of the objects composition lineage. Other option could be to use "/" for separators in the flattened lineage.

3esmit commented 8 years ago

From my research is the best framework avaliable in LabVIEW ecossystem, not that Actor is bad, is that LabVIEW LVOOP should not be used that way. I believe SMO is bringing to LabVIEW a set of good programming practices that make LabVIEW much more agile development. I think this discussion could be extended for other convetions LVOOP programming, that not only would apply to SMO, but to other types of classes and even for single VIs. We should also discuss when and why use lvlibs, that Is a thing that only caused troubles to me (so I avoid it).

Libraries

We should use lvlibs? The intersting feature of them is compiling libraries them and delivering classes and VIs without giving source code, so I see them comparable to 'labview DLLs'. So I would only use this if there is plans to provide compiled libraries to labview programs.

Class naming

SMO Editor, examples and templates use dot notation for aggregation in class notation and for template class specialization.

I'm working with 2 projects on SMO, and I'm following this; smoprojects

I will explain just one part to dont get too extensive Endurance.lvclass --- this is the main smo, enumaratesStaticDependencies with Endurance.DB (shared) Endurance.UI and others. Endurance.UI.lvclass -- this is the main ui, it dependes on other UIs that are embedded to subpanel. This UI's may have other nested SMOs, and also use shared resource Endurance.DB Endurance.DB.lvclass -- recieve insert,find,aggregate and other operations, responds as events (all SMOs recieve all requests, but each one listen for specific messages and may ignore some messages based of what is in response.

Why should I name a shared resouce EnduranceDB instead of Endurance.DB? When the . notation should be used in SMO process naming? When the system uses it automatically? Could you please provide an example that shows the 'automatic' naming tool of aggregation ? How should I name INSTANCES of aggregated and name INSTANCES of composited? How the CLASS naming should be done? How the aggregation and composition of instances affect class naming?

francois-normandin commented 8 years ago

Is Endurance.DB inheriting from Endurance class? If so, then this is a good naming convention. If Endurance.lvclass is the top-level app and it owns a database through composition, then your database class should not be named Endurance.DB, but simply DB... thus its qualified "path" in the composition tree would be Endurance.DB.

The name of a subsystem should not be mistaken with the class of the subsystem. For example, you could create a database class for a specific project that you would name (on disk) as DB.ProjectA, which extends the DB class. When your top-level app instantiates one of those objects, it should still create it with a generic name such as "DB", not "DB.projectA".

Think about the experience your user will have as if you want to access your app from a browser:

Let's assume your user connects to your Endurance application through http://localhost/Endurance. And let's assume your user wants to access the database subsystem, it would then connect as http://localhost/Endurance/DB. Under the hood, your application might be executing a DB.projectA class to service the request, but that is a detail that the user does not need or care to know about the innards of your app.

In this context, your class is of type "DB.ProjectA" but its given subsystem name is "DB". The top-level app (Endurance) is composed of 1 database named "DB", accessible from that name. If you were to try to expose this URL from a dynamically created composition, you would need to find a way to resolve the request at the URL specified. The composition/aggregation (lineage) tree is how you would do that.

That being said, to answer you questions specifically:

Why should I name a shared resouce EnduranceDB instead of Endurance.DB? When the . notation should be used in SMO process naming?

If I understand your app structure correctly, you should name your database class "DB.lvclass". It does not inherit from the Endurance class.

When the . notation should be used in SMO process naming?

You should use a dot notation in the class name when you extend a specific class, such as a more specific type of database inheriting from your base DB.lvclass. I would avoid perpetuating the SMO part of the name, because it is kind of obvious and redondant. (Don't name your class SMO.DB, unless you want to namespace it from another DB class...) All templates (ex: SMO.Basic) are inheriting from the SMO base class. This is why there is a dot in the class name. This is by no means a requirement, just a convention to tell the developer that this class inherits from another one. It could be called "Basic.lvclass" and work just the same.

When the system uses it automatically?

As of now, it uses it automatically only to report the qualified path (public method) and is built as part of the dependency launching process where the owner injects its path into the "owner attribute". This qualified path should give the user a way to extend the framework in such a way that it can be used to create configuration files, URLs, etc. that is self-consistent from one app to another (and therefore can be generic).

Could you please provide an example that shows the 'automatic' naming tool of aggregation ?

There is a basic example in the SMO folder (/vi.lib/JKI/JKI SMO/Test-SMObase.vi) This example shows how two pumps are created and added to a System object. You can see how the unique names are created for each pump and what are their qualified names (composition paths) once they are owned by the system object. Change the names of these objects (to add a dot) and see the effect on the name and qualified name.

Demonstrate Qualified Names.zip

How should I name INSTANCES of aggregated and name INSTANCES of composited? How the CLASS naming should be done?

First, the difference between composition and aggregation is only one of ownership (responsibility of the lifetime of the object, aka. which object is responsible for creating and destroying the reference). You should name them whatever you want, but in general keep the name simple and meaningful, ie its name should tell you about what it is, not about its relationship to others. Do not name an object based on what it will become once it is part of the whole. In essence, don't name a pump "projectA.Pump"... if it is owned by projectA, then "Pump" is its name no matter if you are creating it as part of a standalone test fixture, or as part of a larger application.

How the aggregation and composition of instances affect class naming?

It does not. The qualified name (flattened lineage) is affected by the class name, not the other way around.

3esmit commented 8 years ago

Class naming convention

Is Endurance.DB inheriting from Endurance class? If so, then this is a good naming convention. If Endurance.lvclass is the top-level app and it owns a database through composition, then your database class should not be named Endurance.DB, but simply DB... thus its qualified "path" in the composition tree would be Endurance.DB.

I learned this from tmplates, I see that all of them are specialization of SMO, none is of UI. This made me imagine I should name classes separating them by context - this is explains how I was naming the SMOs in my projects. image Following the convention detailed here, the templates should be named: SMO.UI_Basic SMO.UI_ByRef SMO.UI_Splash SMO.UI_Embed SMO.UI_Chart

I think the SMO part could be keept, and I think would be a good convetion to use in projects class named like SMO.EnduranceDB.lvclass, SMO.UI_ClimateSimulation.lvclass This is good as we are making explicit in class what it is about. SMO.EnduranceDB.lvclass -- Its a SMO that contain Endurance Test Database Logic SMO.UI_ClimateSimulationEditor.lvclass -- Its a SMO that contain ClimateSimulationEditor UI; So all relevant information can be read in class name.

Qualified Resources

There is one thing that was not clear: Qualified Names vs. 'Simple/Unique' Name. Myself I was ignoring the Qualified names, since they are not used in 'ListSharedResources'.

Unique names // Object Names

In my first perception, unique names would be unique in their level, just like a filesystem with folders and unique filenames per level, like a variable that just exists in a certain class scope.

Shared resources

I would imagine shared resources like 'static variables' to certain objects ~ as LVOOP is poor in OO, this is only possible when using together with DVR. Looking in 'java', we have the same by implementing a class with private constructor and a static method usually called 'getInstance()'

class SystemDB 
{
  private static SystemDB instance = null;

  //only created by itself
  private SystemDB()
  {
  }

  public static SystemDB getInstance()
  {
    if (SystemDB.instance == null) { 
      SystemDB.instance = new SystemDB(); 
    }
    return SystemDB.instance;
  }
}

Of course SMO works a lot different than this, but it actually gives a similar result for shared instances.

List This Level Dependencies

It's nice to bold that ListThisLevelDependencies.vi also load the SharedResources, this was not clear to me at first sight, i would imagine that it would only list the non shared resources, as the shared resources were started/owned by other level.

francois-normandin commented 8 years ago

Class Naming Convention

These SMO UIs inherit from SMO.UI class:

This one inherits from SMO base:

List This Level Dependencies

That method returns all the Static dependencies first, then all the dynamic ones. It does not discriminate between composition and aggregation. The goal is to maintain a list of everything that an object is responsible for. It allows for casting the strictly typed cluster of dependencies using the first elements of the array, and to return the rest as an array of generic SMO classes. It is used to send a Stop command to all dependencies during execution of onStop( ).

Unique Names

In my first perception, unique names would be unique in their level, just like a filesystem with folders and unique filenames per level, like a variable that just exists in a certain class scope.

That's an excellent point. I thought about that before but there is a complication as soon as you entertain the idea of changing the dependencies dynamically (i.e. reorganizing the dependency tree at runtime). It's not impossible to imagine we could do that in the future. We already have a GUID that is unique to each object, so the name would be akin to a "Displayed Name", with a different "Unique Name" per lineage level. I can see that this would be useful.

One name limitation that exists right now is that we cannot yet have this situation:

This limitation would go away by introducing this concept. I'll create a thread to keep track of this.

Qualified Name

I was ignoring the Qualified names, since they are not used in 'ListSharedResources'.

You should ignore the qualified name, unless you want to use it for configuration management, URL parsing, etc.

Shared Resources

as LVOOP is poor in OO, this is only possible when using together with DVR.

I wouldn't say that. LVOOP is OO for dataflow, not for pointers. The fact that we build our OO as both byValue and byRef is, IMO, a testament to the level of thoughtfulness that was put into it in the first place at the language level. It does not allow all the things that come naturally in other OO languages, but I don't think it is poor in that sense. Yes, interfaces and the such would be nice to support natively, but the OO implementation is sound and respect most of the conventions we find in other languages.

Creating accessors explicitely lets you control the abstraction leakage you allow. One of the weaknesses of SMOs is that the protected methods of the base class can be used on a dependency and there is not much we can do about that without adding a lot of overhead (latency) or praying the developer will not misuse the ability of the framework. In case it's not clear where I'm going, an example would be: I have an Instrument class that owns a Pump object. The instrument class is supposed to only be able to call into the pump public API... well, because of the use of DVRs and the fact that both the Instrument and the Pump are inheriting from SMO base, the Instrument can act on Pump using SMO.lvclass protected methods at any time as long as it is within its own method (part of the instrument class). Nothing prevents the Instrument from calling a onStart( ) method on its pump reference... When I think about this possibility, now that's a total hack, but it's still doable. Try doing this with actor Framework: it won't work. You are forced to use a message to enqueue to the core and this will be respecting the concepts of dataflow. In that sense, Actor Framework is a better framework. Don't get me wrong, I still prefer to use SMOs over Actor Framework (even though I use AF everyday too) because of a lot of other reasons, but there are differences, strenghts and weaknesses for all frameworks. Recognizing them is key in deciding which one to use in the right context.

3esmit commented 8 years ago

Thank you for the LVOOP deep abstractness clarification.

That method returns all the Static dependencies first, then all the dynamic ones. It does not discriminate between composition and aggregation. The goal is to maintain a list of everything that an object is responsible for. It allows for casting the strictly typed cluster of dependencies using the first elements of the array, and to return the rest as an array of generic SMO classes. It is used to send a Stop command to all dependencies during execution of onStop( ).

So it will return all dependencies, shared and non shared, even if the shared dependency was not started by this SMO (is not owner), but at onStop() it will ask all SMO to stop, but the ones not owned by them will refuse to stop as the key don't matches?

Also, I didn't knew that it always return shared dependencies first, also I don't see where this organization of 'first shared resources' happens in code. image image But it's a programming convention I use to start shared resources first.

I noticed that hack of being able to call a protected method from foreign class without problems, it was strange and funny that actually works. I see why this is possible and why it shouldn't, but can't see any simple fix for this 'framework exploit'. I imagine this would be a fix in compiler, but I see that this would add complexity to LabVIEW as something like to ensure protected methods are called only by "dynamic dispatch" terminal - and don't know if this would be better or worst... I don't remember if this happens in Java too, but I don't think so, as the compiler sees if the protected class is being called by inside scope 'this.method()' (or by a child inside scope 'super.method()'), and would not allow compile if a protected method being called from outside 'object.method()' I like comparing with Java as is a strong OO popular language, even LabVIEW is not comparable to Java, some language features are.

In middle of 2015 I was working with Actor Framework and State Machines for UIs and worked well, but LabVIEW 2013 IDE forced me totally drop Actor Framework, as it causes an overload of lvclass in project and this makes IDE slow, especially if using complicated hierarchy (multiple level heritance, aggregation, compsition) and also interacting with G#. Actor Framework by itselfs works fine, but LabVIEW 2013 IDE is not optimized to work with too much classes.

francois-normandin commented 8 years ago

So it will return all dependencies, shared and non shared, even if the shared dependency was not started by this SMO (is not owner), but at onStop() it will ask all SMO to stop, but the ones not owned by them will refuse to stop as the key don't matches?

Yes, you got that right. The ordering is done naturally by simply allowing dynamic dependencies to be added when the SMO is started. Since the static dependencies are added during the onStart( ) transition, they will be the first elements in the array. Since we know the first "n" elements of the dependencies array correspond to the "n" elements found in the dependencies cluster, we can use the knowledge to cast to more specific SMO.

But it's a programming convention I use to start shared resources first.

Good design choice. It ensures you always know which subsystem is the owner of the resources. You could also make sure that your top-level app creates all the shared resources before the other subsystems are launched.

but LabVIEW 2013 IDE forced me totally drop Actor Framework

Been there and I feel your pain. Actually this would also be a problem for SMOs if we didn't use templates with a numeric to store DVR references in the private data. This breaks the type propagation that so often kills the IDE performance.

If you don't know about this, check out Jon McBee's dependency viewer. Optimizing these metrics for large applications would surely lead to better IDE performance as a result.

francois-normandin commented 6 years ago

I am closing this discussion since the main discussion point was about uniqueness of names in the composition tree. This is addressed in #18 which will be part of the next release (1.4).

Thanks for the feedback.