diff --git a/README.md b/README.md index 96cc7b2..a194e5b 100644 --- a/README.md +++ b/README.md @@ -1,410 +1,180 @@ -# [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 classes. 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 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. -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 +This package can be installed from [Maven Central Repository][mvn-url]. -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. +The examples of declaring that repository and the dependency on this package in Maven `pom.xml` and Gradle `build.gradle` are as follows: -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. - - -### 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"); - } -``` +## Usage -### Data accesses for actual use +### 1. Implementing DataSrc and DataConn -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`. +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. -The following code is an example of a dax struct which inputs an user name and -the hour from an environment variable. +```java +import com.github.sttk.errs.Exc; +import com.github.sttk.sabi.DataSrc; +import com.github.sttk.sabi.DataConn; +import com.github.sttk.sabi.AsyncGroup; -``` -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; - } +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(); } +} - @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 FooDataConn implements DataConn { + @Override public void commit(AsyncGroup ag) throws Exc {} + @Override public void rollback(AsyncGroup ag) {} + @Override public void close(AsyncGroup ag) {} } -``` -The following code is an example of a dax struct which output a text to -console. +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(); } +} -``` -interface ConsoleDax extends GreetDax, Dax { - @Override default void output(String text) throws Err { - System.out.print(text); - } +class BarDataConn 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. +### 2. Implementing logic functions and data traits -``` -class GreetDaxBase extends DaxBase - implements EnvVarsDax, ConsoleDax {} -``` - -### Executing a logic +Define interfaces and functions that express your application logic. These interfaces are independent of specific data source implementations, improving testability. -The following code executes the above `GreetLogic` in a transaction process. +```java +import com.github.sttk.errs.Exc; +import com.github.sttk.sabi.Logic; -``` -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()); - } - } +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 default 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 default 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 +190,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 +201,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