Some leaky abstractions are sneaky, they are not visible right away. At my current assignment we are thinking about refactoring some co-located Services so only one single Service per machine exists. One way to achieve this is to “remote” the Services using RMI. Since the service is already an interface and configured in an IOC container you would think it is ready to change the underlying implementation; from co-locating to remoting. However I came across some sneaky leaky abstractions that caused the refactoring more time consuming than I expected.
Let’s first start with a simplified version of the service interface. Currently the service calls a SOAP service using Axis.
// service interface MyService { MyResult findSomething() } //returned class public class MyResult { //enum type private MyDomainType type; } //the enum enum MyDomainType { static MyDomainType convert(GenerateAxisClass other) {...} }
1. Static factory methods on domain class
For our domain we are converting between generated Axis classes and our own domain, a sort of anticorruption layer so to speak. For conversion you can choose between several patterns, the one I use and see the most are a separate factory (or converter) classes and static factory methods on the class it self.
The static factory method is shown above, a separate factory may look something like this:
public class MyDomainTypeFactory { public MyDomainType convertFrom(OtherDomain other) {...} }
Now although both implementations seem okay, the static method approach actually makes your client dependent on the underlying implementation, Axis in this case. Since the MyDomainType is eventually returned by the service, you have now exposed the underlying implementation to your client; the client now also needs the Axis generated classes and is therefor kind of dependent on Axis.
2. Not using Serializable
Maybe this one is on the edge but the fact that our domain/dto classes are not implementing Serializable excluced the possibility to use something like rmi remoting. We first need to change all dto/domain classes so they implement Serializable.
So I suggest that if you already now that your domain objects will be returned from a Service and are therefor some kind of dto’s it is good practice to already let them implement Serializable. It is not a lot of work to let the classes implement Serializable, and saves you a hell of a lot of time updating all client modules if you are going to choose remoting.
3. Nested and unchecked exceptions
There have been tons of discussions on the Internet about checked versus unchecked exceptions. I like to use unchecked exceptions for various reasons. But using unchecked and also nested exceptions brings great responsibility; especially when implementing a service layer.
Let me illustrate this with an example:
class HibernateDao { void persist(Person transient) throws HibernateException{} } interface PersonCreateService { Person create(....); } class PersonCreateServiceImpl implements PersonCreateService{ Person create(....) { //.. hibernateDao.persist(..); } }
The PersonCreateServiceImpl is not obliged to catch the HibernateException. So if something goes wrong in Hibernate the HibernateException will propagate all the way up to the client. Since the client is usually unaware of the implementation details of a Service and might not have a dependency on Hibernate on its own, this can result in a ClassNotFoundException at runtime.
The same goes for nested exceptions. Consider the previous example with a refactored PersonCreateService and PersonCreateServiceImpl.
class HibernateDao { void persist(Person transient) throws HibernateException{} } interface PersonCreateService { Person create(....) throws CannotCreatePersonException; } class PersonCreateServiceImpl implements PersonCreateService{ Person create(....) throws CannotCreatePersonException { try { hibernateDao.persist(..); } catch(HibernateException e) { throw new CannotCreatePersonException("...", e); } } }
This also causes the same hidden runtime dependency on Hibernate for the calling client.
Conclusion
So if you are implementing a Service then make sure you don’t expose any implementation details.
- If you are going to return domain/dto classes from a Service then don’t put any conversion methods that make your domain/dto classes dependent on a specific implementation.
- Let your dto/domain classes implement Serializable if you are going to return them from a Service. Although this may sound like over designing your Service in the beginning it actually keeps all options, like remoting, open as long as possible. This enables the lean principle Decide as late as possible.
- Catch all expected RuntimeExceptions from your underlying implementations (like Hibernate or Spring and so on) in your Service . Don’t wrap them in your own (Runtime)Exception, but log them and throw a new NamedException to the client.