Object Slicing and Component Design with Java.
Object-oriented systems are usually partitioned into layers of related responsibilities and only dependencies in one direction are allowed, from higher layers (more specific, less reusable) to lower ones (more general, more reusable). Classes in higher layers can extend or wrap classes in lower ones, but not the other way around. However, they can register themselves as event listeners with classes in lower levels through a generic interface. A component resides in a single layer, which can be itself divided in multiple levels of separate concerns. For example, a business component residing in the domain layer consists not only of business logic, but also of other types of functionalities, like database persistence, lifecycle callback methods or upper layer event notification. A level is created for each such type of functionality, and the component is divided into level specific classes, with those in higher levels enhancing some in the lower ones. If components live in a container of some sort, some levels will probably have to deal with container related services, and the classes in these levels may be generated. Ideally, the lower levels have to be oblivious of any enhancements in levels above them. One of the following three approaches is usually chosen to handle the layer/level dependencies:
1. Each layer/level of the system has its own types, and a conversion is needed when crossing boundaries. A lot of data copying is going on, and logic is duplicated. 2. A class in a higher layer/level enhances one in a layer/level below by using composition. If relationship maintenance is needed, it may have to be duplicated at the wrapper level. 3. A class in a higher layer/level enhances one in a layer/level below through extension. But upcasting does not prevent a user of objects of the base type to downcast them to the derived type and use the enhancements, so additional work is needed if true obliviousness of the enhancements is desired. Also, if the derived class overrides methods from the base class, the lower layer will get access to functionality or data representations that were designed for the above layer.
While each approach has its advantages and disadvantages, the third one seems to be the choice in a lot of JDO and EJB implementations. Most of the experts advocate using inheritance for enhancing business objects with persistence logic by using a two level domain design. However, this approach does not address the overridden methods issue (which in many cases could be acceptable). This is going to be
addressed by presenting a simple technique that achieves obliviousness to both kinds of enhancements allowed by inheritance: new and overridden methods. Object Slicing : Object slicing is defined as the conversion of an object into something with less information (typically a superclass). In C++ it occurs when an object is passed by value and copying the parameter value results in an upcast and it is considered a bad thing, as it may result in very subtle bugs. Object slicing throws away information. But in some situations this may be exactly what we want. In Java, objects are passed by reference, and all methods are virtual, so object slicing will not occur inadvertently. Even if you upcast an object, its real type is not forgotten, that's the definition of polymorphism. Let’s consider the simplest version of object slicing. Suppose class Derived extends class Base, and we have an object of type Derived. We want to "extract" the base part from it. This does not work, because the compiler will make sure you put a dot after super, followed by a valid identifier.
Also, it does not help to return a reference to this from the Base class (which is equivalent to an upcast), because this is polymorphic. Overridden methods will be invoked instead of those defined in the Base class, and any additional methods would also be available for invocation. That is exactly what we want to avoid. We want a proxy to the base type part of an object, not just to restrict the interface, and not just a copy of the data and behavior.
We could create a proxy to a base type of an object this way: for a subclass of that base type where a method is overridden, we also add a (final) method invoking the overridden method using super. Then we create a wrapper to forward the calls to the right methods. But this is not elegant, and it requires keeping track of too much information. The object oriented way of doing it is using inner classes. Also, we cannot use dynamic proxies to avoid implementing each method from the base class, because we cannot invoke a method with reflection on super. How to do Object Slicing in Java : Let's recall that a (non-static) inner class needs an instance of the enclosing class in order to be instantiated, and all members of the enclosing object (including those with private access) are accessible from the inner class.
Our idea is to exploit this feature and the fact that unlike this, super is not polymorphic. In the Derived class, extending Base, we define an inner class Slice, which also extends Base and overrides all the methods in Base by forwarding them to the base part of the enclosing object. The getBase() method in the Derived class returns an instance of Slice, which in a way is a proxy for the base part of the object. When thinking of object handles in Java, the analogy with a remote control (handle) for a TV set (object) quite helpful. With this terminology, we want to produce a second remote control that would work only with a part of the TV set, let’s say the sound.
The advantage of doing so is that, we can use composition instead of inheritance by making the Derived class hold a reference of type Base, which would be initialized in a constructor or assigned by using a setter method. This is not always a good idea. Imagine that we have a class Human that needs to be enhanced by the classes Male and Female. It does not make sense to require that Male and Female instances are built out of preexisting Human instances. We have a genuine IS-A relationship here, and the Male and Female classes should extend the Human class. And if we want to disallow discrimination based on type in one of our layers (i.e. we want to work only with Human types), we need to excise the extra information or behavior modifications introduced by the subclasses. What is usually done in order to accommodate this need is to copy the relevant data from a Male or Female object into a new object of type Human. A proliferation of layer specific types occurs when this is done, and a conversion is needed when crossing a layer boundary. Persistence Logic and Domain Design : The possible need for object slicing is very much when working with Domain and Data Access Objects. These contain the business and persistence logic, respectively. Although one could combine them in the same class as it is described in the Active Record pattern, or in many EJB books, in most cases it is not acceptable to do so. These are the most common ways of dealing with the relationship between the business logic and the persistence logic:
1. One class approach: put them in the same class 2. Service oriented or façade approach: put them in 2 different classes and pass one as an argument to a method of the other 3. Component oriented approach: put them in 3 classes
Each choice has its advantages and disadvantages. Of all there is an advantage for a persistent instance for each domain instance because it offers more options (for example we can use the persistent instance directly or drive it from a BMP EJB).
Final Remarks:
* In a way we are also using composition, as an inner class has a reference to its outer object. But the use of the inner class allows making a call to the super of the outer object in a very clean way. If you want to do this with a wrapper you would have to add methods to the wrapped class in which you would call the overridden methods (this is polymorphic, super is not). * We can use a variation of this idiom, where the proxy slice is initialized with the outer object's data (using a constructor, or an instance initializer, perhaps with reflection). This data can be used as temporary in-memory storage of changes, before committing them to the outer object. Or we can implement undo operations this way.