Jump to content

Isolating the API from Script


fixthissite

Recommended Posts

People using designs like the Node system tend to pass their script instance to each node for access to the API:

final class Bank extends Node {
    private Script script;

    public Bank(Script script) {
        this.script = script;
    }

    //...
}

The problem with this is the ability to call onPaint, onLoop and other "critical" methods (onExit, onStart) from the script instance.

 

This tutorial will show you how to create an instance of the API to pass around.

First thing we need is an instance that supplies the methods we need (from MethodProvider). We can do this by extending upon the API type:

final class DefaultAPI extends API {

}

You will be forced to declare an initializeModule method. Don't worry about it, just the devs not following interface segregation wink.png Leave it blank. You could fill it in, but you would be required to call it after instantiating API. 

Once you have your API type, create an instance of it in onStart in your Script subclass:

final class MyScript extends Script {
    private API api;

    public void onStart() {
        api = new DefaultAPI();

    }
}

Finally, we need to pass the context of our script to our api instance. We do this by calling exchangeContext on our api instance, passing in the script's bot:

final class MyScript extends Script {
    private API api;

    public void onStart() {
        api = new DefaultAPI();
        api.exchangeContext(getBot());

    }
}

Please use the getter method getBot() and not the public field bot.

So now our API instance has the context; we can call methods such as myPlayer() from our API instance. We should now pass around the API, rather than the entire script:

final class Bank extends Node {
    private API api;

    public Bank(API api) {
        this.api = api;
    }

    //...
}

For those who might say "Who's stupid enough to call onPaint or onLoop?":

 

Encapsulation does not only help prevent these miniscule mistakes, but helps lower the cognition needed by supplying you with only the things you actually need, rather than a bunch of other irrelevant things. The client should not be presented with onLoop or onPaint, since those do not have purpose within the Node class. Out of sight, out of mind.

Edited by fixthissite
  • Like 8
Link to comment
Share on other sites

  • 2 weeks later...
  • 1 month later...

 

 

Hey i've just quoted you so that you get a notification.

 

I like what you've done here, and was about to implement it myself but I came across a few issues, and was looking for some suggestions.

 

As it stands, I pass the entire script into each class, including the gui, I then use the instance to pass values from the GUI back into the main instance so that it can be properly used by other classes. What I have come across is, how I should be accessing this data using the setup you have suggested, should I just pass it all into the DefaultAPI class you have used as an example, and access my extra data from there instead? (Stuff like if the script should bank, what fish to catch, what logs to chop, etc).

 

Thanks, sorry if its poorly worded

Link to comment
Share on other sites

Hey i've just quoted you so that you get a notification.

 

I like what you've done here, and was about to implement it myself but I came across a few issues, and was looking for some suggestions.

 

As it stands, I pass the entire script into each class, including the gui, I then use the instance to pass values from the GUI back into the main instance so that it can be properly used by other classes. What I have come across is, how I should be accessing this data using the setup you have suggested, should I just pass it all into the DefaultAPI class you have used as an example, and access my extra data from there instead? (Stuff like if the script should bank, what fish to catch, what logs to chop, etc).

 

Thanks, sorry if its poorly worded

 

Why not have a settings class? Within the settings class, have static vars which you change in the GUI?

Link to comment
Share on other sites

 

Get ready for a long post, buddy.

 

It depends. The easiest way to handle this would be to hardcode the settings into the DefaultAPI and pass that around as you've mentioned, more preferably creating a Settings type to decompose the DefaultAPI:

class MySettings {
    private int logId;

    public int getLogId() {
        return logId;
    }

    public void setLogId(int id) {
        logId = id;
    }
}
class DefaultAPI extends API {
     private MySettings settings;

     public void setSettings(MySettings settings) {
         this.settings = settings;
     }

     public MySettings settings() {
          return settings;
     }
}

You could then create an instance of MySettings whenever the user clicks the "start" button on your GUI, passing it to the API.

 

The code above does not account for (im)mutability; it  does what you mention, but it's decomposed for easy maintenence. It allows you to adjust the settings at just about anytime, and you can modify the settings in different ways. So now comes the encapsulation, which strongly depends on how you want this to function.

 

If the settings are handled at the start of your script, and should not be modified afterwards

 

First, remove the ability to modify the settings property in DefaultAPI. You can do this by creating the API when you create the user clicks the "start" button to start the script. The idea is that the GUI handles the "arguments" of the script (similar to how the VM has VM arguments and command-line programs have command-line arguments):

class GUI {
     private Object lock = new Object(); //used for waiting

     public DefaultAPI create() {
          GUI gui = new GUI();
          gui.setVisible(true);

          //halt thread until user clicks "start" button
          try {
               synchronized(lock) {
                     lock.wait();
               }
          } catch(InterruptedException e) {
               //...
          }
          
          MySettings settings = ...;
          DefaultAPI api = new DefaultAPI(settings);
          return api;
     }

     private ActionListener listener = event -> {
          synchronized(lock) {
               lock.notify();
          }
     };
}
class DefaultAPI {
     private MySettings settings;

     public DefaultAPI(MySettings settings) {
          this.settings = settings;
     }

     public MySettings settings() {
          return settings;
     }

     //can no longer switch instance of Settings
}
class MyScript extends Script {
     void onStart() {
           DefaultAPI api = GUI.create();
           //pass api to nodes
     }
}

So once you've passed the API the settings, it can not be modified by passing the API a new MySettings instance. Although, nodes can still adjust the settings through the setter methods of MySettings.

 

For situations like this, where we want to be able to optionally set values on creation, but prevent them from being modified after creation, we would use the builder pattern:

class MySettings {
     private int logId;

     private MySettings(Builder builder) {
          logId = builder.logId;
     }

     public int getLogId() {
          return logId;
     }

     //no mutators; cannot adjust settings after creating

     public static final class Builder {
          private int logId;

          public Builder setLogId(int id) {
                logId = id;
                return this;
          }

          //other builder methods go here...

          public MySettings build() {
                return new MySettings(this);
          }
     }
}

When you create the API in GUI.create(), simply use the builder:

MySettings settings = new MySettings.Builder()
     .setLogId(/* grab from component */)
     .build();

DefaultAPI api = new DefaultAPI(settings);

In this situation, a builder is an overkill; you could simply pass the logId to the constructor. But as the amount of settings options increase, so will the amount of constructor arguments. If you have 10 settings, all which are of type int, it could easily confuse the developer, which is why a builder is preferred.

 

Not sure why theres no support for this in the API already. If I were you guys, I'd request more support for things like this (GUI, script settings).

 

Just to clear the air, it is prefered to use static getter/setters rather than accessing a static field directly. You can read more on my "Use Getter Methods, Not Public Fields" thread - the idea is to hide implementation details, allowing you to change how you store a property without breaking code that relies on it.

 

As for using static getters/setters for this, it would make it a LOT easier. Although, when it comes to unit testing, difficulties arise (hard to isolate code that relies on global state). It could also make graphing your code a mess, with edges that fly all over the place. It's best to keep things as encapsulated as possible. Rather than accessing something globally (accessing a dependency globally), pass in the dependency through a method/constructor. This is called dependency injection, and makes code crazy easy to unit test.

 

Don't do

class MyClass {
     public void doSomething() {
          int answer = OtherClass.number + 5;
     }
}

Do

class MyClass {
     public void doSomething(int number) {
          int answer = number + 5;
     }
}

Passing in the number when you call the method.

 

Sorry for the long post, I like to be fluent. smile.png

Edited by fixthissite
  • Like 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

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.

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...