Skip to content

Latest commit

 

History

History
552 lines (437 loc) · 18.6 KB

README.adoc

File metadata and controls

552 lines (437 loc) · 18.6 KB

Simple Dependency Injection

SDI allows to extensively use Dependency Injection pattern in your application. It achieves it without using annotations or reflections - just simple Java code. Additionally, you can manage your service/application life cycle just by calling methods: init(), start(), stop() and close() also by just calling these method on parent service. You don’t have any "configuration" for wiring up all classes - it is done in code. All that takes about 600 lines of code.

Please let me know if you have any comments or suggestions about this project.

How to start

Add dependency on SDI framework in your build file:

Gradle

dependencies {
    compile 'net.igsoft:sdi:0.9.3'
}

Maven

<dependency>
    <groupId>net.igsoft</groupId>
    <artifactId>sdi</artifactId>
    <version>0.9.3</version>
</dependency>

SDI has only dependency on Guava and Logback. Dependency on Guava is not critical, so I will consider to remove it.

Concept and simple usage

The general concept is that for all classes which you want to put into this smallish DI framework you prepare creator. Creator for some specific class has to inherit from abstract class CreatorBase<>. If the creation of the class is simple because that class only depends on other available classes, it is possible to use AutoCreator<> but it is still the same concept, but a little bit more automatization.

In the simplest case, class and its creator can look like below:

SimpleCreatorExample.java
    public static class Config {}

    public static class ConfigCreator extends CreatorBase<Config, LaunchType> {
        @Override
        public Config create(InstanceProvider instanceProvider, LaunchType launchType) {
            return new Config();
        }
    }

Right now it’s not very interesting - just a little bit more code to write. The fun part starts when we have more complicated classes.

Let’s say that we have a class App which has a class Config as a dependency.

SimpleCreatorExample.java
    public static class App {
        public App(Config e) {}
    }

    public static class AppCreator extends CreatorBase<App, LaunchType> {
        @Override
        public App create(InstanceProvider instanceProvider, LaunchType launchType) {
            Config config = instanceProvider.getOrCreate(Config.class);
            return new App(config);
        }
    }

That way we have just applied dependency injection pattern in the simple case of two classes. We have just delegated the work to InstanceProvider, which is responsible for finding the correct creator.

But how we can use such classes?

Very simple: let’s just create service which will keep information about all available instances:

SimpleCreatorExample.java
    public static void main(String[] args) {
        Service service = Service.builder()
                                 .withRootCreator(new AppCreator())
                                 .withCreator(new ConfigCreator())
                                 .build();

        Runtime.getRuntime().addShutdownHook(new Thread(service::close));

        service.start();
    }

That way we can get any instance of a class from the class hierarchy of App without worrying about dependencies.

You might notice a different way in which creators are added to the Service - application creator is added by using .withRootCreator() method, but configuration creator is added with .withCreator() method. What is the difference? Basically you can use only .withCreator() method to add all creators to the service. All class hierarchy roots will be find automatically and everything will work as expected. The drawback of such an approach is that all classes will be created, even if their creators are there only by mistake. In such a case it is just not possible to find out that some creators are in service just because they were added by mistake. In case of marking root creators as such during instantiation you will get a message that some of creators are not used during that process. It is possible because when the Service() instance is build all the instances are already created and it is clear which creators were used during initialization and which are not. Another advantage of using explicit root creators is for code readability: it is then imediatelly visible which classes should be analyzed first, when trying to understand application. Notice that sometimes it is necessary to add a few root creators, which is possible and necessary if you want to have them all instantiated.

Initialization of class instances

That’s nice, but very often we need to initialize and later start at least some of the classes in order to start our application. And when finishing application, we need to stop e.g. working threads and finally release resources. That’s still a lot of manual work.

Fortunately, there is a simple solution to this problem in SDI. It’s just necessary to mark classes, which should be initialized/started/stopped/closed with one of the two interfaces: Manageable or ManageableBasic. ManageableBasic has two methods init() and close(). In many cases, it is enough to initialize and shutdown instance of the class. In Manageable we can find additionally two methods: start() and stop().

Let’s see how it will work for another class: MqListener.

