Proxies everywhere
Do it carefully, but not so afraid of that
In one of previous articles we discussed how we can provide a generic caching mechanism for Retrofit calls. Check it out, if you interest. Basically we’ve made a proxy object, that wrapped created by Retrofit object and implemented api’s interface and handled special @WithCache
annotations.
In other more old article we used a proxy to provide a declarative analytics for application. Click here, if you like to know more.
Today we will talk about other usages of java proxies. But will make a small remark before we dive into samples. This article doesn't intend to use Proxy object whenever you can, we will just to try find interesting ways of solving common (at least in my experience) problems with usage of Proxy. It is one of alternatives. Not the primary solution.
Ready? Let’s go.
Little bit about Proxy
Java’s Proxy
is a class that is a parent of all proxy instances and provides to user a number of static methods to create its instances based on interfaces you want to implement and a handler, that we intercept all calls, giving to you the ability to control function calls and provide additional or fully new logic.
More you can read here about java’s Proxy
and here about Proxy pattern.
Logs
One of the needed features for every developers is ability to understand, why different crashes happens. Logging user interaction with application and its responses to the client is one of the key for fast identifying the problem. If you have some variant of MVP, you can wrap all your View interfaces and Presenter interfaces (if you have these) with logs, so you will have at least context, what user and application have done before crash or non fatal error happened.
I don’t want to write every time Log.d/i/e
in the beginning of each function, because I will need to remember that, if new method will appear. Moreover I will need to repeat same action again and again, which is pretty boring. Also, there is a great library Hugo provided by Jake Warton. Be sure to check it out. But’s I don’t want to mark needed function either. I just want to wrap every interface function with measuring its time complexity and logging its arguments.
So let’s apply our knowledges about Proxies and write our implementation of InvocationHandler
. The basic algorithm is next:
- Log a method information (class name, method name, arguments) before the method invocation
- Wrap method invocation with
measureNanoTime
call, to know, what precise amount of nano seconds was used to run it - Format that value at log it with the answer of the method
I’ve used Timber
for logs, but you can use whatever you want, even your own interface and give possibility for client of this class to choose its own implementation.
The shown implementation is pretty straightforward. Just need to clarify one thing about withMeasureNs
method. It uses special measureNs
method, defined bu us, which just wraps kotlin’s measureNanoTime
with contract:
@UseExperimental(ExperimentalContracts::class)
inline fun measureNs(block: () -> Unit): Long {
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
return measureNanoTime(block)
}
Thus, we able to make an assignment inside this method.
One slightest thing. Let’s add some convenient methods for wrapping:
Now, let’s see its usage. Suppose we have an interface Abstraction
with two methods: one returns something, other just a procedure (no return value). This interface will have two implementions: the simple one and with some long computation:
Let’s write our test:
As we wrapped implementations with Logging Proxy, we will be able to see next results:
Dummies
We don’t want to use null
. Okay, some of us use it, and happy we it. All depends on a situation. But often we just want to have some stupid fake implementation that will express default behaviour.
The example is some feature service. Suppose you have a chat inside the application and you have ChatService
instance that informs you about chat availability by calling isChatAvailable()
.
So in ordinary case you will have something like:
If you have many services especially which can return other instances of other interfaces, this can became a problem, since for each such interface you will need to identify a Dummy instance.
So let’s try to use proxy for such purpose. Again, this is not silverbullet solution. Moreover it is more fragile solution, than explicitly add a Dummy class for each case, because we moving type checks from compile time period to runtime period, which is more dangerous, since you more likely to ship broken version to the user and, thus, it becoming more expensive to fix that. So I warned you. :)
The basic algorithm is next:
- We define a
InvocationHandler
implementation that checks return type of method and in case of procedure (Unit
return type) returnsUnit
. - In case of other input type it checks whether it has default value for such type or not
- The default values defined in special
Defaults
class
Now let’s see, how it looks like:
For the purpose of example Defaults
contains only info about basic default values, but you can define your own default values for custom types, for instance, by adding a property and a method inside:
val customMap = mutableMapOf<Class<*>, Any>()
inline fun <reified T : Any> custom(value: T) = customMap.put(T::class.java, value)
And provide a check inside handler:
if (method.returnType in defaults.customMap) return defaults.customMap[method.returnType]
Now let’s see, how it works. Let’s provide a small logging test to see, what’s happening. We will describe an small chat system. We have a ChatService
, which can say wether the chat available or not, by some condition; which can provide the history of Message
instances and which gives you the ability to subscribe on new Message
is coming:
Let’s look on our test:
And results:
Afterwards
Combining those two examples with my examples from previous articles (generic cache for Retrofit and declarative analytic) we have 4 examples of using java Proxies in real life.
Remember. Proxies are not a all time solution, but it is hacky approach, which is used time to time and which you need to apply with great caution. But you will decide to use it, now you have an examples :)