4

This is not an easy one but here goes. I'm working on adapting a JavaScript project (ACE Editor) to support two different targets but maybe more. Any way I look at it, it looks like a large task and no approach feels quite right.

I started to create a class that extends a HTML web view. This is basically a web view, a browser component that works in the desktop application application. The class has a document and window property and I can pass in an HTML page. This works. The page loads and all the methods talk directory with the ace editor. The class is here.

For example,

public function find(value:String, options:Object = null, animate:Boolean = false):Object {
    return editor.find(value, options, animate);
}

But now I need this class to also work in the browser.

My Editor class implementation has or will have over 265 methods and counting. The class extends the HTML control. But this control won't run in the browser.

So to support the browser I'm now extending only EventDispatcher. I then create an instance of the HTML control when I know that I'm running outside of the browser. That's working.

But to support the browser I need to create an IFrame component which creates an iframe in the browser and it has a different way to talk to it. For example, to make same call to the iframe as mentioned above I have to use the following:

result = ExternalInterface.call("find", value, options, animate);

or

result = ExternalInterface.call("editor.find", value, options, animate); // doesn't look like I drill down into a method

I'm not sure which it is.

So what I'm thinking is:

  • Create a base class and have the a desktop version and a browser version and both implement an common interface
  • Make the editor object an ObjectProxy and here. Then branch to the HTML control editor or browser editor and somehow abstract out the calls to each one. Might be smooth but might be sloppy.
  • Extend the Editor and override each method that knows how to talk to the browser instance
  • Create a separate class BrowserEditor with or without implementing an interface
  • New option: Use if else conditions in every method

One more thing, if I can't talk to the editor with ExternalInterface.call("editor.method") and can only use call("method") then I have to write all the methods in JavaScript as an adapter, call("MyFindMethod").

Any suggestions would be helpful.

More information:

I'm using ActionScript in the desktop application and in the browser the Flash Player. The Flash Player can talk to JavaScript through ExternalInterface and here and I can setup listeners for events from JavaScript objects to talk to Flash. I'm using the Flex Framework that runs on top of the Flash Player.

0

2 Answers 2

1

It seems unlikely that a class with 265 methods is the right solution, you may want to consider breaking it up into smaller pieces.

The problem you describe sounds a lot like the device driver type problem where you have application logic that depends on some capability, but there are multiple ways to provide that capability. The classic solution is not any of the options you proposed above, but to inject the piece that varies into the application logic.

Drilling down, the idea would be to figure out the smallest API that can be implemented by the browser and desktop versions and will allow your application to do everything it needs. You should encapsulate this API in an interface definition, and inject it into your application logic, then at runtime you choose whether to inject the desktop implementation of the interface or the browser version.

5
  • I was typing up my comment into an answer when I saw you post this. I've removed the original comment and I've listed my reasons for the choices in the answer I posted. Thank you for your answer. Dec 25, 2016 at 23:01
  • That's how I normally would go about it. I would create an interface or an abstract class and then create the browser class or desktop class on top of it. But in this case the class will only grow. It is more like a composite component. It encapsulates a lot of small parts to make it portable and easy to work with. And I asked myself, how do I want to work with this component? Having the methods on the class makes the most sense for me and what I predict it will be used for by others. Dec 25, 2016 at 23:09
  • Ok, I'm missing something. If you need any further input I am willing to help, but right now I think I don't understand your problem.
    – bikeman868
    Dec 26, 2016 at 2:57
  • I wrote a reply and then placed that at the end of my answer. Dec 26, 2016 at 21:06
  • Note: I've realized that the method I posted in my answer may be overkill. I started to implement the full interface and then realized the main component class did not need repeating and using an interface to declare all the properties as getters and setters was not necessary. It was only the method calls. So now I'm calling method calls that have branching logic. I might later extend the main class later as BrowserEditor extends Editor and override the methods. Right now, I consider this discovery phase and I'll refactor after it's working. I might post a link to the WIP class later... Jan 1, 2017 at 21:11
0

Going to start by trying an object proxy. If that doesn't work well going to use if else conditions, if in browser do this else if on desktop do this.

It feels like there could be a better way but I don't see it.

The benefit of object proxy or if else statements is:

  • I or another developer can drop in one class when needed instead of multiple classes or sub classes. Instead of having instances of BrowserEditor, DesktopEditor, and MobileEditor I can just drop in Editor. Less management.
  • Using one class instead of multiple or sub classes is that the one class takes care of talking to all the different parts. If a developer needs to make changes he just has one class to manage. Using multiple classes you have to make sure to override each method (could be overlooked) or implement the interface in two or more places. Less separate parts and less chance of forgetting to update, add or remove code across multiple classes.
  • When it comes time to use the class the developer just has to be concerned with addressing one instance instead of multiple. He can set properties and values to the same instance in his application instead of managing multiple instances. His code doesn't have if else statements all over the place.

This last point is important in languages that target multiple platforms from the same code base. So in my case I can use a single code base and export to mobile, desktop or browser.