LifecycleExample.java
    public static class Config {}

    public static class ConfigCreator extends CreatorBase<Config, LaunchType> {
        @Override
        public Config create(InstanceProvider instanceProvider, LaunchType launchType) {
            return new Config();
        }
    }

    public static class MqListener implements Manageable {
        @Override
        public void init() {
            //Initialize class
        }

        @Override
        public void start() {
            //Start class
        }

        @Override
        public void stop() {
            //Stop class (with ability to start it again)
        }

        @Override
        public void close() {
            //Destruct class
        }
    }

    public static class MqListenerCreator extends CreatorBase<MqListener, LaunchType> {
        @Override
        public MqListener create(InstanceProvider instanceProvider, LaunchType launchType) {
            return new MqListener();
        }
    }

    public static class App {
        public App(Config e, MqListener mqListner) {}
    }

    public static class AppCreator extends CreatorBase<App, LaunchType> {
        @Override
        public App create(InstanceProvider instanceProvider, LaunchType launchType) {
            Config config = instanceProvider.getOrCreate(Config.class);
            MqListener mqListener = instanceProvider.getOrCreate(MqListener.class);
            return new App(config, mqListener);
        }
    }

Now we can start an application from our main class:

LifecycleExample.java
    public static void main(String[] args) {
        Service service = Service.builder()
                                 .withRootCreator(new AppCreator())
                                 .withCreator(new ConfigCreator())
                                 .withCreator(new MqListenerCreator())
                                 .build();

        Runtime.getRuntime().addShutdownHook(new Thread(service::close));

        service.start();
    }

That way you have full control over application lifecycle.

Automatic creators

In the above example we were creating App instance. In such a simple case coding creator is quite a big overhead: you don’t do anything interesting there - just passing other instances to the App constructor. In such a case very helpful are automatic creators, which, as the name implies, doesn’t have to be implemented. How to use them? Below is above example rewritten with automatic creators:

AutoCreatorExample.java
package net.igsoft.sdi;

public class AutoCreatorExample {

    public static class Config {
    }

    public static class MqListener implements Manageable {
        @Override
        public void init() {
            //Initialize class
        }

        @Override
        public void start() {
            //Start class
        }

        @Override
        public void stop() {
            //Stop class (with ability to start it again)
        }

        @Override
        public void close() {
            //Destruct class
        }
    }

    public static class App {
        public App(Config e, MqListener mqListner) {
        }
    }

    public static void main(String[] args) {
        Service service = Service.builder()
                                       .withRootCreator(new AutoCreator<>(App.class))
                                       .withCreator(new AutoCreator<>(Config.class))
                                       .withCreator(new AutoCreator<>(MqListener.class))
                                       .build();

        Runtime.getRuntime().addShutdownHook(new Thread(service::close));

        service.start();
    }
}

There is much less code, but you have still simplicity and configurability of a solution. If you use auto creators you have to take into consideration that not every class can be instantiated using auto creators. Notable exception are classes with many constructors or constructors not taking only other known to the framework classes as a parameters. Also you can not pass creator parameters to automatic creators.

Parametrized creators

Sometimes we would like to reuse creators in different contexts. For example when we create MqReceiver it can be used with different topics. Of course, we can add a dependency to configuration class to MqReceiverCreator but then it will be difficult to reuse this creator in other application. That’s why creators can be parametrized.

ParametrizedCreatorExample.java
    public static class Config {
            static Config createFromFile(File file) {
                return new Config();
            }
        }

        public static class ConfigCreator extends CreatorBase<Config, ConfigCreator.Params> {
            @Override
            public Config create(InstanceProvider instanceProvider, ConfigCreator.Params params) {
                File file = params.getFile();
                return Config.createFromFile(file);
            }

            public static class Params extends ParameterBase {
                private final File file;

                public Params(File file) {
                    super(false);
                    this.file = file;
                }

                public File getFile() {
                    return file;
                }

                @Override
                public String uniqueId() {
                    return file.getName();
                }
            }
        }

On the call side we use it like this:

ParametrizedCreatorExample.java
    static class AppEnvironment extends ParameterBase {
            private final String name;

            public AppEnvironment(String name) {
                this.name = name;
            }

            public String getName() {
                return name;
            }

            @Override
            public String uniqueId() {
                return name;
            }
        }

        static class App {
            public App(Config e, MqListener mqListener) {
            }
        }

        static class AppCreator extends CreatorBase<App, AppEnvironment> {
            @Override
            public App create(InstanceProvider instanceProvider, AppEnvironment appEnvironment) {
                ConfigCreator.Params params = new ConfigCreator.Params(new File("~/config.init"));
                Config config = instanceProvider.getOrCreate(Config.class, params);
                MqListener mqListener = instanceProvider.getOrCreate(MqListener.class);

                if ("PROD".equals(appEnvironment.getName())) {
                    System.out.println("Warning! Creating PROD version of application!");
                }

                return new App(config, mqListener);
            }
        }

