FriendPackages
From APIDesign
(→Friend Packages) |
|||
Line 1: | Line 1: | ||
Common [[wikipedia::Object-oriented_programming_language|object oriented languages]] offer some kind of [[encapsulation]], which often takes form of having '''public''', '''protected''' and '''private''' access modifiers. Indeed, designers soon found that this is not enough and as such [[C++]] has '''friend''' keyword and [[Java]] adds additional ''package private'' access type. | Common [[wikipedia::Object-oriented_programming_language|object oriented languages]] offer some kind of [[encapsulation]], which often takes form of having '''public''', '''protected''' and '''private''' access modifiers. Indeed, designers soon found that this is not enough and as such [[C++]] has '''friend''' keyword and [[Java]] adds additional ''package private'' access type. | ||
- | The problem with the [[Java]] approach is that it generally dictates the files layout of your friends. Their sources need to be placed in the same directory. Many people do not like that, as this mixes the classes representing the API with the classes that | + | The problem with the [[Java]] approach is that it generally dictates the files layout of your friends. Their sources need to be placed in the same directory. Many people do not like that, as this mixes the classes representing the API with the classes that provide their implementation. This is not perfect. Common wisdom and also [[Code Against Interfaces, Not Implementations|Chapter 8]] advice, that it is preferable to ''code against interfaces and not the implementation''. The [[Code Against Interfaces, Not Implementations|Chapter 8]] explains that this old good advice can have various meanings and take multiple forms. One of such forms is the practise of putting the implementation and interfaces sources in two distinct disk locations. This is exactly against the requirements of the [[Java]]'s ''package private'' access modifier, making it useless for establishing friendship between distinct API and implementation packages. |
This often leads people to open the APIs up and use '''public''' or '''protected''' modifiers, sometimes annotating the method with a javadoc note ''do not call me, I am part of the implementation''. This is unfortunate as it pollutes the APIs with useless implementation details, distracts the API users and potentially creates security holes in the API. | This often leads people to open the APIs up and use '''public''' or '''protected''' modifiers, sometimes annotating the method with a javadoc note ''do not call me, I am part of the implementation''. This is unfortunate as it pollutes the APIs with useless implementation details, distracts the API users and potentially creates security holes in the API. |
Revision as of 12:39, 24 October 2008
Common object oriented languages offer some kind of encapsulation, which often takes form of having public, protected and private access modifiers. Indeed, designers soon found that this is not enough and as such C++ has friend keyword and Java adds additional package private access type.
The problem with the Java approach is that it generally dictates the files layout of your friends. Their sources need to be placed in the same directory. Many people do not like that, as this mixes the classes representing the API with the classes that provide their implementation. This is not perfect. Common wisdom and also Chapter 8 advice, that it is preferable to code against interfaces and not the implementation. The Chapter 8 explains that this old good advice can have various meanings and take multiple forms. One of such forms is the practise of putting the implementation and interfaces sources in two distinct disk locations. This is exactly against the requirements of the Java's package private access modifier, making it useless for establishing friendship between distinct API and implementation packages.
This often leads people to open the APIs up and use public or protected modifiers, sometimes annotating the method with a javadoc note do not call me, I am part of the implementation. This is unfortunate as it pollutes the APIs with useless implementation details, distracts the API users and potentially creates security holes in the API.
One of the solutions is to add yet another modifiers to Java and introduce concept of friend packages. This may happen occasionally. Meanwhile let's learn how to use simple API design pattern that makes connecting two packages possible even now.
Friend Packages
Imagine you have an API class called Item with public getter and setter while its constructor and its addChangeListener methods are dedicated only for friends. Then just write:
Code from Item.java:
See the whole file.public final class Item { private int value; private ChangeListener listener; /** Only friends can create instances. */ Item() { } /** Anyone can change value of the item. */ public void setValue(int newValue) { value = newValue; ChangeListener l = listener; if (l != null) { l.stateChanged(new ChangeEvent(this)); } } /** Anyone can get the value of the item. */ public int getValue() { return value; } /** Only friends can listen to changes. */ void addChangeListener(ChangeListener l) { assert listener == null; listener = l; } }
This is regular Java code relying on the standard public and package private modifiers. This is exactly the code that you would write, if the friend code would reside in the same package as the API. As we want to use distinct packages, let us take another step and teleport the API to another friend package with implementation classes. This can be done by defining an accessor class in such package:
Code from Accessor.java:
See the whole file.public abstract class Accessor { private static volatile Accessor DEFAULT; public static Accessor getDefault() { Accessor a = DEFAULT; if (a == null) { throw new IllegalStateException("Something is wrong: " + a); } return a; } public static void setDefault(Accessor accessor) { if (DEFAULT != null) { throw new IllegalStateException(); } DEFAULT = accessor; } public Accessor() { } protected abstract Item newItem(); protected abstract void addChangeListener(Item item, ChangeListener l); }
This class defines an internal, friend API specifying what is the functionality that our implementation classes want to get from our API interfaces (in addition to regular access to all public elements). In this particular case it is the access to constructor via the newItem method and listener manipulation via addChangeListener method. Please note that these methods are abstract, without implementation and that this internal API is waiting for someone to register the implementation by calling the setDefault static method.
Who can provide meaningful implementation of this class? Well, only someone who can call the package private methods from the Item class. Does a class like that exist? Indeed, it does, any class in the same package as Item can do that. So just write:
Code from AccessorImpl.java:
See the whole file.final class AccessorImpl extends Accessor { protected Item newItem() { return new Item(); } protected void addChangeListener(Item item, ChangeListener l) { item.addChangeListener(l); } }
Place this non-public AccessorImpl next to the Item class, so they are friends in the classical Java sense. Now all our intrinsic classes are ready. We have the public API class Item with hooks for friends, we have the API for friends in the Accessor class and we also have a provider of this friend API. Now we need to bind this all together. As soon as the Item class is loaded, we need to register the implementation. In Java this can be done with the use of static initializer block in the API class:
Code from Item.java:
See the whole file.static { Accessor.setDefault(new AccessorImpl()); }
And this is all (especially if we want as clueless understanding as possible). We have connected two Java packages together and provided an API that allows them to perform a privileged communication. As such any code in the same package as the Accessor class can call:
Code from AccessorTest.java:
See the whole file.Item item = Accessor.getDefault().newItem(); assertNotNull("Some item is really created", item); Accessor.getDefault().addChangeListener(item, this);
Moreover, because all the methods in the Accessor class are protected and there is just one reasonable subclass, only code in this one package can make the calls, no code in other packages has any chance to access these methods. As such the solution is safe and creates a secure teleport between the API package and the implementation one, allowing us to separate interfaces and implementation as advised by Code Against Interfaces, Not Implementations wisdom.