REST based microservice architecture is a current hype in architecture. I am therefore not yet clear with it from the point of architecture.
I'm not clear with it
- The good
- It reduces the complexity in the code artifacts for a simpler maintenance.
- The bad
- It increases complexity of governance of the software.
- The ugly
- It moves the complexity of the software to the network.
The ugly
11 years ago David Wheeler died. Wheeler formulated one of the most important phrases for software architectures:
All problems in computer science can be solved by another level of indirection, except of course for the problem of too many indirections.
I must ask myself: what are the benefits to add the network as a layer of indirections? Handling software with networks is not trivial. The network might be not available or under heavy load. Timeouts are commonplace and many developers have never heard of the Circuit Breaker design pattern.
The bad
Governance of software is on of the most underestimated factor. In bigger systems you can't do what you want. Rules have introduced, established and must be respected. Without these rules a growing software system will collapse after a while - or maintenance costs will increase dramatically.
REST based microservice architectures are related to SOA systems. And SOA without a strong governance will create an epic cluster fuck also.
Fine grained microservices will increase the complexity of the governance. The diagrams to visualize are increasingly confusing. And we need visualizations to get an overview and understanding of the system. And as we know, a process diagram with more than 7 boxes/tasks/whatever has the tendency to create chaos in our head.
The good
Software artifacts gets smaller. A microservice implementation should not have more than a few hundred lines of code - howsoever you measure it. This is good for refactoring or throwing the code away and write it new from scratch.
But I'm not quite sure that this is really a benefit. Do you really rewrite an implementation? In normal cases it is not a very complex tasks to refactor a code system with a few hundred lines of code. The benefit of rewriting comes in when you switch the technology stack. With microservices this is not a huge change-it-all tasks but you can change one service after the other.
More often than rewriting or refactoring a microservice (that works) is to refactor the entire system. Here REST based microservice based only on JSON can become a pain in the ass. Refactoring a not typed software system is a mess. Also the communicating of code intention without a type system. Without a type system also design and governance (see: The bad) can become very problematic.
Although some advocates of Microservice architectures do not want to read: exports not only JSON, but also XML hedged with XSD or any other technology that simple adds type safety.
Find the whole code for the proof of concept on Github
A proof of concept to use the incredible JCommander API from Cédric Beust together with CDI in a Java SE environment.
The goal was to use the commands API with the @Parameters
annotation together with CDIs natural plugin API Instance
.
This means, a lot of sub commands with different parameters can be executed by a main command from command line. The parameters of the main command should be injected to the sub command implementation. The sub command can have own, different parameters.
An example for such command system is git.
Example
git -c author=sascha.kohlmann@example.com commit -am "a commit"
Implementation
The main command configuration
The class with the main
method must contain @Produces
annotated method which returns a static variable with the only MainConfiguration
instance.
public class Application { private final static MainConfiguration MAIN_CONFIG = new MainConfiguration(); @Produces @Config private MainConfiguration configuration() { return MAIN_CONFIG; } public static final void main(final String... args) { // do something } }
The main
method contains only the initialization of the Weld-SE container and runs the application.
public static final void main(final String... args) { final Weld weld = new Weld(); try (final WeldContainer container = weld.initialize()) { result = container.select(Application.class).get().run(args); } } String run(final String... args) { // the application }
MainConfiguration
The MainConfiguration
class is a straight forward data holder. The configuration field are annotated with @Parameter
.
public class MainConfiguration { @Parameter(names = "-m") private String main; public String getMain() { return main; } }
The sub commands
Sub commands implements a common command API like the following:
public interface Command { String execute(); }
Implementations of the command must have at least the @Parameters
type annotation with the names
attribute given. They may have sub command specific parameters and the parameter data of the main command.
@Parameters(commandDescription = "Simple sub command", commandNames = "subcmd") public class SubCommand implements Command { @Inject @Config private MainConfiguration mainConfig; @Parameter(names = "-p") private String parameter; @Override public String execute() { return this.mainConfig.getMain() + this.parameter; } }
After the CDI initialization all sub command with the injection point for the MainConfiguration
contains now the static instance from the main class. But the configuration is yet not initialized. So having a method with @PostConstruct
annotated, using the main configuration will not work. Also using the sub command specific parameters in such a method will not work.
The JCommander initialization follows in the next step. But before, we must enhance the Application
class with the CDI plugin API.
Instance<Command>
The Application
class gets an injectable Ìnstance<Command>
field. This field will be filled by CDI after the Weld initialization with all available Command
implementations.
public class Application { @Inject private Instance<Command> commands; public static final void main(final String... args) { // do something } }
JCommander initialization
The run
method initialize the JCommander with the following steps:
- Create a new JCommander instance with the static
MAIN_CONFIG
:JCommander jc = new JCommander(MAIN_CONFIG);
- Add all instances of
private Instance
to the JCommander instance:commands this.commands.forEach(cmd -> jc.addCommand(cmd));
Afterwards, parse the command line arguments with the JCommander instance.
jc.parse(args);
That's it.
Execute the sub command
The last step is now to get the parsed sub command name an fetch the sub command implementation from JCommander. Then call the execute()
method.
final String parsedCommand = jc.getParsedCommand(); for (final Map.EntrycmdEntry : jc.getCommands().entrySet()) { final String name = cmdEntry.getKey(); if (name != null && name.equals(parsedCommand)) { ((Command) cmdEntry.getValue().getObjects().get(0)).execute(); } }
Testing
Testing ist straight forward:
@Test public void test_sub_command_execution() { Application.main("-m", "main", "subcmd", "-p", "sub"); assertThat(Application.result, is(equalTo("mainsub"))); }
Conclusion
Using CDI and JCommander with complex commands in a Java SE environment is quite simple. Using the CDI natural plugin API (Instance
) is also very simple. Together this is a strong duo to simplify the development of Java command line tools.
Find the whole code of the proof of concept on Github.