I have code that repeats a lot, but I don't know how to refactor it properly.
I have a class Foo
, it parses the network messages/data it receives from a socket and calls the corresponding onEvent()
methods. Foo
is purely a parser of network messages, it has no logic as to what action to take for the events it receives. Whoever wants to add such logic must subclass Foo
and override the onEvent()
methods.
abstract class Foo {
void processNetwotkMessage(String message) {
...
onEvent1(arg1, arg2, arg3)
reutn;
...
onEvent2(arg4);
return;
...
onEvent3()
return;
...
onEvent999(arg1337);
}
abstract protected void onEvent1(Arg1 arg1, Arg2 arg2, Arg3 arg3);
abstract protected void onEvent2(Arg4 arg4);
abstract protected void onEvent3();
...
abstract protected void onEvent999(Arg1337 arg1337);
}
Now, my program is supposed to be modular, I have many separate module classes that want to receive those events and process them. The modules implement Plugin
interface. The interface matches the onEvent()
methods from Foo
, but adds PluginContext ctx
as the first argument.
interface Plugin {
void onEvent1(PluginContext ctx, Arg1 arg1, Arg2 arg2, Arg3 arg3);
void onEvent2(PluginContext ctx, Arg4 arg4);
void onEvent3(PluginContext ctx);
...
void onEvent999(PluginContext ctx, Arg1337 arg1337);
}
And now, to dispatch the events to the modules, I creating a module-aware subclass of Foo
called PluginSupporingFoo
.
class PluginSupporingFoo extends Foo implements PluginContext {
List<Plugin> plugins;
@Override
protected void onEvent1(Arg1 arg1, Arg2 arg2, Arg3 arg3) {
synchronized (plugins) {
for (Plugin p : plugins) {
p.onEvent1(this, arg1, arg2, arg3);
}
}
}
@Override
protected void onEvent2(Arg4 arg4) {
synchronized (plugins) {
for (Plugin p : plugins) {
if (PluginIsAllowedToBeAwareOfThisEvent(p, arg4)) {
p.onEvent2(this, arg4);
}
}
}
}
@Override
protected void onEvent3() {
synchronized (plugins) {
for (Plugin p : plugins) {
p.onEvent3(this);
}
}
}
...
@Override
protected void onEvent999(Arg1337 arg1337) {
synchronized (plugins) {
for (Plugin p : plugins) {
p.onEvent999(this, arg1337);
}
}
}
}
As you can see, whenever Foo
calls one of onEvent()
methods, the overrided method from PluginSupporingFoo
gets called and then it dispatches this event to all of modules by calling the corresponding onEvent()
method of Plugin
interface, with one extra argument added -- PluginContext ctx
. Sometimes there is also a condition whether to tell a module about an event or not, like you can see in PluginSupporingFoo.onEvent2()
.
Now, there is a lot of code duplication going on that I would like to remove.
First off, Plugin
interface and Foo
class have almost identical methods. In fact, Plugin
interface needs to have all onEvent
methods Foo
has but with PluginContext ctx
as the an extra first argument.
Another code duplication is in PluginSupporingFoo
. All onEvent()
methods are pretty-much a copy of each other:
.
protected void on${EventName} ( ${ArgList} ) {
synchronized (plugins) {
for (Plugin p : plugins) {
${ OPTIONAL: if (filter(p, ${ArgList}.arg1)) { }
p.on{EventName}(this, ${ArgList}.allArgs);
${ OPTIONAL: } }
}
}
}
And given that there are many many onEvent
methods, it's frustrating to have so much copy-paste code and it would be hard to modify them all if needed.
Wow... this is a poor design to have eventXX( foo, bar, baz)
etc because each time you add a new event you must add a corresponding listener method.
Perhaps a better design would be to refactor this so that your Foo
class only has a few or ideally one onEvent()
method that takes a new Event
interface
public class Foo{
void onEvent(Event e){ ... }
}
public interface Event{
Object[] getArgs();
//other Event specific methods
...
}
Then each of the eventXX
methods would be a new implementation of the Event
interface.
public class Event2 implements Event{
public Object[] getArgs(){
//Arg4 like in your code
return new Object[]{ new Arg4() };
}
}
the Plugin could could would also similarly only have 1 method
interface Plugin{
onEvent(PluginContext ctx, Event e);
}
Now whenever you need to add a new Event it's just a new Event implementation and these interfaces don't need any extra methods.
Handlers can check the type of Event or you can make EventType
or other kind of discriminator however you wish.
class MyPlugin implements Plugin{
public void onEvent(PluginContext ctx, Event e){
//this is only useful if we only care about a few types
if( e instanceOf Event2){
//we know this is Arg4
Arg4 arg4 = (Arg4) e.getArgs()[0];
...
}
}
}
Now with Java Lambdas we we could even have a handler Map<Class<? extends Event>, Function>
if you wanted to get fancy.