From 8f1f1cbcc65715a4d0635a6289d0ed21194dccce Mon Sep 17 00:00:00 2001 From: sttk Date: Mon, 14 Jul 2025 08:53:56 +0900 Subject: [PATCH 1/3] doc: modified README --- README.md | 459 ++++++++++++++---------------------------------------- 1 file changed, 117 insertions(+), 342 deletions(-) diff --git a/README.md b/README.md index 96cc7b2..1208ff7 100644 --- a/README.md +++ b/README.md @@ -1,410 +1,181 @@ -# [Sabi][repo-url] [![GitHub.io][io-img]][io-url] [![CI Status][ci-img]][ci-url] [![MIT License][mit-img]][mit-url] +# [Sabi][repo-url] [![Maven Central][mvn-img]][mvn-url] [![GitHub.io][io-img]][io-url] [![CI Status][ci-img]][ci-url] [![MIT License][mit-img]][mit-url] -A small framework to separate logics and data accesses for Java application. +A small framework for Java designed to separate logic from data access. -## Concept +It achieves this by connecting the logic layer and the data access layer via traits, similar to traditional Dependency Injection (DI). This reduces the dependency between the two, allowing them to be implemented and tested independently. -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. +However, traditional DI often presented an inconvenience in how methods were grouped. Typically, methods were grouped by external data service like a database or by database table. This meant the logic layer had to depend on units defined by the data access layer's concerns. Furthermore, such traits often contained more methods than a specific piece of logic needed, making it difficult to tell which methods were actually used in the logic without tracing the code. +This framework addresses that inconvenience. The data access interface used by a logic function is unique to that specific logic, passed as an argument to the logic function. This interface declares all the data access methods that specific logic will use. -### Separation of logics and data accesses +On the data access layer side, implementations can be provided by concrete types that fulfill multiple `DataAcc` derived class. This allows for implementation in any arbitrary unit — whether by external data service, by table, or by functional concern. -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. +This is achieved through the following mechanism: -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. +- A `DataHub` class aggregates all data access methods. `DataAcc` derived classs are attached to `DataHub`, giving `DataHub` the implementations of the data access methods. +- Logic functional interfaces accept specific, narrowly defined data access interfaces as arguments. These interfaces declare only the methods relevant to that particular piece of logic. +- The `DataHub` class implements all of these specific data access interfaces. When a `DataHub` instance is passed to a logic functional interface, the logic functional interface interacts with it via the narrower interface, ensuring it only sees and uses the methods it needs. Using Java's inheritance mechanism, a type implements an interface by methods of other classes. The `DataHub` simply needs to have methods that match the signatures of all the methods declared across the various logic-facing data access interfaces. -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. +This approach provides strong compile-time guarantees that logic only uses what it declares, while allowing flexible organization of data access implementations. +## Installation -### Separation of data accesses for each logic +## Install -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. +This package can be installed from [Maven Central Repository][mvn-url]. -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. +The examples of declaring that repository and the dependency on this package in Maven `pom.xml` and Gradle `build.gradle` are as follows: -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. - - -### Separation of data accesses by data sources and reintegration of them - -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. - - -## Usage - -### Logic and an interface for its data access - -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`. +### for Maven ``` -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 { - @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"); - } -} + + + io.github.sttk + sabi + 0.4.0 + + ``` -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. - -### Data accesses for unit testing - -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. +### for Gradle ``` -class MapGreetDax extends DaxBase implements GreetDax { - Map 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); - } +repositories { + mavenCentral() +} +dependencies { + implementation 'io.github.sttk:sabi:0.4.0' } ``` -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"); - } -``` - -### Data accesses for actual use +## Usage -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`. +### 1. Implementing DataSrc and DataConn -The following code is an example of a dax struct which inputs an user name and -the hour from an environment variable. +First, you'll define `DataSrc` which manages connections to external data services and creates `DataConn`. Then, you'll define `DataConn` which represents a session-specific connection and implements transactional operations. -``` -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; - } +```java +import com.github.sttk.errs.Exc; +import com.github.sttk.sabi.DataSrc; +import com.github.sttk.sabi.DataConn; - @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); - } - } +class FooDataSrc implements DataSrc { + @Override public void setup(AsyncGroup ag) throws Exc {} + @Override public void close() {} + @Override public DataConn createDataConn() throws Exc { return new FooDataConn(); } } -``` -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); - } +class FooDataConn implements DataConn { + @Override public void commit(AsyncGroup ag) throws Exc {} + @Override public void rollback(AsyncGroup ag) {} + @Override public void close(AsyncGroup ag) {} } -``` -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 BarDataSrc implements DataSrc { + @Override public void setup(AsyncGroup ag) throws Exc {} + @Override public void close() {} + @Override public DataConn createDataConn() throws Exc { return new BarDataConn(); } +} -``` -class GreetDaxBase extends DaxBase - implements EnvVarsDax, ConsoleDax {} +class BarDataConn implements DataConn { + @Override public void commit(AsyncGroup ag) throws Exc {} + @Override public void rollback(AsyncGroup ag) {} + @Override public void close(AsyncGroup ag) {} +} ``` -### Executing a logic +### 2. Implementing logic functions and data traits -The following code executes the above `GreetLogic` in a transaction process. +Define interfaces and functions that express your application logic. These interfaces are independent of specific data source implementations, improving testability. -``` -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); - } - } +```java +import com.github.sttk.errs.Exc; +import com.github.sttk.sabi.Logic; - static void app() throws Err { - try (var base = new GreetDaxBase()) { - base.txn(new GreetLogic()); - } - } +interface MyData { + String getText() throws Exc; + void setText(String text) throws Exc; } -``` - -### Changing to a dax of another data source - -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(); +class MyLogic implements Logic { + @Override public void run(MyData data) throws Exc { + String text = data.getText(); + data.setText(text); } } ``` -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 -``` - -### Moving outputs to next transaction process +### 3. Implementing DataAcc derived classes -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. +The `DataAcc` interface abstracts access to data connections. The methods defined here will be used to obtain data connections via `DataHub` and perform actual data operations. -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. +```java +import com.github.sttk.errs.Exc; +import com.github.sttk.sabi.DataAcc; -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; +interface GettingDataAcc extends DataAcc, MyData { + @Override public String getText() throws Exc { + var conn = getDataConn("foo", FooDataConn.class); + // ... + return "output text"; + } } -class PrintLogic extends Logic { - @Override public void run(PrintDax dax) throws Err { - var text = dax.getText(); - return dax.print(text); +interface SettingDataAcc extends DataAcc, MyData { + @Override public void setText(String text) throws Exc { + var conn = getDataConn("bar", BarDataConn.class); + // ... } } ``` -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. +### 4. Integrating data interfaces and DataAcc derived classes into `DataHub` -The following codes are the implementations of `MemoryDaxSrc`, `MemoryDaxConn`, -and `MemoryDax`. +The `DataHub` is the central component that manages all `DataSrc` and `DataConn`, providing access to them for your application logic. By implementing the data interface (`MyData`) from step 2 and the `DataAcc` class from step 3 on `DataHub`, you integrate them. -``` -class MemoryDaxSrc implements DaxSrc { - StringBuilder buf = new StringBuilder(); +```java +import com.github.sttk.errs.Exc; +import com.github.sttk.sabi.DataHub; - @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 MyDataHub extends DataHub implements GettingDataAcc, SettingDataAcc {} ``` -``` -class MemoryDaxConn implements DaxConn { - StringBuilder buf; - public MemoryDaxConn(StringBuilder buf) { - this.buf = buf; - } +### 5. Using logic functions and `DataHub` - public void append(String text) { - this.buf.append(text); - } +Inside your init function, register your global `DataSrc`. Next, main function calls run function, and inside run function, setup the sabi framework. Then, create an instance of `DataHub` and register the necessary local `DataSrc` using the Uses method. Finally, use the txn method of `DataHub` to execute your defined application logic function (`MyLogic`) within a transaction. This automatically handles transaction commits and rollbacks. - public String get() { - return this.buf.toString(); - } +```java +import com.github.sttk.errs.Exc; +import com.github.sttk.sabi.Sabi; - @Override public void commit(AsyncGroup ag) throws Err { +public class Main { + static { + // Register global DataSrc. + Sabi.uses("foo", new FooDataSrc()); } - @Override public boolean isCommitted() { - return true; - } + public static void main(String[] args) { + // Set up the sabi framework. + try (var ac = Sabi.setup()) { - @Override public void rollback(AsyncGroup ag) { - } + // Creates a new instance of DataHub. + var hub = new MyDataHub(); - @Override public void forceBack(AsyncGroup ag) { - buf.setLength(0); - } + // Register session-local DataSrc to DataHub. + hub.uses("bar", new BarDataSrc()); - @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); - } + // Execute application logic within a transaction. + // MyLogic performs data operations via DataHub. + hub.txn(new MyLogic()); - @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 + } catch (Exception e) { + System.exit(1); } } -``` - -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. - ## Native build @@ -420,7 +191,7 @@ 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. +And all logic and data access 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. @@ -431,21 +202,25 @@ This framework supports JDK 21 or later. ### Actually checked JDK versions: -- GraalVM CE 21.0.1+12.1 (openjdk version 21.0.1) - +- Oracle GraalVM 21.0.7+8.1 +- Oracle GraalVM 22.0.2+9.1 +- Oracle GraalVM 23.0.2+7.1 +- Oracle GraalVM 24.0.1+9.1 ## License -Copyright (C) 2022-2023 Takayuki Sato +Copyright (C) 2022-2025 Takayuki Sato This program is free software under MIT License.
See the file LICENSE in this distribution for more details. [repo-url]: https://github.com/sttk/sabi-java +[mvn-img]: https://img.shields.io/badge/maven_central-0.4.0-276bdd.svg +[mvn-url]: https://mvnrepository.com/artifact/io.github.sttk/sabi/0.4.0 [io-img]: https://img.shields.io/badge/github.io-Javadoc-4d7a97.svg [io-url]: https://sttk.github.io/sabi-java/ [ci-img]: https://github.com/sttk/sabi-java/actions/workflows/java-ci.yml/badge.svg?branch=main -[ci-url]: https://github.com/sttk/sabi-java/actions +[ci-url]: https://github.com/sttk/sabi-java/actions?query=branch%3Amain [mit-img]: https://img.shields.io/badge/license-MIT-green.svg [mit-url]: https://opensource.org/licenses/MIT From e80f7b6aae329bba97b5b50f5fa9384b068b0d7a Mon Sep 17 00:00:00 2001 From: sttk Date: Mon, 14 Jul 2025 13:07:52 +0900 Subject: [PATCH 2/3] fix: modified what pointed out --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1208ff7..873b144 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ However, traditional DI often presented an inconvenience in how methods were gro This framework addresses that inconvenience. The data access interface used by a logic function is unique to that specific logic, passed as an argument to the logic function. This interface declares all the data access methods that specific logic will use. -On the data access layer side, implementations can be provided by concrete types that fulfill multiple `DataAcc` derived class. This allows for implementation in any arbitrary unit — whether by external data service, by table, or by functional concern. +On the data access layer side, implementations can be provided by concrete types that fulfill multiple `DataAcc` derived classes. This allows for implementation in any arbitrary unit — whether by external data service, by table, or by functional concern. This is achieved through the following mechanism: -- A `DataHub` class aggregates all data access methods. `DataAcc` derived classs are attached to `DataHub`, giving `DataHub` the implementations of the data access methods. +- A `DataHub` class aggregates all data access methods. `DataAcc` derived classes are attached to `DataHub`, giving `DataHub` the implementations of the data access methods. - Logic functional interfaces accept specific, narrowly defined data access interfaces as arguments. These interfaces declare only the methods relevant to that particular piece of logic. - The `DataHub` class implements all of these specific data access interfaces. When a `DataHub` instance is passed to a logic functional interface, the logic functional interface interacts with it via the narrower interface, ensuring it only sees and uses the methods it needs. Using Java's inheritance mechanism, a type implements an interface by methods of other classes. The `DataHub` simply needs to have methods that match the signatures of all the methods declared across the various logic-facing data access interfaces. @@ -20,8 +20,6 @@ This approach provides strong compile-time guarantees that logic only uses what ## Installation -## Install - This package can be installed from [Maven Central Repository][mvn-url]. The examples of declaring that repository and the dependency on this package in Maven `pom.xml` and Gradle `build.gradle` are as follows: @@ -98,7 +96,7 @@ interface MyData { void setText(String text) throws Exc; } -class MyLogic implements Logic { +class MyLogic implements Logic { @Override public void run(MyData data) throws Exc { String text = data.getText(); data.setText(text); From bcb255bd75a9f1fb36df2be73aa6f1ea88648caf Mon Sep 17 00:00:00 2001 From: sttk Date: Mon, 14 Jul 2025 13:20:11 +0900 Subject: [PATCH 3/3] fix: modified what pointed out --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 873b144..a194e5b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ This package can be installed from [Maven Central Repository][mvn-url]. The examples of declaring that repository and the dependency on this package in Maven `pom.xml` and Gradle `build.gradle` are as follows: -### for Maven +### For Maven ``` @@ -36,7 +36,7 @@ The examples of declaring that repository and the dependency on this package in ``` -### for Gradle +### For Gradle ``` repositories { @@ -57,6 +57,7 @@ First, you'll define `DataSrc` which manages connections to external data servic import com.github.sttk.errs.Exc; import com.github.sttk.sabi.DataSrc; import com.github.sttk.sabi.DataConn; +import com.github.sttk.sabi.AsyncGroup; class FooDataSrc implements DataSrc { @Override public void setup(AsyncGroup ag) throws Exc {} @@ -113,7 +114,7 @@ import com.github.sttk.errs.Exc; import com.github.sttk.sabi.DataAcc; interface GettingDataAcc extends DataAcc, MyData { - @Override public String getText() throws Exc { + @Override default String getText() throws Exc { var conn = getDataConn("foo", FooDataConn.class); // ... return "output text"; @@ -121,7 +122,7 @@ interface GettingDataAcc extends DataAcc, MyData { } interface SettingDataAcc extends DataAcc, MyData { - @Override public void setText(String text) throws Exc { + @Override default void setText(String text) throws Exc { var conn = getDataConn("bar", BarDataConn.class); // ... }