A small framework to separate logics and data accesses for Java application.
The overall concept of this framework is separation and reintegration of necessary and redundant parts based on the perspectives of the whole and the parts. The separation of logics and data accesses is the most prominent and fundamental part of this concept.
In general, a program consists of procedures and data. And procedures include data accesses for operating data, and the rest of procedures are logics. So we can say that a program consists of logics, data accesses and data.
We often think to separate an application to multiple layers, for example, controller layer, business logic layer, and data access layer. The logics and data accesses mentioned in this framework may appear to follow such layering. However, the controller layer also has data accesses such as transforming user requests and responses for the business logic layer. Generally, such layers of an application is established as vertically positioned stages of data processing within a data flow.
In this framework, the relationship between logics and data accesses is not
defined by layers but by lanes.
Although their relationship is vertical in terms of invocation, it is
conceptually horizontal.
DaxBase
serves as an intermediary that connects both of them.
A logic is a functional interface of which the sole method takes a dax
interface as its only one argument.
The type of this dax is declared by the type parameter of the logic interface,
and also the type parameter of the transaction method, DaxBase#txn
, that
executes logics.
Therefore, since the type of dax can be changed for each logic or transaction,
it is possible to limit data accesses used by the logic, by declaring only
necessary data access methods from among ones defined in DaxBase
instance.
At the same time, since all data accesses of a logic is done through this sole dax interface, this dax interface serves as a list of data access methods used by a logic.
Data access methods are implemented as methods of some Dax
structs that
embedding a DaxBase
.
Furthermore these Dax
structs are integrated into a single new DaxBase
.
A Dax
struct can be created at any unit, but it is clearer to create it at
the unit of the data source.
By doing so, the definition of a new DaxBase
also serves as a list of the
data
sources being used.
A logic is implemented as a functionnal interface. This sole method takes only an argument, dax, which is an interface that gathers only the data access methods needed by this logic interface.
Since a dax for a logic conceals details of data access procedures, this interface only includes logical procedures. In this logical part, there is no concern about where the data is input from or where it is output to.
For example, in the following code, GreetLogic
is a logic interface and
GreetDax
is a dax interface for GreetLogic
.
interface GreetDax {
record NoName() {}
record FailToGetHour() {}
record FailToOutput(String text) {}
String getUserName() throws Err;
int getHour() throws Err;
void output(String text) throws Err;
}
class GreetLogic implements Logic<GreetDax> {
@Override public void run(GreetDax dax) throws Err {
int hour = dax.getHour();
String s;
if (5 <= hour && hour < 12) {
s = "Good morning, ";
} else if (12 <= hour && hour < 16) {
s = "Good afternoon, ";
} else if (16 <= hour && hour < 21) {
s = "Good evening, ";
} else {
s = "Hi, ";
}
dax.output(s);
var name = dax.getUserName();
dax.output(name + ".\n");
}
}
In GreetLogic,
there are no codes for inputting the hour, inputting a user
name, and outputing a greeting.
This logic function has only concern to create a greeting text.
To test a logic interface, the simplest dax struct is what using a map.
The following code is an example of a dax struct using a map and having three
methods that are same to GreetDax
interface methods above.
class MapGreetDax extends DaxBase implements GreetDax {
Map<String, Object> m = new HashMap<>();
@Override public String getUserName() throws Err {
var name = this.m.get("username");
if (name == null) {
throw new Err(new NoName());
}
return String.class.cast(name);
}
@Override public int getHour() throws Err {
var hour = this.m.get("hour");
if (hour == null) {
throw new Err(new FailToGetHour());
}
return Integer.class.cast(hour);
}
@Override public void output(String text) throws Err {
String s = "";
var v = this.m.get("greeting");
if ("error".equals(v)) {
throw new Err(new FailToOutput(text));
} else if (v != null) {
s += v;
}
this.m.put("greeting", s + text);
}
}
And the following code is an example of a test case.
@Test void testGreetLogic_morning() {
var base = new MapGreetDaxBase();
base.m.put("username", "everyone");
base.m.put("hour", 10);
try (base) {
base.txn(new GreetLogic());
} catch (Err e) {
fail(e.toString());
}
assertEquals(base.m.get("greeting"), "Good morning, everyone.\n");
}
In actual use, multiple data sources are often used.
In this example, an user name and the hour are input as an environment
variable, and greeting is output to console.
Therefore, two dax struct are created and they are integrated into a new
struct based on DaxBase
.
Since Golang is structural typing language, this new DaxBase
can be casted
to GreetDax
.
The following code is an example of a dax struct which inputs an user name and the hour from an environment variable.
interface EnvVarsDax extends GreetDax, Dax {
@Override default String getUserName() throws Err {
var u = System.getenv("GREETING_USERNAME");
if (u == null || u.isBlank()) {
throw new Err(new NoName());
}
return u;
}
@Override default int getHour() throws Err {
var h = System.getenv("GREETING_HOUR");
try {
return Integer.valueOf(h);
} catch (Exception e) {
throw new Err(new FailToGetHour(), e);
}
}
}
The following code is an example of a dax struct which output a text to console.
interface ConsoleDax extends GreetDax, Dax {
@Override default void output(String text) throws Err {
System.out.print(text);
}
}
And the following code is an example of a constructor function of a struct
based on DaxBase
into which the above two dax are integrated.
This implementation also serves as a list of the external data sources being
used.
class GreetDaxBase extends DaxBase
implements EnvVarsDax, ConsoleDax {}
The following code executes the above GreetLogic
in a transaction process.
public class GreetApp {
public static void main(String[] args) {
try (var ac = Sabi.startApp()) {
app();
} catch (Err e) {
System.err.println(e.toString());
System.exit(1);
}
}
static void app() throws Err {
try (var base = new GreetDaxBase()) {
base.txn(new GreetLogic());
}
}
}
In the above codes, the hour is obtained from command line arguments. Here, assume that the specification has been changed to retrieve it from system clock instread.
interface SystemClockDax extends GreetDax, Dax {
@Override default int getHour() throws Err {
return OffsetTime.now().getHour();
}
}
And the DaxBase
struct, into which multiple dax structs have been integrated,
is modified as follows.
class GreetDaxBase extends DaxBase
implements EnvVarsDax, SystemClockDax, ConsoleDax {} // Changed
The above codes works normally if no error occurs. But if an error occurs at getting user name, a incomplete string is being output to console. Such behavior is not appropriate for transaction processing.
So we should change the above codes to store in memory temporarily in the existing transaction process, and then output to console in the next transaction.
The following code is the logic to output text to console in next transaction process and the dax interface for this logic.
interface PrintDax {
String getText() throws Err;
void print(String text) throws Err;
}
class PrintLogic extends Logic<PrintDax> {
@Override public void run(PrintDax dax) throws Err {
var text = dax.getText();
return dax.print(text);
}
}
Here, we try to create a DaxSrc
and DaxConn
for memory store, too.
Since a dax interface cannot have its own state, the DaxSrc
holds the memory
store as its state.
The following codes are the implementations of MemoryDaxSrc
, MemoryDaxConn
,
and MemoryDax
.
class MemoryDaxSrc implements DaxSrc {
StringBuilder buf = new StringBuilder();
@Override public void setup(AsyncGroup ag) throws Err {
}
@Override public void close() {
buf.setLength(0);
}
@Override public DaxConn createDaxConn() throws Err {
return new MemoryDaxConn(buf);
}
}
class MemoryDaxConn implements DaxConn {
StringBuilder buf;
public MemoryDaxConn(StringBuilder buf) {
this.buf = buf;
}
public void append(String text) {
this.buf.append(text);
}
public String get() {
return this.buf.toString();
}
@Override public void commit(AsyncGroup ag) throws Err {
}
@Override public boolean isCommitted() {
return true;
}
@Override public void rollback(AsyncGroup ag) {
}
@Override public void forceBack(AsyncGroup ag) {
buf.setLength(0);
}
@Override public void close() {
}
}
interface MemoryDax extends GreetDax, PrintDax, Dax {
@Override default void output(String text) throws Err {
MemoryDaxConn conn = getDaxConn("memory");
conn.append(text);
}
@Override default String getText() throws Err {
MemoryDaxConn conn = getDaxConn("memory");
return conn.get();
}
}
class GreetDaxBase extends DaxBase
implements EnvVarsDax, SystemClockDax, MemoryDax, ConsoleDax {} // Changed
void app() throws Err {
try (var base = new GreetDaxBase()) {
base.uses("memory", new MemoryDaxSrc()); // Added
base.txn(new GreetLogic());
base.txn(new PrintLogic()); // Added
}
}
And we need to change the name of the method ConsoleDax#output
to avoid name
collision with the method MemoryDax#output
.
interface ConsoleDax extends PrintDax, Dax { // Changed from GreetDax
@Override default void print(String text) throws Err { // Changed from Output
System.out.print(text);
}
}
That completes it.
The important point is that the GreetLogic
function is not changed.
Since these changes are not related to the existing application logic, it is
limited to the data access part (and the part around the newly added logic)
only.
This framework supports native build with GraalVM.
See the following pages to setup native build environment on Linux/macOS or Windows.
And see the following pages to build native image with Maven or Gradle.
Since this framework does not use Java reflections, etc., any native build configuration files are not needed.
And all dax
implementations should not use them, too.
However, some of client libraries provided for data sources might use them,
and it might be needed those configuration files.
This framework supports JDK 21 or later.
- GraalVM CE 21.0.1+12.1 (openjdk version 21.0.1)
Copyright (C) 2022-2023 Takayuki Sato
This program is free software under MIT License.
See the file LICENSE in this distribution for more details.