The task at hand is to create a part of my Java web application which will allow me to easily execute small pieces of codes in a compositionnal manner. The task at hand is to allow the user to compose "actions" in any order. What I'm struggling with is passing parameters to my actions.
All starts with the action interface :
public interface Action {
void resolve(Context context);
}
When the action is resolved, it's code is executed. The code can be anything : calling a method in Java, executing some Javascript...
Here, the "context" is the problem for me. Each action executes within a specific context. The idea is that the user that creates the action can specify which object to retrieve from the concept, for example the user that is resolving the current action, or other objects specified in the action's specific interface.
Example, let's look at this action :
public final class ActionScript implements Action {
private final Parameters parameters;
private final String methodName;
private final ScriptsLibrary library;
public ActionScript(ScriptsLibrary library, String methodName, Parameters parameters) {
this.parameters = parameters;
this.library = library;
this.methodName = methodName;
}
@Override
public void resolve(Context context) {
try {
((Invocable) library.getEngine()).invokeFunction(methodName, context);
} catch (ScriptException | NoSuchMethodException ex) {
throw new RuntimeException(ex);
}
}
}
It's a simple wrapper calling an action in Javascript using Nashorn. The parameters can be anything : objects with specific ids in the DB, int/String/boolean values configured by the user...
The code from an action can look like this :
public class DummyAction implements Action {
@Override
public void resolve(Context context) {
User userExecutingTheAction = context.get("ExecutingUser");
System.out.println("User " + userExecutingTheAction);
}
}
So the action can retrieve runtime parameters (ex: user that executes the action...) and static parameters (created when the action is loaded - from a config file for instance), and all that from the concept. Besides, the user can for example specify references to object in the parameters that will be injected at runtime.
Actions can also be nested/decorated in order to achieve full compositionnality. For instance :
public class DummyWrapperAction implements Action {
private final Action wrappedAction;
public DummyWrapperAction(Action wrappedAction) {
this.wrappedAction = wrappedAction;
}
@Override
public void resolve(Context context) {
System.out.println("Before");
wrappedAction.resolve(context);
System.out.println("After");
}
}
This allows actions to be created easily :
// executes specific action 1 or specific action 2 based on a condition
Action myAction = new LoggingAction(new ConditionalAction(new Condition(3), new SpecificAction1(), new SpecificAction2()));
Knowing all this, what is the cleanest well to design the context class ? Should it be splitted into several elements ? The struggle is to inject everything needed to the class at runtime and be sure to not conflict with potential wrapped actions.
The Context basic implementation is responsible for :
I feel it is doing too much. The design is affected by the Concept classes owning much responsabilities and their should be fragmented (right now every part of the application is linked to the concept). But how ? Here's what I try to achieve in clean coding :
In a true object-oriented way, and method oriented, how to solve this specific design problem ?
edit : removed public declarator in method in interface
edit 2 : I had a lot of interesting solutions, but the one that made the more sense to me was the one in which each action is parametrized with a specific kind of Context. I reorganized things as such :
I'd made the Action
interface generic on its Context
:
public interface Action<C extends Context> {
void resolve(C context); // no need to use 'public' modifier here
// interface methods are always public
}
And then, Context
could be either a marker interface, an interface that enforces a contract, or an abstract class with default method implementations:
public interface Context {
User get(String user);
// other default methods here
}
Then, you could do:
public class LogUserContext implements Context {
@Override
public User get(String user) {
// materialize user here
}
}
And an Action
that logs the user in could be:
public class LogUserAction implements Action<LogUserContext> {
@Override
public void resolve(LogUserContext context) {
User user = context.get("theUser");
// log the user in
}
}
For a wrapped Action
, I'd use a WrapperContext
context:
public interface WrapperContext extends Context {
Context getWrappedContext();
}
Implementation:
public class DummyWrappedContext implements WrapperContext {
private final Context wrappedContext;
public DummyWrapperContext(Context wrappedContext) {
this.wrappedContext = wrappedContext;
}
@Override
public Context getWrappedContext() {
return this.wrappedContext;
}
// TODO other methods from Context, etc.
}
So now your DummyWrapperAction
could be as follows:
public class DummyWrapperAction implements Action<WrapperContext> {
private final Action wrappedAction;
public DummyWrapperAction(Action wrappedAction) {
this.wrappedAction = wrappedAction;
}
@Override
public void resolve(WrapperContext context) {
System.out.println("Before");
Context wrappedContext = context.getWrappedContext();
wrappedAction.resolve(wrappedContext);
System.out.println("After");
}
}
The idea is to also wrap contexts. You can enhance this design by allowing to chain or decorate contexts, and by using abstract classes that would be in charged of the tasks common to all contexts.
Interpreter GoF pattern, interpreted tree can be externalized in some way. You can decorate any node, and will respond to your request nicely. One more thing: action interface should have two methods at least if the representation is external (i.e. made by user): one to check for validity of the tree (e.g. a node requiring two children must have exactly those children there) and one for executing the tree.
Edit: here's an example
import java.util.*;
//context can be further complicated if you want visibility blocks (i.e.you need a stack of contexts)
class Context {
private Map<String, Object> variables;
Context() {
variables = new HashMap<java.lang.String, Object>();
}
//this is to retrieve stuff from your context (variables)
Object getVariable(String name) { return variables.get(name); }
// put here the shared structs
void setVariable(String name, Object value) { variables.put(name, value); }
}
interface Action<T> {
void verify(Context ctx);
void execute(Context ctx);
T getData();
void setData(T t);
List<Action<?>> getChildren();
void addChild(Action<?> action);
}
// we offer some default impl, but we keep it abstract
// note on design: you can split (derive) this in Terminal / Non-terminal nodes (careful at the above interface)
// however, it's not in my objective to follow the pattern to the letter, but to explain how the things can be done
// plus, I need to kepp this short, it's long enough
abstract class BaseAction<T> implements Action<T> {
private List<Action<?>> children;
private T data;
public BaseAction() {
children = new LinkedList<Action<?>>();
}
@Override
public void verify(Context ctx) {
for(Action<?> a : children) {
a.verify(ctx);
}
}
@Override
public void execute(Context ctx) {
for(Action<?> a : children) {
a.execute(ctx);
}
}
@Override
public T getData() {
return data;
}
@Override
public void setData(T t) {
this.data = t;
}
@Override
public List<Action<?>> getChildren() {
return Collections.unmodifiableList(children);
}
@Override
public void addChild(Action<?> action) {
children.add(action);
}
}
class BlockAction<T> extends BaseAction<T> {} //needs further refinement, including push/pop contexts if necessary
//let's implement your Action Script, some stuff left out.
// we suppose that the action produces some string
// that's a final node
final class ActionScript extends BaseAction<String>{
private final String library;
private final String methodName;
private final String aParameter;
public ActionScript(final String library, final String methodName, final String aParameter) {
this.library = library;
this.methodName = methodName;
this.aParameter = aParameter;
}
@Override
public void verify(Context ctx) {
if(!getChildren().isEmpty()) {
throw new RuntimeException("Terminal node with children ?!?");
}
}
@Override
public void execute(Context ctx) {
//do whatever here (your code)
String paramValue = (String) ctx.getVariable(aParameter);
setData(library + "." + methodName + "(" + paramValue + ")");
}
}
// this can be further complicated, i.e. to have 2 subnodes, but for simplicity:
final class AssignmentAction<T> extends BaseAction<T> {
private String variableName;
public AssignmentAction(String variableName) {
this.variableName = variableName;
}
@Override
public void verify(Context ctx) {
if(getChildren().size() != 1) {
throw new RuntimeException(String.format("= node with %d children ?!?", getChildren().size()));
}
super.verify(ctx);
}
@Override
public void execute(Context ctx) {
@SuppressWarnings("unchecked")
Action<T> child = (Action<T>) getChildren().get(0);
child.execute(ctx);
ctx.setVariable(variableName, child.getData());
}
}
public class IP {
public static void main(String []args) {
Context ctx = new Context();
ctx.setVariable("inputVar", "Hello world!");
Action<String> root = new BlockAction<String>();
root.addChild(new AssignmentAction<String>("var"));
root.getChildren().get(0).addChild(new ActionScript("myLib", "foo", "inputVar"));
root.verify(ctx);
root.execute(ctx);
System.out.println(ctx.getVariable("var"));
}
}
Hope it's clear now.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With