Tutorial Java Optional Dependencies at runtime

Discussion in 'Resources' started by Europia79, Apr 13, 2014.

Thread Status:
Not open for further replies.
  1. Offline

    Europia79

    1st Example: Holograms & Composition

    As an example, let's say that we want our plugin to display Holograms (using an external library)... But remember, we don't even know if the server is going to have the external Hologram library installed at runtime. Maybe they made this decision deliberately because they do not want this extra functionality. In any case, it's completely optional, and we don't want the absence of an external library to cause our plugin to fail.

    First, let's choose a library: Me personally, I want to make things as easy as possible on the end-user, so I'm choosing both HolographicDisplays and HoloAPI. As a side benefit, if one of these plugins ever becomes inactive, then there's a 2nd one to power our Holograms!

    Next, add a line to your plugin.yml: softdepend: [HolographicDisplays,HoloAPI]

    Let's pause for a second here and contemplate good design.

    Now, what we could do is have boolean flag(s) and if-statements everywhere. But one of the guiding principles that I wanted to follow was to NOT litter my code with conditionals.

    Another thing we could do is some kind of switch statement for the 3 cases:
    • use HolographicDisplays to display Holograms
    • use HoloAPI to display Holograms
    • don't display any Holograms
    I'll quote Misko here:
    So let's make an abstraction of what we want: We'll call it HologramInterface. Then we'll create 3 different implementations.
    Code:java
    1. public class ExamplePlugin extends JavaPlugin {
    2. HologramInterface holograms;
    3. }

    Next, inside your onEnable(), we'll instantiate one of the implementations and assign it to the holograms field:
    Code:java
    1. boolean ShowHolograms = getConfig().getBoolean("ShowHolograms", true);
    2. Version HD = Version.getPluginVersion("HolographicDisplays");
    3. Version Holoapi = Version.getPluginVersion("HoloAPI");
    4. debug.log("HolographicDisplays version = " + HD.toString());
    5. debug.log("HoloAPI version = " + Holoapi.toString());
    6. if (ShowHolograms && HD.isCompatible("1.8.5")) {
    7. this.holograms = new HolographicDisplay(this);
    8. debug.log("HolographicDisplays support is enabled.");
    9. } else if (ShowHolograms && Holoapi.isEnabled()) {
    10. this.holograms = new HolographicAPI(this);
    11. debug.log("HoloAPI support is enabled.");
    12. } else {
    13. this.holograms = new HologramsOff();
    14. debug.log("Hologram support is disabled.");
    15. }

    So, normally, you'd condense down all the ifs and put them into your factory, but in this case, we're putting it inside onEnabe(). (Btw, you can find my Version class here).

    Next, let's make a method in our main class that gives Hologram creation+deletion to other classes:
    Code:java
    1. public HologramInterface holograms() {
    2. return this.holograms;
    3. }


    Okay, at this point, your plugin might want to do things differently than mine. In general tho, your HologramInterface is going to have 3 basic operations (methods): create() that takes a Location and String (or createX() that just takes a Location), remove() or delete(), and move() aka teleport().

    Now, let's suppose that we create() a Hologram... we're going to want some kind of way to reference it... in order to pass that reference to remove() or delete(). The problem is that we can't just say Hologram hologram = plugin.holograms().createX(location); Because the question arises: Are you talking about
    Code:java
    1. com.gmail.filoghost.holograms.api.Hologram
    2. or
    3. com.dsh105.holoapi.api.Hologram ?

    The first thing that popped into my mind is use an id system (like the Scheduler will return a taskID). But I'm sure there are other valid ways too. Like maybe store it in an Object ?

    Anyways, imma stop here. I don't want you to copy & paste code. I just wanted to give you ideas with examples on how to use optional dependencies. Btw, this particular example with Holograms uses a technique called composition... where your class is composed of Objects with the right behavior. The beauty of composition is that you can change the behavior of the holograms field at runtime: Like a command that toggles holograms on/off (where in this case, there are 2 implementations of the ON state, and the server may have BOTH!) This technique is also called the Strategy Design Pattern.

    For more information, you can watch Misko give an excellent presentation on polymorphism here. Or you can watch Derek explain the Strategy Design Pattern here. If you'd rather delve into a good book, Derek recommends Head First Design Patterns while Misko recommends Refactoring: Improving the Design of Existing Code.

    If you want to take a look at my specific implementations for the HologramInterface, you can find it on github:
    https://github.com/Europia79/Demolition/tree/master/src/main/java/mc/euro/demolition/holograms

    Remember, if you're ever in doubt: just ask yourself, "What would Misko do ?" Or, leave a questions/comments below and I'll try to help.

    And thanks to filoghost and DSH105 for such awesome plugins!

    2nd Example:

    i'll use BattleTracker as an example of using optional dependencies at runtime.

    This plugin uses a MySQL or SQLite database to keep track of player stats (thanks alkarinv!) So, as you can see, server owners may or may not want this extra functionality.

    One of the guiding principles that I wanted to follow was to NOT litter my code with conditionals everywhere that I used this optional dependency. Here's how:

    First, add a line to your plugin.yml: softdepend: [BattleTracker]

    Next, go ahead and hardcore to your hearts content (I personally don't mind going back and refactoring because #1. You want to get something working then go back for optimizations later and #2. I'll show you how easy it is). After I was done coding what I needed with BattleTracker, i called it 5 times. That would be 5 if-statements (not much, but it would grow as the plugin's codebase grew).

    So, inside of my Main class, i declared
    Code:java
    1. public TrackerInterface ti;


    And anywhere that I wanted to hook into BattleTracker, i simple used the public field:
    Code:java
    1. plugin.ti.someMethodCall();


    As it stands right now, this is really a mandatory dependency.

    Now, what we're going to do is make a man-in-the-middle Class: I called mine PlayerStats (couldn't think of a better name).

    In the Main class, replace
    Code:java
    1. public TrackerInterface ti;


    with
    Code:java
    1. public PlayerStats ti;


    Look for the errors your in IDE and click
    When you're done with that, create the constructor that loads BattleTracker, and create an isEnabled() method that will help you implement all those methods with conditional calls to whatever optional dependency you want.

    All those conditionals that you would have had are now condensed down into the methods of the man-in-the-middle class, PlayerStats. (almost).

    So, as I was saying: I called plugin.ti.someMethod() 5 times. That would have been 5 conditionals. All said and done, I was able to condense it down to 2 conditionals (1 in the Command Class and 1 in the man-in-the-middle Class - Not counting the contructors load method since it runs once).

    How 2 you say ?

    Notice that when hooking into this plugin, the methods fall into two categories: GETTING and ADDING. Adding player stats to the database. And getting those stats back.

    The add methods are really easy to deal with because we simple use our man-in-the-middle-class PlayerStats and call the appropriate addX method... And we don't care if it actually adds it or not.

    This is the final man-in-the-middle class, PlayerStats:
    PlayerStats.java (open)

    Code:java
    1. package tld.yourdomain.yourproject.tracker;
    2.  
    3. import java.util.List;
    4. import mc.alk.tracker.Tracker;
    5. import mc.alk.tracker.TrackerInterface;
    6. import mc.alk.tracker.objects.Stat;
    7. import mc.alk.tracker.objects.StatType;
    8. import mc.alk.tracker.objects.WLT;
    9. import mc.euro.demolition.Main;
    10. import org.bukkit.Bukkit;
    11.  
    12. /**
    13. *
    14. * @author Nikolai
    15. */
    16. public class PlayerStats {
    17. Main plugin;
    18. TrackerInterface tracker;
    19. boolean enabled;
    20.  
    21. public PlayerStats(String x) {
    22. plugin = (Main) Bukkit.getServer().getPluginManager().getPlugin("Demolition");
    23. loadTracker(x);
    24. }
    25.  
    26. public boolean isEnabled() {
    27. return enabled;
    28. }
    29.  
    30. private void loadTracker(String i) {
    31. Tracker t = (mc.alk.tracker.Tracker) Bukkit.getPluginManager().getPlugin("BattleTracker");
    32. if (t != null){
    33. enabled = true;
    34. tracker = Tracker.getInterface(i);
    35. } else {
    36. enabled = false;
    37. plugin.getLogger().warning("BattleTracker turned off or not found.");
    38. }
    39. }
    40.  
    41. public void addPlayerRecord(String name, String bombs, WLT wlt) {
    42. if (this.isEnabled()) {
    43. tracker.addPlayerRecord(name, bombs, wlt);
    44. }
    45. }
    46.  
    47. public List<Stat> getTopXWins(int n) {
    48. return tracker.getTopXWins(n);
    49. }
    50.  
    51. public List<Stat> getTopX(StatType statType, int n) {
    52. return tracker.getTopX(statType, n);
    53. }
    54.  
    55. }
    56.  



    EDIT by Moderator: merged posts, please use the edit button instead of double posting.
     
    Last edited by a moderator: Jun 7, 2016
    filoghost, DSH105 and NoChanceSD like this.
  2. Offline

    Europia79

    My google search results for "Java optional dependencies at runtime" were abssymal.

    I looked it up first because I was curious what techniques other people were using.

    When I didn't find any search results, this was the approach I developed.

    Posted here because I didn't see any a tutorials on this topic (unless my search results failed me again).

    I'm still curious what techniques other people are using tho. Lemme know!

    Thanks!

    I'm currently working on another bukkit dev experimental project... But when I get time, I plan to do more examples. Most likely it will be WorldGuard or Boss Bars API. Or if anyone has any other suggestions ?
     
  3. Offline

    Europia79

    EDIT: Moved BattleTracker to the 2nd example, and added Holograms as the 1st example (because it uses a more object-oriented approach... And it's cleaner + more complex in that it allows unknown implementations to be easily added in the future).
     
Thread Status:
Not open for further replies.

Share This Page