Dynamic subtyping in Java
Join the DZone community and get the full member experience.
Join For FreeSubtyping Issues in Java
Behavior customization is a common requirement for large software systems. A software product cannot fully satisfy requirements of all customers; there are always differences that must be implemented, which are specific to each customer. Thus, software products must be flexible enough to allow these customizations in behavior even without changes in product's source code.In Java-based software products, a solution for such problems is usually based on a principle called subtyping. Subtyping can be defined on an example of two types A and B: if type B is a subtype of type A, then all code that operates correctly on objects of type A will operate correctly on objects of type B. Naturally, a software product behavior can be customized by replacing some default class A with custom class B where B is a subtype of A.
If subtyping is based on an interface, then implementations of the interface can be independent of each other, which decreases complexity of the system and benefits modularity. At the same time such independence limits code reusability. A custom implementation can use Delegation pattern to call methods, but it is impossible to override a method in the base class.
Another approach for subtyping is direct subclassing of the implementation class. Subclassing allows method overriding in the base class and also introduces design-time dependencies on that class. These dependencies limit subclass reusability.
As you can see standard Java subtyping techniques limit reusability of either a base class or a subclass.
Dynamic subtyping
The approach described below combines the advantages of standard Java subtyping techniques. It allows creation of a new class, which can override methods in an original class, but at the same time, is dependent only on the interface of the original.Let's start with the following example:
Suppose we have an interface Action as described below and its implementation in form of class ActionImpl.
//This is an interface of Action
public interface Action{
void doAction1();
void doAction2();
void doAll();
}
//This is a default implementation of the interface Action
class ActionImpl implements Action{
public void doAction1(){ ... }
public void doAction2(){ ... }
public void doAll(){
doAction1();
doAction2();
}
}
Class ActionImpl implements all methods of the interface and is used as a default implementation.
Now let's suppose we want to override method doAction2 in the ActionImpl in such a way that the new class won't depend on ActionImpl.
Let's create a new class ActionExtension as the following:
//This is a custom implementation that overrides one method.This class extends a simple base class which looks like this:
abstract class ActionExtension extends ImplementationOf<Action> implements Action{
public void doAction2(){
…
Super.doAction2();
}
}
//This is base class for dynamic subclassesNote that ActionExtension class is an abstract class, so we don't have to define all methods, just those that we want to override in ActionImpl.
public abstract class ImplementationOf<T>{
/**
* A field for calling super class methods.
* Should be used only in expressions where super keyword can be used.
* */
protected final T Super = null;
}
That's all we need to do at design time. Now let's look how it works at run-time.
At the point of code where instance of Action is expected we need to create a new class:
//Fragment of code that creates dynamic subclass of ActionImplThe created class is a subclass of ActionImpl and contains all methods copied from ActionExtension class and modified in such a way that all calls to methods of Super field are directed to ActionImpl class. This class can be used to create an instance which can be used instead of instance of ActionImpl class:
Class dynamicSubclass = DynamicClassExtender.extend(ActionImpl.class, Action.class, ActionExtension.class);
Action action = (Action) dynamicSubclass.newInstance();In the real product the code above should be integrated into a dependency injection framework of your choice, and actual mapping of extension classes to implementations should be configurable.
Dynamic class extender
From the previous chapter we saw that all the complexities of dynamic subclassing are hidden in DynamicClassExtender class, which uses a byte code manipulation library to create a direct subclass of ActionImpl from ActionExtension class. In order to see what kind of byte code manipulation is involved let's start with ActionExtender class decompiled using standard javap utility:public abstract class ActionExtension extends ImplementationOf implements Action{Now if we manually create a class that extends ActionImpl and overrides doAction2 method and then decompile it with javap we'll see the following byte code:
public ActionExtension();
Code:
0: aload_0
1: invokespecial #10; //Method ImplementationOf."<init>":()V
4: return
public void doAction2();
Code:
0: aload_0
1: getfield #17; //Field Super:Ljava/lang/Object;
4: checkcast #5; //class Action
7: invokeinterface #21, 1; //InterfaceMethodAction.doAction2:()V
12: return
}
public class ActionExtension$ActionImpl extends ActionImpl{After comparing byte codes from both classes we can figure out a list of manipulations that DynamicClassExtender must apply on ActionExtension class in order to create a dynamic subclass of ActionImpl:
public ActionExtension$ActionImpl();
Code:
0: aload_0
1: invokespecial #8; //Method ActionImpl."<init>":()V
4: return
public void doAction2();
Code:
0: aload_0
1: invokespecial #15; //Method ActionImpl.doAction2:()V
4: return
}
- change class name to ActionExtension$ActionImpl
- change access attribute to public, remove abstract attribute
- change super class name to ActionImpl
- replace references to ImplementationOf with references to ActionImpl
- replace method invocations on Super field (instructions 1,4,7 in ActionExtension.doAction2) with method invocations in super class (instruction 1 in ActionExtension$ActionImpl.doAction2)
Conclusion
The dynamic approach for subtyping, presented above, may help to create customizable Java applications without sharing implementation source code and without sacrificing code reusability.Opinions expressed by DZone contributors are their own.
Comments