Jump to content

AOP architecture issues and options


Recommended Posts

I am posting this in the Application Design and Architecture forum instead of the OOP forum because I think it fits here better, but admins feel free to move the thread to the appropriate spot.

 

Also, I am posting this to LAVA instead of the AF forum because my questions relate to all Actor Orientated Programming architectures and not just AF.

 

Some background, I looked at AF and a few other messages based architectures before building my own.  I stole liberally from AF and others to build what I am using now.  But I am having a bit of a crisis of confidence (and several users I am sure will want to say 'I told you so') in the command pattern method of messaging.  Specifically in how it binds all actors in your project together via dependencies.  For example:

If Actor A has a message (that in turns calls a method in Actor A) to create an instance of Actor B, then adding Actor A to a project create a static dependency on Actor B.  This makes it impossible to test Actor A in isolation with a test harness.  The recent VI Shots episode about AOP discussed the need to isolate Actors so they could be built and tested as independent 'actors'.  If Actor B also has the ability to launch Actor C, then Actor C also becomes a dependency of Actor A.  And if Actor B sends a message to Actor A, then the static link to that message (required to construct it) will create a dependency on Actor A for Actor B.  So, the end result is adding any actor in your project to another project loads the entire hierarchy of actors into memory as a dependency and makes testing anything in isolation impossible.

This is the effect I am seeing.  If there is a way to mitigate or remove this issue without abandoning command pattern messaging, I would be very interested.

 

In the meantime, I am considering altering my architecture to remove the command pattern portion of the design.  One option I am considering is to have the generic message handler in the top level actor no longer dispatch to a 'do' or 'execute' method in the incoming message class but instead dispatch to an override method in each actor's class.  This 'execute messages' method would then have a deep case structure (like the old QMH design) that would select the case based on message type and then in each case call the specific method(s) in the actor to execute the message.  I would lose the automatic type handling of objects for the message data (and have to revert back to passing variant data and casting it before I use it) and I would lose the advantages that dynamic dispatch offers for selecting the right message execution code.  I would have to maintain either an enum or a specific set of strings for message names in both the actor and all others that message it.  But I will have decoupled the actor from others that message it.  I think I can remove the launch dependency by loading the child actor from disk by name and then sending it an init message to set it up instead of configuring it directly in the launching actor.

 

I guess am wondering if there other options to consider?  Is it worth the effort to decouple the actors or should I just live with the co-dependency issues.  And how does this affect performance?  I suspect that by eliminating all the message classes from my project, the IDE will run a lot smoother, but will I get a run-time performance boost as well?

Has anyone build systems with both types of architectures and compared them?

I also suspect I will get a benefit in readability as it will be easier to navigate the case structure than the array of message classes but is there anything else I will lose other than the type safety and dispatch benefits?

 

And does anyone who uses AF or a similar command pattern based message system for AOP want to try to defend it as a viable option?  I am reluctant to give up on it as is seems to be the most elegant design for AOP, but I am already at 326 classes (185 are message classes) and still growing.  The IDE is getting slower and slower and I am spending more and more time trying to keep the overall application design clear in my head as I navigate the myriad of message classes.

 

I appreciate your thoughts on this.  I think we need to understand the benefits and pitfalls these architectures present us so we can find a better solution for large LabVIEW application designs.

 

-John

 

Link to comment
I guess am wondering if there other options to consider?  Is it worth the effort to decouple the actors or should I just live with the co-dependency issues.  And how does this affect performance?  I suspect that by eliminating all the message classes from my project, the IDE will run a lot smoother, but will I get a run-time performance boost as well?

Has anyone build systems with both types of architectures and compared them?

I also suspect I will get a benefit in readability as it will be easier to navigate the case structure than the array of message classes but is there anything else I will lose other than the type safety and dispatch benefits?

I haven't built any systems with the goal of comparing the two architectures, and certainly not at that level of complexity you're talking about. I'm sorry for pulling just a few sentences out of a much longer post, but a couple comments about performance and other possible disadvantages.

 

I don't think you'll get a run-time boost; you might even get worse run-time performance. A case structure with many cases and a string as a selector will most likely be slower than dynamic dispatch, and may get slower as you add more cases (somewhat dependent on the algorithm used to find the right case). Dynamic dispatch is constant time regardless of how many methods override the base call.

 