Supporting cross platform development was a big feature before Steve Jobs destroyed all the lessons and progress towards unification that the web had taught us. His walled garden model and restriction of technology was soon followed by Google, Microsoft, RIM and others.

Instead of the benefit of publishing your content to the web to make it accessible to all devices he blocked plugins like the JavaFX player, Silverlight player, Flash Player, Unity Player and others. Instead of publishing to the world through the ubiquity of the web, you had to publish through his app store and if you chose to sell your content he got to take a cut (a huge cut).

He preached that plugins were a problem on mobile devices (and then it spread that plugins were a problem on desktop environments) while secretly allowing his own plugins like Quicktime in Safari and installing app launcher in other browsers. If you are on Mac look at the plugins in your browser. There's one from Apple right there. As further proof, the few the things he said about plugins that were true, he didn't allow those plugins the opportunity to fix them. Plugin authors added power management, increased security and other features and fixed the issues he mentioned. It was never about the issues it was about control. If you need a break.

Anyway, the problem wasn't plugins, it was about control. And the very argument that plugins are an unsolvable problem is saying that software is an unsolvable problem. Software, is not an inflexible medium. If there is a bug found in a plugin or a browser or an operating system it can be fixed because it's [expletive deleted] software not hardware! Bugs are fixed every [deleted] day by Apple, MS, Google and so on!

Anyway, the question is about cross platform development which is why I'm bring this all up. This is something that Adobe, Oracle (Java?), and Microsoft supported with tons of resources for many many years until they let the uneducated tech media talk them into all using HTML (with a fancy new imaginary specification that does or will do everything plugins do) and JavaScript. And even now they are the best technologies of 1995. Except that now Microsoft is reasserting itself and standing up for cross platform development and has released Visual Studio for Mac (it was once only a Windows IDE). And hopefully other companies with some [deleted] [deleted] will do the same.

Sorry not sorry, rant over.


After sleeping on @bikemans868's suggestion it reminded me of a design pattern I saw used before (don't remember the name).

It goes like this, I'm a framework author and I want to create a button but this button has to work in two different environments. Each environment has a different syntax to get it to work. I don't want the developer using my button to worry about creating different buttons for each environment.

So I create MyButton class and it has all the methods and API that I want. I then create two additional classes for environment A and environment B. Then at runtime or compile time I determine what environment I'm in and then set the implementation in the Button class.

In the following code I set the implementation in the constructor. So the class looks like this:

public class Button implements IButton {

   public function Button() {

       // the registration could take place somewhere else
       if (desktop) {
           ImplementationClass = getClass("com.example.DesktopButton");
       }
       else if (browser) {
           ImplementationClass = getClass("com.example.BrowserButton");
       }

       implementation = new ImplementationClass();
   }

   public var ImplementationClass:Object;

   public var implementation:IButton;

   public function setLabel(value:String):void {
       implementation.setLabel(value);
   }
}

public class DesktopButton implements IButton {

   public function setLabel(value:String):void {
       button.label = value;
   }
}

public class BrowserButton implements IButton {

   public function setLabel(value:String):void {
       ExternalInterface.call("function(value) { document...button.label = value; }", value);
   }
}

Also, I tried ObjectProxy and that only abstracts out the first method. It fails because the proxy only passes in the name of the method but nothing after it. With ExternalInterface I have to pass the full block of code rather and then return a value.

The object proxy call method:

function callProperty(name:String, ...args):* {
    if (isDesktop) {
        return myObject[name].apply(this, args);
    }
    else {
        return ExternalInterface.call("function (name, args) { return myObject[name].apply(this, args); }", name, args);
    }
}

so this works:

myProxy.doMethod(value1, value2);

But fails with this:

myProxy.getElement(id).getTextRange(start, end);

That's because I can't return a reference from another vm besides other limitations.


Update:
I found a real world example for the DragManager class that I'm going to model it after. In the browser the class uses mx.managers.DragManagerImpl and on the desktop it uses NativeDragManagerImpl that interacts with the operating system:

3
  • 1
    This is very close to what I suggested in my answer. The only difference is that I suggested passing the implementation class to the constructor (dependency injection) rather than have button know how to determine the environment it is running in. You can encapsulate the knowledge of determining the environment using a dependency injection framework (of which there are many).
    – bikeman868
    Dec 26, 2016 at 22:09
  • You suggested passing in the class through the constructor. If I wasn't able to pass in the class through the constructor is there a second best method you could recommend? The framework I use doesn't allow constructor arguments when declared in XML so I have to find an alternative. The way I'm handling it for XML is to let the developer define the browserClass or the desktopClass as an attribute. Then, when the components initialized I create the instance. Dec 27, 2016 at 1:17
  • 1
    Constructor injection is the preferred method, second best is property injection and least favourite is an Initialize method. With property injection you would add an attribute to a property identifying it to the dependency injection framework as something that needs to be built up after construction. After deserializing the XML, you pass the constructed object to the DI framework and it will set the dependency properties.
    – bikeman868
    Dec 27, 2016 at 1:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.