To understand what lens is and where to use it, consider following example:
Our domain contains several entities: Payment
, Account
and Currency
.
class Currency {
String code;
// accessors here
}
class Account {
String accountNumber;
Currency currency;
// accessors here
}
class Payment {
Double amount;
Account payerAccount;
Account receiverAccount;
// accessors here
}
We have to implement a function to validate that payer account currency is not empty. A usual way to do that would look like this:
class PaymentValidator {
boolean isPayerAccountCurrencyNotEmpty(Payment payment) {
if (payment == null) {
return false;
}
Account payerAccount = payment.getPayerAccount();
if (payerAccount == null) {
return false;
}
Currency currency = payerAccount.getCurrency();
if (currency == null) {
return false;
}
return currency.getCode() != null;
}
}
Such implementation has several drawbacks:
- It is too easy to do it wrong. We have to check each property before dereference it.
- The biggest part of the method is boilerplate and only last line is a real logic.
Lens is a simple functional interface with method get
, which can extract some value of type P
from instance of
type O
.
interface ReadLens<O, P> {
P get(O object);
}
Suppose we have a instance of type ReadLens<Payment, String>
which encapsulate currency code extracting logic. Having
such lens instance we can reimplement our function. I will look like this:
class PaymentValidator {
static final ReadLens<Payment, String> PAYER_ACCOUNT_CODE_LENS = ...;
boolean isPayerAccountCurrencyNotEmpty(Payment payment) {
return PAYER_ACCOUNT_CODE_LENS.get(payment) != null;
}
}
We can construct lenses manually with combinator functions or use annotation processor to construct them at compile time.
There are several combinator functions to combine lenses with each other: endThenF
and composeF
. These functions
have analogical semantic as Function#endThen
and Function#compose
. To construct PAYER_ACCOUNT_CODE_LENS
we can do
the following:
class PaymentValidator {
static final ReadLens<Payment, String> PAYER_ACCOUNT_CODE_LENS =
Lenses.readLens(Payment::getPayerAccount)
.andThenF(Accout::getCurrency)
.andThenF(Currency::getCode);
}
Sometimes it is necessary to construct lenses by hand but most of the time we can do it automatically, at compile time.
To construct the same lens instance we can annotate our Payment
class with
GenLenses
annotations.
@GenLenses(lenses = @Lens(path = "payerAccount.currency.code"))
class Payment {
Double amount;
Account payerAccount;
Account receiverAccount;
}
PaymentLenses
factory class will be generated at compile time. It will look like this:
final class PaymentLenses {
public static final ReadLens<Payment, String> PAYER_ACCOUNT_CODE_LENS =
Lenses.readLens(Payment::getPayerAccount)
.andThen(Lenses.readLens(Accout::getCurrency))
.andThen(Lenses.readLens(Currency::getCode));
}
Now, we can use PaymentLenses#PAYER_ACCOUNT_CODE_LENS
in our code
class PaymentValidator {
boolean isPayerAccountCurrencyNotEmpty(Payment payment) {
return PaymentLenses.PAYER_ACCOUNT_CODE_LENS.get(payment) != null;
}
}
For maven-based projects, add the following to your pom.xml
file:
<!-- version property -->
<properties>
<lens4j.version>${LATEST}</lens4j.version>
</properties>
<!-- dependency for core api -->
<dependencies>
<dependency>
<groupId>dev.khbd.lens4j</groupId>
<artifactId>lens4j-core</artifactId>
<version>${lens4j.version}</version>
</dependency>
</dependencies>
<!-- processor configuration -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>dev.khbd.lens4j</groupId>
<artifactId>lens4j-processor</artifactId>
<version>${lens4j.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
We are going to support separate version for each LTS release as long as that release is supported. In the following table, you can find the latest lens4j version for each supported java version.
Java version |
Latest release |
---|---|
1.8 |
|
11 |
|
17 |
|
21 |
Lenses can be generated in different way, so called, inlined way.
Inlined generation is experimental and disabled by default.
To enable inlined generation set option lenses.generate.inlined
to true
.
For maven-based projects, add the following:
<!-- processor configuration -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>dev.khbd.lens4j</groupId>
<artifactId>lens4j-processor</artifactId>
<version>${lens4j.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<compilerArg>-Alenses.generate.inlined=true</compilerArg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
Inlined lenses are look like manually written code, so instead of such code
final class PaymentLenses {
public static final ReadLens<Payment, String> PAYER_ACCOUNT_CODE_LENS =
Lenses.readLens(Payment::getPayerAccount)
.andThen(Lenses.readLens(Accout::getCurrency))
.andThen(Lenses.readLens(Currency::getCode));
}
something like that will be generated:
final class PaymentLenses {
public static final ReadLens<Payment, String> PAYER_ACCOUNT_CODE_LENS = new ReadLens<>() {
@Override
String get(Payment object) {
if (object == null) {
return null;
}
Account payerAccount = payment.getPayerAccount();
if (payerAccount == null) {
return null;
}
Currency currency = payerAccount.getCurrency();
if (currency == null) {
return null;
}
return currency.getCode();
}
};
}
See comparison between inlined and not-inlined lenses here.
To add lens4j support to Intellij, install Lens4j intellij plugin
All benchmarks were run on:
- Machine: MacBook Pro 2015
- Processor: 2.2 GHz Quad-Core Intel Core i7
- Memory: 16 GB 1600MHz DDR3
See latest benchmark result here .
As you can see, generated lenses are as fast as manually written code, but lenses which were build
manually with Lenses.compose
api are several times slower than generated ones.
Lenses' performance is a subject for father optimization. Any help is welcome :)
To run benchmarks do several steps:
- pull project to your machine
- run from root directory
mvn package -Pbenchmark
- go to
lens4j-benchmark/target
directory.lens4j-benchmark-${version}-jar-with-dependencies.jar
should be generated - run
command
java -cp ./lens4j-benchmark-${version}-jar-with-dependencies.jar dev.khbd.lens4j.benchmark.BenchmarkRunner -rf json
jmh-result.json
report should be generated- view it through jmh visualizer