There's almost no overhead in conversion to a variant. There's probably a small amount of overhead in conversion from a variant because it requires checking that the types match, but it may be insignificant. Don't underestimate the value of type safety, though - it shifts potential errors from run-time, where they're problematic, to edit-time, where they can be easily corrected.

 

I'm not sure that having a case structure will be easier to read; might depend on how many cases are in it. One thing you will lose is the ability to call the parent. That makes it more difficult to share code across multiple similar cases - you'll either need to duplicate the contents of some cases, or make some cases that match multiple selections and then have special cases inside that for each slight difference.

 

Is your goal simply to improve editor performance? It seems like your architecture works well in an application, so if the issues are purely in the development environment, maybe this is a good opportunity to bang on NI and see if they can find ways to improve the IDE (admittedly this isn't a short-term solution).

Link to comment

My goal is to make a better AOP architecture that decouples actors so they can be built and tested separate from the overall application.

IDE performance, execution speed and code complexity are just some areas that might benefit or suffer from a move away from command pattern messages.

I am hoping to get either a 'stick with command pattern, it is really the best and worth the issues' or a 'dump it and go with the more traditional string-variant message because it is the best way in LabVIEW'.

And I want to be sure I can truly decouple when using string-variant and what pitfalls and best practices exist for spawning child actors and having a mix of common and specific messages.  So, I appreciate your thoughts on the use of case to select execution of messages.

I am not looking forward to a major re-factor of the code so I am hoping for answer #1 but the sooner I get on the right track the better as I am about 50% of the way through this project.

Link to comment

I believe thinking about this problem in the context of Actors (be it the AF proper or in the generic sense) is a red herring. To me it appears to be a traditional problem of static dependencies getting out of control. Not only is this an issue for Actors, but classes of any type, libraries in general, and even more primitive constructs like the lowly typedef or VI.

The only way out of this is to bake some layer of loose coupling into the system: rather than dropping a constant you use mechanism of looking up at run time the type you need.

So I'm not convinced the issue is specific to the command pattern, though since it does invoke objects it does introduce a potential escape of the problem-- abstraction.

Link to comment

Yes, there are several ways of coupling classes together.  But command-pattern causing a very tight coupling.  This is due to message classes being statically linked to senders (they need a copy of the class to construct the message since the message class control defines the data being sent) and being statically linked to the receiver because the message contains the execute method that needs access to the receiver private data.

So, if we could divorce the message data from the message execution and then agree to a common set of objects, typedefs or other data types to define all message data, we could break most of the coupling and only share those data objects (the 'language' of the application).

But then we need to ensure the message names (strings) are always correct, and the sender constructs the data in the same arrangement the receiver expects.

The next issue is spawning child actors.  Normally this requires an actor to have a static link to the child actor's class to instantiate it.  To break the coupling, we could load the child from disk by name but then we need some means to set any initial values in its private data.  This could be accomplished by an init message using the same loose coupling from above.

But will the end result be a better architecture?  As noted, this design is at risk for runtime errors.  But the advantage is you can fully test an actor in isolation.

If only there was some way to get both benefits.

Link to comment

So I'm not convinced the issue is specific to the command pattern, though since it does invoke objects it does introduce a potential escape of the problem-- abstraction.

Agreed. It is a language artifact but the escape isn't abstraction it's modularisation. Abstraction just makes the fat pipes fatter.

 

The only way out of this is to bake some layer of loose coupling into the system: rather than dropping a constant you use mechanism of looking up at run time the type you need.

Again agreed. If you break the actors into identifiable groups to form APIs (and thus limit their inter-dependencies), you can then use AF for internal API messaging but strings for inter-API messaging. This also helps you bridge the network gap as you essentially use use AOP for modules and string based SOP for process.

I remember a discussion with Daklu where we discussed this with his Lapdog stuff.

Edited by ShaunR
Link to comment

Regarding your original post here:  Maybe I have the wrong end of the stick here but here goes.....

 

Your Actor A has a base "Do" implementation which gets called on any message being received, right?  The only thing adding B as a dependency is the message set up to do this (and it's associated dynamic dispatch "Do").

 

I was the opinion this would make Actor B a dependency on the specific message for launching B and NOT of the actual Actor A.  Or do you somehow have the messages stored within A?

 

I don't currently use the AF because I too found it not a correct fit for what I do (and have been pondering an event-based system for quite some time now - unfortunately without concrete results).

 

Shane.

Link to comment

If Actor A has a message (that in turns calls a method in Actor A) to create an instance of Actor B, then adding Actor A to a project create a static dependency on Actor B. This makes it impossible to test Actor A in isolation with a test harness. The recent VI Shots episode about AOP discussed the need to isolate Actors so they could be built and tested as independent 'actors'. If Actor B also has the ability to launch Actor C, then Actor C also becomes a dependency of Actor A. And if Actor B sends a message to Actor A, then the static link to that message (required to construct it) will create a dependency on Actor A for Actor B. So, the end result is adding any actor in your project to another project loads the entire hierarchy of actors into memory as a dependency and makes testing anything in isolation impossible.

How about making Actor B an interface and use the factory pattern?

If Actor B were an interface it would not have any dependencies to Actor C, since that would imply implementation. Now if you test Actor A in isolation it would in fact load the interface of Actor B into memory (cannot be avoided due to a static dependency), but the actual implementation of Actor B (that causes more dependencies to load) is not necessary. I have little experience with AF, but in my opinion using interfaces and the factory pattern should drastically reduce the number of classes in your project (you would "only" need the interfaces and all Message classes which belong to them). The actual implementation can be done seperately.

Link to comment

Your Actor A has a base "Do" implementation which gets called on any message being received, right?  The only thing adding B as a dependency is the message set up to do this (and it's associated dynamic dispatch "Do").

I was the opinion this would make Actor B a dependency on the specific message for launching B and NOT of the actual Actor A.  Or do you somehow have the messages stored within A?

You are correct, but in most AF systems, the message simply calls a method in the Actor where the real work is done.  So, the code that launches B would be in A.  But that does not mean you could not put the code in the message 'Do' only and isolate B from A.  But then you need to ask how is the message being called?  In most cases, A is calling itself to create B due to some state change or other action.  In that case, some method in A need to send the message to itself and then A has a static link to the message which has a static link to B.  This is exactly what happened to me and took a while to understand what what happening since there is no way to visualize this.

 

How about making Actor B an interface and use the factory pattern?

If Actor B were an interface it would not have any dependencies to Actor C, since that would imply implementation. Now if you test Actor A in isolation it would in fact load the interface of Actor B into memory (cannot be avoided due to a static dependency), but the actual implementation of Actor B (that causes more dependencies to load) is not necessary. I have little experience with AF, but in my opinion using interfaces and the factory pattern should drastically reduce the number of classes in your project (you would "only" need the interfaces and all Message classes which belong to them). The actual implementation can be done seperately.

Wouldn't this result in even more classes?  I would need the abstract interface class and then concrete class for every actor?  Or am I misunderstanding you?

Link to comment
Hi John,
 
Are you designing a generic framework, or a specific application?
 

 

If Actor A has a message (that in turns calls a method in Actor A) to create an instance of Actor B, then adding Actor A to a project create a static dependency on Actor B.  This makes it impossible to test Actor A in isolation with a test harness.  The recent VI Shots episode about AOP discussed the need to isolate Actors so they could be built and tested as independent 'actors'.  If Actor B also has the ability to launch Actor C, then Actor C also becomes a dependency of Actor A.  And if Actor B sends a message to Actor A, then the static link to that message (required to construct it) will create a dependency on Actor A for Actor B.  So, the end result is adding any actor in your project to another project loads the entire hierarchy of actors into memory as a dependency and makes testing anything in isolation impossible.

 

A static dependency doesn't necessarily preclude testing in isolation.

 

Let's say we have 2 classes (A and B), where some of Class A's actions involve launching an instance of B and exchanging messages with it. Your unit tests could look something like this:

  • Class B testing:
    1. Launch an instance of Class B from a top-level VI
    2. Let your top-level VI send a variety of messages to your B instance, and check that it reacts appropriately
  • Class A testing:
    1. Launch an instant of Class A from a top-level VI
    2. Let your top-level VI send a variety of messages to your B instance, and check that it reacts appropriately

(For even better isolation, launch a new instance for every message tested)

 

Yes, the Class A tests will also cause Class B to be loaded and launched, but your test harness for Class A doesn't know that Class B is involved at all. Some might classify the Class A test as an Integration test rather than a Unit test, but the bottom line is that you can still treat Class A as an isolated black box.

Link to comment

You are correct, but in most AF systems, the message simply calls a method in the Actor where the real work is done.  So, the code that launches B would be in A.  But that does not mean you could not put the code in the message 'Do' only and isolate B from A.  But then you need to ask how is the message being called?  In most cases, A is calling itself to create B due to some state change or other action.  In that case, some method in A need to send the message to itself and then A has a static link to the message which has a static link to B.  This is exactly what happened to me and took a while to understand what what happening since there is no way to visualize this.

 

If the code for launching B is in A, then yes then A is dependent on B, that's the way the system is.  If A needs to have knowledge of B in order to be able to function then those two modules are coupled whether you like it or not.

 

With the code for launching B in a message you are right that a static copy of that message on the BD of A will still cause a link.  Can you interact with B as a plain actor?  If so, then you could also load the messages which are associated with certain state changes (used for deciding whent o launch B) via Factory method - as a base Actor - and then your static dependencies are gone.

 

I've done this kind of thing before where I have a pre-determined state machine (Actually based on User Events from specific UI elements in my case - nary an actor in sight) and then I provide at run-time the objects (commands, messages whatever you want to call them) to the running code to tell them "When A happens, do this".  The list of Events is static (This is the API) but the actual actions performed at any stage remains reconfigurable and any specific dependencies disappear.  You have a skeleton of a state machine where the meat is provided at run-time.

Edited by shoneill
Link to comment

Wouldn't this result in even more classes?  I would need the abstract interface class and then concrete class for every actor?  Or am I misunderstanding you?

You're right, the total number of classes increases. I should have written "..reduce the number of classes in your current project..". You'll of course have to implement the Actor in another project.

I've attached two pictures to clarify what I mean. Left(top) is the current situation, right(bottom) is the example with an interface. Blue is one project, green another.

post-17453-0-20758400-1408100075_thumb.jpost-17453-0-37440900-1408100079_thumb.j

Edited by LogMAN
Link to comment

 

Hi John,
 
Are you designing a generic framework, or a specific application?
Both, actually.  I created a framework that is generic, but has features I need for a few specific applications, like network messaging and subscriptions.
Then I started implementing a specific application with the framework.  That is where I have observed the dependency issue.

 

Yes, the Class A tests will also cause Class B to be loaded and launched, but your test harness for Class A doesn't know that Class B is involved at all. Some might classify the Class A test as an Integration test rather than a Unit test, but the bottom line is that you can still treat Class A as an isolated black box.

I agree and if I could limit it to just a few classes, that would be fine.  But these links build up quickly to the point now where if I load Actor B to test it, it loads A and C-Z!  To paraphrase that old STD PSA from the 80's-90's: if you are statically linked to one actor, you are linked to every actor they are linked to and every actor those other actors are linked to.

I can actually open a new project, add one of my actor classes and watch as LabVIEW loads 80% of the Actors (and many of their messages) into the dependency list.

 

I guess the real question is: should I care?  Is this dependency issue just annoying due to IDE slowness and difficulty in testing/developing in isolation or is it a fundamental flaw in the command-pattern message approach that makes it unworkable for anything other than small simple systems?

You're right, the total number of classes increases. I should have written "..reduce the number of classes in your current project..". You'll of course have to implement the Actor in another project.

This is basically how I solve the issue when sending messages between two applications.  But the level of complexity and extra classes it introduces would make the project even more difficult to deal with.  In reality, I would have to create an interface for every Actor and every message to isolate them.  It is already become difficult to follow the execution flow with no way to visualize the overall application other than how I arrange the project.  Adding abstract classes on top of everything would make that worse and lead to maintenance issues down the road.

 

I am beginning to think there are many solutions but maybe no good ones.  Surprised that no AF people have stepped up and defended the use of command-pattern.  I would like to hear their thoughts.

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
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
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.