When definining a parameter bean it must extend ParameterBase and implement a method:

String uniqueId()

This method returns unique identifier of creator parameters - it is used to differentiate instances of target classes as it is assumed that different creator parameters creates a different instance of some specific class. For simple implementation it is possible to use method:

String concatenate(String... parts)

which just concatenates different Strings together.

Example implementation of uniqueId() method
String uniqueId() {
  return concatenate(name, surname, Integer.toString(age));
}

Creator parameter class always extends ParameterBase class. In that class, besides of uniqueId() method, is also defined property:

boolean manualStartAndStop

This is the single parameter to all creators which is always required. This property defines if the instance of some specific class should be automatically started. If it is set to 'true' then this DI micro framework will not automatically start instance of this specific class, although initialization, stoping and termination of this instance will work as usuall. It might be sometimes useful to have more control over the starting of the instance of the class. This parameter of creator is required, but you don’t have to pass it explicitly - it will be assumed that all instances should be started automatically. That’s what for is LaunchType creator parameter - it deafults to LaunchType.AUTOMATIC and is passed automatically to creators when no other explicit argument is given.

Creator default parameters

As mentioned above there is one default creator parameter type which SDI applies whenever user does not provide other defaults. But it is possible to provide default creator’s parameter explicitly. It is done when providing creators in ServiceBuilder:

ParametrizedCreatorExample.java
    public static void main(String[] args) {
        Service service = Service.builder()
                                 .withRootCreator(new AppCreator(), new AppEnvironment("PROD"))
                                 .withCreator(new ConfigCreator(), new ConfigCreatorParam(new File(".")))
                                 .withCreator(new AutoCreator<>(MqListener.class))
                                 .build();

        Runtime.getRuntime().addShutdownHook(new Thread(service::close));

        service.start();
    }

Default parameters will be applied whenever there is a need to provide parameter during instance creation and there is no explicit parameter provided.

Default creators

Each creator can provide a set of default creators which can be used to create its dependencies.

For example if MqListener creator needs for its work class MqListenerWorker, you can provide its creator in MqListener. It is accomplished by overriding method:

List<Creator<?, ?>> defaultCreators()

and returning from it instances of creators.

DefaultCreatorsExample.java
      public static class MqListenerWorker {
      }

      public static class MqListenerCreator extends CreatorBase<MqListener, LaunchType> {
          @Override
          public MqListener create(InstanceProvider instanceProvider, LaunchType params) {
              MqListenerWorker mqListenerWorker = instanceProvider.getOrCreate(MqListenerWorker.class);
              return new MqListener(mqListenerWorker);
          }

          @Override
          public List<Creator<?, ?>> defaultCreators() {
              return Lists.newArrayList(new AutoCreator<>(MqListenerWorker.class));
          }
      }

      public static class MqListener {
          public MqListener(MqListenerWorker mqListenerWorker) {
          }
      }

That way we do not have to provide above creators during Service construction. When SDI finds that there is no explicit creator, then it will take a default one. You can provide all or only some of dependant creators in defaultCreators() method.

Please notice that it is still possible to override default creator by setting different one on Service setting level.

Properties of SDI in a glance

  • SDI manages only singleton instances of classes. If you need to create a bean on every request, just use standard Java mechanism: new Request() in listening code.

  • SDI allows you to manage life cycle of application.

What are the advantages of such an approach?

  • Mild learning curve - you do not have to learn many new concepts on the start. Just leverage your Java knowledge. Well, it’s even hard to say about "curve" - above information is pretty much all in this subject.

  • Encourages writing easily testable code. To get easily testable code you should write simple constructors, (and creators take care about construction) and split your logic into construction and business logic (it’s like that by design). Of course, you still have dependency injection.

  • Does not pollute your application with annotations specific to DI framework.

  • Does not force you to create programs according to strict, but not always fitting, rules imposed by the framework.