システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

ArchUnitのメモ

参考

ArchUnit User Guide

GitHub - TNG/ArchUnit: A Java architecture test library, to specify and assert architecture rules in plain Java

ArchUnitで考えるアーキテクチャ構造とその検証 | 豆蔵デベロッパーサイト

ArchUnit 実践:集約操作専用のリポジトリ(やDAO)によってのみ、集約が永続化されることを強制する①<個別 ver.> - Qiita

ArchUnit使い方メモ - Qiita

https://blogs.oracle.com/otnjp/post/unit-test-your-architecture-with-archunit-ja

テスト実装の公式サンプル

GitHub - TNG/ArchUnit-Examples: Examples for ArchUnit (A Java architecture test library, to specify and assert architecture rules in plain Java)

typescript-generatorのメモ

参考リンク

GitHub - vojtechhabarta/typescript-generator: Generates TypeScript from Java - JSON declarations, REST service client

JavaのクラスからTypeScriptの型定義を生成するtypescript-generatorを使ってみた

typescript-generator カテゴリーの記事一覧 - 毎日へっぽこ

生成例

コントローラー

@Path("/users")
@EntryPoint
public class UserController {

  private SearchUser searchUser;

  @Inject
  UserController(SearchUser searchUser) {
    this.searchUser = searchUser;
  }

  @GetEntry
  @Schema(description = "ユーザー情報を検索します")
  @Path("{id}")
  public UserResourse getUsersById(@PathParam("id") String id) {
    var model = searchUser.findById(UserId.of(Integer.valueOf(id))).orElseThrow();
    return UserResourse.from(model);
  }

  @GetEntry
  @Schema(description = "ユーザー情報を全件取得します")
  public List<UserResourse> getUsers() {
    var models = searchUser.findAll();
    return UserResourse.from(models);
  }
}

レスポンスクラス

/**
 * UserResourse.
 */
@lombok.Builder(access = AccessLevel.PRIVATE)
@lombok.Data
public class UserResourse {

  private final Gender gender;
  private final Optional<Integer> id;
  private final Optional<String> name;

  static UserResourse from(User user) {
    var res = UserResourse.builder()
        .gender(user.getGender())
        .id(Optional.ofNullable(user.getUserId().getId()))
        .name(user.getName().getValue())
        .build();
    return res;
  }

  static List<UserResourse> from(Users users) {
    var resList = users.getItems().stream()
        .map(UserResourse::from)
        .sorted(Comparator.comparing(res -> res.getId().get(), nullsFirst(naturalOrder())))
        .collect(Collectors.toList());

    return resList;
  }
}

Enumクラス

@lombok.AllArgsConstructor
@lombok.Getter
public enum Gender {

  MALE("0"), FEMALE("1"), OTHER("2");

  private final String cd;

  public static final EnumReverseLookup<Gender, String> byCd
      = new EnumReverseLookup<>(Gender.class, Gender::getCd);

}

pom

<plugin>
    <groupId>cz.habarta.typescript-generator</groupId>
    <artifactId>typescript-generator-maven-plugin</artifactId>
    <version>${typescript-generator.version}</version>
    <executions>
        <execution>
            <id>typescript-generator-generate</id>
            <goals>
                <goal>generate</goal>
            </goals>
            <phase>process-classes</phase>
        </execution>
    </executions>
    <configuration>
        <jsonLibrary>jackson2</jsonLibrary>
        <noFileComment>true</noFileComment>
        <optionalPropertiesDeclaration>questionMarkAndNullableType</optionalPropertiesDeclaration>
        <sortTypeDeclarations>true</sortTypeDeclarations>
        <outputKind>module</outputKind>
        <outputFileType>implementationFile</outputFileType>
        <generateJaxrsApplicationClient>true</generateJaxrsApplicationClient>
        <generateJaxrsApplicationInterface>true</generateJaxrsApplicationInterface>
        <namespace>rest</namespace>

        <extensions>
            <extension>org.vermeerlab.plugin.typescript.generator.ext.EnumToConstConverter</extension>
        </extensions>

        <classPatterns>
            <classPattern>**.presentation.**</classPattern>
        </classPatterns>
        <outputFile>type-script/rest.ts</outputFile>
    
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.verneermlab</groupId>
            <artifactId>ts-generator-plugin</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</plugin>

生成されたTypeScriptファイル

export namespace rest {

    export interface HttpClient {

        request<R>(requestConfig: { method: string; url: string; queryParams?: any; data?: any; copyFn?: (data: R) => R; }): RestResponse<R>;
    }

    export interface RestApplication {

        /**
         * HTTP GET /users
         * Java method: ee.sample.apps.context.user.presentation.UserController.getUsers
         */
        getUsers(): RestResponse<UserResourse[]>;

        /**
         * HTTP GET /users/{id}
         * Java method: ee.sample.apps.context.user.presentation.UserController.getUsersById
         */
        getUsersById(id: string): RestResponse<UserResourse>;
    }

    export class RestApplicationClient implements RestApplication {

        constructor(protected httpClient: HttpClient) {
        }

        /**
         * HTTP GET /users
         * Java method: ee.sample.apps.context.user.presentation.UserController.getUsers
         */
        getUsers(): RestResponse<UserResourse[]> {
            return this.httpClient.request({ method: "GET", url: uriEncoding`users` });
        }

        /**
         * HTTP GET /users/{id}
         * Java method: ee.sample.apps.context.user.presentation.UserController.getUsersById
         */
        getUsersById(id: string): RestResponse<UserResourse> {
            return this.httpClient.request({ method: "GET", url: uriEncoding`users/${id}` });
        }
    }

    export interface UserResourse {
        gender: Gender;
        id?: number | null;
        name?: string | null;
    }

    export interface UserResourseBuilder {
    }

    export type Gender = "MALE" | "FEMALE" | "OTHER";

    export type RestResponse<R> = Promise<R>;

    function uriEncoding(template: TemplateStringsArray, ...substitutions: any[]): string {
        let result = "";
        for (let i = 0; i < substitutions.length; i++) {
            result += template[i];
            result += encodeURIComponent(substitutions[i]);
        }
        result += template[template.length - 1];
        return result;
    }


    // Added by 'EnumToConstConverter' extension

    export const Gender = {
        MALE: "MALE",
        FEMALE: "FEMALE",
        OTHER: "OTHER",
    } as const;

}

org.vermeerlab.plugin.typescript.generator.ext.EnumToConstConverter
enumの変換用につくった拡張(別プロジェクトで作成して dependencyしているもののコード)
EE(Payara)で同一プロジェクトで準備をすると、起動時にエラーになるので別プロジェクトにしています(Jackson2あたりが競合している様子)

/**
 * Enumクラスからconstを作成します.
 */
public class EnumToConstConverter extends EmitterExtension {

  @Override
  public EmitterExtensionFeatures getFeatures() {
    var emitterExcensionFeatures = new EmitterExtensionFeatures();
    emitterExcensionFeatures.generatesRuntimeCode = true;
    return emitterExcensionFeatures;
  }

  @Override
  public void emitElements(Writer writer, Settings settings, boolean exportKeyword, TsModel model) {
    var exportStr = exportKeyword ? "export " : "";
    var tsEnums = model.getOriginalStringEnums();

    tsEnums.forEach((var tsEnum) -> {
      writer.writeIndentedLine("");

      var constName = new StringBuilder()
          .append(exportStr)
          .append("const ")
          .append(tsEnum.getName().getSimpleName())
          .append(" = {").toString();

      writer.writeIndentedLine(constName);

      tsEnum.getMembers().forEach((var member) -> {
        var memberProp = new StringBuilder()
            .append(settings.indentString)
            .append(member.getPropertyName())
            .append(": \"")
            .append(member.getEnumValue())
            .append("\",")
            .toString();
        writer.writeIndentedLine(memberProp);
      });

      writer.writeIndentedLine("} as const;");

    });
  }
}

その他

コードの全量はいずれ公開予定

BigDecimalを扱う共通クラス

はじめに

今までBigDecimalを使うようなコードを書くことが無かったのですが、とうとう(?)書く機会を得ました。
開発現場で共通部品として書いたものを振り返ることを目的にしつつ、現場では必要十分として*1 諦めたところなどを充足したものを整理しようと思ってOSS*2にしたものです。

どこかに ありもののサンプルがあれば良かったのですが、それらしいものが見つからなかったので自分で作ってみました。

何が嬉しいの?

  • BigDecimalを扱う汎用クラスで数値を扱うケース(Integer、Long、BigDecimal)を透過的に扱えます
  • 四則演算の組み合わせを持たせられ(加算だけのクラスが作れる)

実装と説明

基底の型

Integer、Long、BigDecimal いずれの数値型からもinstanceを生成してアクセスできるようにしています。

public interface CalculatorBase {

    /**
     * 計算に使用するプロパティ値を返却します.
     *
     * @return 計算に使用するプロパティ値
     */
    default BigDecimal getCalcValue() {
        return this.toBigDecimal();
    }

    /**
     * プロパティ数値を返却します.
     *
     * @return プロパティで保持している値
     */
    BigDecimal toBigDecimal();

    /**
     * プロパティ数値を返却します.
     *
     * @return プロパティで保持している値(桁落ちした場合は、桁落ちした値)
     */
    default Long toLong() {
        return this.toBigDecimal().longValue();
    }

    /**
     * プロパティ数値を返却します.
     *
     * @return プロパティで保持している値(桁落ちした場合は、桁落ちした値)
     */
    default Integer toInt() {
        return this.toBigDecimal().intValue();
    }

加算

加算は異なる型による加算はNGとして引数と戻り値に対して型縛りをしています。
加算に限ってはリストを一括で加算したいケースがありそうなので、インターフェースにもそれを入れました。

public interface Plus<T> extends CalculatorBase {

    /**
     * 加算したインスタンスを返却します.
     *
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス
     */
    @SuppressWarnings("unchecked")
    public T plus(T... other);

    /**
     * 複数要素を加算します.
     * <br>
     * 加算は複数情報を一括で処理したいケースがあるため、デフォルトでも準備しています.
     *
     * @param others 計算するインスタンスリスト
     * @throws RuntimeException 引数インスタンスからクラス情報が取得出来ない場合
     * @return 計算後のインスタンス
     */
    public T plusAll(List<T> others);

}

減算

減算も加算と同じく、異なる型による加算はNGとして引数と戻り値に対して型縛りをしています。
可変長引数により複数要素を一括で減算するようにはしていますが、リストで一括は あまり無さそうなので外しました。

public interface Minus<T> extends CalculatorBase {

    /**
     * 減算したインスタンスを返却します.
     *
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス
     */
    @SuppressWarnings("unchecked")
    public T minus(T... other);

}

乗算

乗算は自身とは異なる型を引数に指定したいことは多々あります(単位(ダース) × 数量 のような感じで)。
それに加えて、戻り値の型も自身の型、引数の型とも異なるようにしたい場合もあります(金額 =単価 × 数量)。

public interface Multiply<T extends Multiply<T>> extends CalculatorBase {

    /**
     * 乗算したインスタンスを返却します.
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス
     */
    public <U extends Multiply<U>> T multiply(U other);

    /**
     * 乗算したインスタンスを返却します.
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param <R> 戻り値の型
     * @param other 計算するインスタンス
     * @param newInstance 任意のクラスを生成するコンストラクタもしくはFactoryメソッド
     * @return 計算後のインスタンス
     */
    public <U extends Multiply<U>, R extends CalculatorBase> R multiply(U other, Function<BigDecimal, R> newInstance);
}

除算

除算は、scale や 丸め を直接計算では使うので そういう感じに。
型については乗算と同じく。

public interface Divide<T extends Divide<T>> extends CalculatorBase {

    /**
     * インスタンスが保持しているscaleを返却します.
     *
     * @return プロパティで保持している値
     */
    Integer getScale();

    /**
     * インスタンスが保持しているRoundingModeを返却します.
     *
     * @return プロパティで保持している値
     */
    RoundingMode getRoundingMode();

    /**
     * 除算したインスタンスを返却します.
     * 小数点以下の桁数(scale)と丸め({@code RoundingMode})はインスタンスのデフォルトを使用します.<br>
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    default <U extends Divide<U>> T divide(U other) {
        return this.divide(other, this.getScale(), this.getRoundingMode());
    }

    /**
     * 除算したインスタンスを返却します.
     *
     * 小数点以下の桁数(scale)はインスタンスのデフォルトを使用します.
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param other 計算するインスタンス
     * @param roundingMode 丸めモード
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    default <U extends Divide<U>> T divide(U other, RoundingMode roundingMode) {
        return this.divide(other, this.getScale(), roundingMode);
    }

    /**
     * 除算したインスタンスを返却します.
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param other 計算するインスタンス
     * @param scale 小数点以下の有効桁数
     * @param roundingMode 丸めモード
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    public <U extends Divide<U>> T divide(U other, int scale, RoundingMode roundingMode);

    /**
     * 除算したインスタンスを返却します.
     * 小数点以下の桁数(scale)と丸め({@code RoundingMode})はインスタンスのデフォルトを使用します.<br>
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param <R> 戻り値の型
     * @param other 計算するインスタンス
     * @param newInstance 任意のクラスを生成するコンストラクタもしくはFactoryメソッド
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    default <U extends Divide<U>, R extends CalculatorBase> R divide(
            U other, Function<BigDecimal, R> newInstance) {
        return this.divide(other, this.getScale(), this.getRoundingMode(), newInstance);
    }

    /**
     * 除算したインスタンスを返却します.
     *
     * 小数点以下の桁数(scale)はインスタンスのデフォルトを使用します.
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param <R> 戻り値の型
     * @param other 計算するインスタンス
     * @param roundingMode 丸めモード
     * @param newInstance 任意のクラスを生成するコンストラクタもしくはFactoryメソッド
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    default <U extends Divide<U>, R extends CalculatorBase> R divide(
            U other, RoundingMode roundingMode, Function<BigDecimal, R> newInstance) {
        return this.divide(other, this.getScale(), roundingMode, newInstance);
    }

    /**
     * 除算したインスタンスを返却します.
     *
     * @param <U> 引数の型, 乗算は自身と異なるクラスを指定できます.
     * @param <R> 戻り値の型
     * @param other 計算するインスタンス
     * @param scale 小数点以下の有効桁数
     * @param roundingMode 丸めモード
     * @param newInstance 任意のクラスを生成するコンストラクタもしくはFactoryメソッド
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    public <U extends Divide<U>, R extends CalculatorBase> R divide(
            U other, int scale, RoundingMode roundingMode, Function<BigDecimal, R> newInstance);
}

汎用計算機

全部の四則演算をもった汎用計算機。
これを各ドメインモデルで使用します。
もちろん、ただのクラスなので直接使っても良いです。

public class BigDecimalCalculator implements CalculatorBase {

    private final Integer DEFAULT_SCALE = 0;
    private final RoundingMode DEFAULT_ROUND_MODE = RoundingMode.DOWN;

    private final BigDecimal value;
    private final Integer scale;
    private final RoundingMode roundingMode;

    BigDecimalCalculator(BigDecimal value, Integer scale, RoundingMode roundingMode) {
        this.value = value;
        this.scale = Objects.isNull(scale)
                ? DEFAULT_SCALE
                : scale;

        this.roundingMode = Objects.isNull(roundingMode)
                ? DEFAULT_ROUND_MODE
                : roundingMode;
    }

    public static BigDecimalCalculatorBuilder builder(BigDecimal value) {
        return new BigDecimalCalculatorBuilder(value);
    }

    public static BigDecimalCalculatorBuilder builder(Integer value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return new BigDecimalCalculatorBuilder(bigDecimal);
    }

    public static BigDecimalCalculatorBuilder builder(Long value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return new BigDecimalCalculatorBuilder(bigDecimal);
    }

    public static BigDecimalCalculatorBuilder builder(String value) {
        var bigDecimal = new BigDecimal(value);
        return new BigDecimalCalculatorBuilder(bigDecimal);
    }

    /**
     * 加算したインスタンスを返却します.
     *
     * @param <T> 計算対象の型
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス
     */
    @SafeVarargs
    public final <T extends CalculatorBase> BigDecimalCalculator plus(T... other) {
        if (other.length == 1) {
            var result = this.getCalcValue().add(other[0].getCalcValue());
            return BigDecimalCalculator.builder(result).build();
        }
        BigDecimal result = Stream.of(other)
                .map(T::getCalcValue)
                .filter(Objects::nonNull)
                .reduce(this.getCalcValue(), BigDecimal::add);

        return BigDecimalCalculator.builder(result).build();
    }

    /**
     * 複数要素を加算したインスタンスを返却します.
     *
     * 加算は複数情報を一括で処理したいケースがあるため、デフォルトでも準備しています.
     *
     * @param <T> 計算対象の型
     * @param others 計算するインスタンスリスト
     * @throws RuntimeException 引数インスタンスからクラス情報が取得出来ない場合
     * @return 計算後のインスタンス
     */
    public final <T extends CalculatorBase> BigDecimalCalculator plusAll(List<T> others) {
        BigDecimal result = others.stream()
                .map(T::getCalcValue)
                .filter(Objects::nonNull)
                .reduce(this.getCalcValue(), BigDecimal::add);

        return BigDecimalCalculator.builder(result).build();
    }

    /**
     * 加算したインスタンスを返却します.
     *
     * @param <T> 計算対象の型
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス
     */
    @SafeVarargs
    public final <T extends CalculatorBase> BigDecimalCalculator minus(T... other) {
        if (other.length == 1) {
            var result = this.getCalcValue().subtract(other[0].getCalcValue());
            return BigDecimalCalculator.builder(result).build();
        }
        BigDecimal result = Stream.of(other)
                .map(T::getCalcValue)
                .filter(Objects::nonNull)
                .reduce(this.getCalcValue(), BigDecimal::subtract);

        return BigDecimalCalculator.builder(result).build();
    }

    /**
     * 乗算したインスタンスを返却します.
     *
     * @param <T> 計算対象の型
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス
     */
    public final <T extends CalculatorBase> BigDecimalCalculator multiply(T other) {
        var result = this.getCalcValue().multiply(other.getCalcValue());
        return BigDecimalCalculator.builder(result).build();
    }

    /**
     * 除算したインスタンスを返却します.
     * 小数点以下の桁数(scale)と丸め({@code RoundingMode})はインスタンスのデフォルトを使用します.<br>
     *
     * @param <T> 計算対象の型
     * @param other 計算するインスタンス
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    public final <T extends CalculatorBase> BigDecimalCalculator divide(T other) {
        return this.divide(other, this.getScale(), this.getRoundingMode());
    }

    /**
     * 除算したインスタンスを返却します.
     *
     * 小数点以下の桁数(scale)はインスタンスのデフォルトを使用します.
     *
     * @param <T> 計算対象の型
     * @param other 計算するインスタンス
     * @param roundingMode 丸めモード
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    public final <T extends CalculatorBase> BigDecimalCalculator divide(T other, RoundingMode roundingMode) {
        return this.divide(other, this.getScale(), roundingMode);
    }

    /**
     * 除算したインスタンスを返却します.
     *
     * @param <T> 計算対象の型
     * @param other 計算するインスタンス
     * @param scale 小数点以下の有効桁数
     * @param roundingMode 丸めモード
     * @return 計算後のインスタンス.ゼロ除算の場合の戻り値はゼロを返却します(実行時例外をスローしません)
     */
    public final <T extends CalculatorBase> BigDecimalCalculator divide(T other, int scale, RoundingMode roundingMode) {
        if (other.getCalcValue().compareTo(BigDecimal.ZERO) == 0) {
            return BigDecimalCalculator.builder(BigDecimal.ZERO)
                    .scale(scale)
                    .roundingMode(roundingMode)
                    .build();
        }

        var result = this.getCalcValue().divide(other.getCalcValue(), scale, roundingMode);
        return BigDecimalCalculator.builder(result)
                .scale(scale)
                .roundingMode(roundingMode)
                .build();
    }

    /**
     * インスタンスが保持しているscaleを返却します.
     *
     * @return プロパティで保持している値
     */
    public Integer getScale() {
        return this.scale;
    }

    /**
     * インスタンスが保持しているRoundingModeを返却します.
     *
     * @return プロパティで保持している値
     */
    public RoundingMode getRoundingMode() {
        return this.roundingMode;
    }

    @Override
    public BigDecimal toBigDecimal() {
        return this.value;
    }
}

scaleと 丸めの指定を任意で出来るようにしたかったので Builderパターンを使いました。 (Builderクラスは NetBeansで自動作成したものを少しアレンジ)

public class BigDecimalCalculatorBuilder {

    private BigDecimal value;
    private Integer scale;
    private RoundingMode roundingMode;

    public BigDecimalCalculatorBuilder(BigDecimal value) {
        this.value = value;
    }

    public BigDecimalCalculatorBuilder scale(Integer scale) {
        this.scale = scale;
        return this;
    }

    public BigDecimalCalculatorBuilder roundingMode(RoundingMode roundingMode) {
        this.roundingMode = roundingMode;
        return this;
    }

    public BigDecimalCalculator build() {
        return new BigDecimalCalculator(value, scale, roundingMode);
    }

}

各種ドメインオブジェクト

計算機としての役割は、インターフェース(実質 抽象クラス)に渡した上で、ドメインオブジェクトで補完するのは「表現力」。
これはView要件という考え方も勿論あるのですが、基底となる丸めや 小数点以下の有効桁は 情報として管理したいところです。
その辺りも広く含めた「表現力」です。

Price

例えば金額クラスとして Price という基本クラスを作ってみました。
仮に金額においては除算は行わないとした場合、インターフェースの implements には Divide<T>は不要ということになります

/**
 * 単価.
 *
 * @author Yamashita.Takahiro
 */
public class Price implements Plus<Price>, Minus<Price>, Multiply<Price> {

    private static final DecimalFormat decimalFormat = new DecimalFormat("#,##0");
    private final BigDecimalCalculator calculator;

    private Price(BigDecimalCalculator calculator) {
        this.calculator = calculator;
    }

    private Price(BigDecimal value) {
        this.calculator = BigDecimalCalculator.builder(value).build();
    }

    public static Price of(BigDecimal value) {
        return new Price(value);
    }

    public static Price of(Integer value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return Price.of(bigDecimal);
    }

    public static Price of(Long value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return Price.of(bigDecimal);
    }

    static Price from(BigDecimalCalculator calculator) {
        return new Price(calculator);
    }

    public static Price ofNonFormat(String value) {
        var bigDecimal = new BigDecimal(value);
        return Price.of(bigDecimal);
    }

    public static Price ofFormatted(String value) {
        return ofFormatted(value, decimalFormat);
    }

    public static Price ofFormatted(String value, DecimalFormat decimalFormat) {
        try {
            var number = decimalFormat.parse(value);
            var bigDecimal = new BigDecimal(number.toString());
            return Price.of(bigDecimal);
        } catch (ParseException ex) {
            throw new NumberFormatException("Price could not parse value = " + value);
        }
    }

    @Override
    @SafeVarargs
    public final Price plus(Price... other) {
        return Price.from(this.calculator.plus(other));
    }

    @Override
    public Price plusAll(List<Price> others) {
        return Price.from(this.calculator.plusAll(others));
    }

    @Override
    public Price minus(Price... other) {
        return Price.from(this.calculator.minus(other));
    }

    @Override
    public <U extends Multiply<U>> Price multiply(U other) {
        return Price.from(this.calculator.multiply(other));
    }

    @Override
    public <U extends Multiply<U>, R extends CalculatorBase> R multiply(U other, Function<BigDecimal, R> newInstance) {
        var calc = this.calculator.multiply(other);
        return newInstance.apply(calc.getCalcValue());
    }

    @Override
    public BigDecimal toBigDecimal() {
        return this.calculator.toBigDecimal();
    }

    public String toNonFormat() {
        return this.calculator.toBigDecimal().toPlainString();
    }

    public String toFormatted() {
        return this.toFormatted(decimalFormat);
    }

    public String toFormatted(DecimalFormat decimalFormat) {
        return decimalFormat.format(this.toBigDecimal());
    }

//(hashCodeとequalsは省略)

}

Quantity

量は、量×量もありますし、量÷量による割合も欲しいところなので 全部載せ。

public class Quantity implements Plus<Quantity>, Minus<Quantity>, Multiply<Quantity>, Divide<Quantity> {

    private static final DecimalFormat decimalFormat = new DecimalFormat("#,##0.00");
    private static final Integer DEFAULT_SCALE = 0;
    private static final RoundingMode DEFAULT_ROUND_MODE = RoundingMode.HALF_UP;

    private final BigDecimalCalculator calculator;

    private Quantity(BigDecimalCalculator calculator) {
        this.calculator = calculator;
    }

    private Quantity(BigDecimal value) {
        this.calculator = BigDecimalCalculator.builder(value)
                .scale(DEFAULT_SCALE).roundingMode(DEFAULT_ROUND_MODE).build();
    }

    public static Quantity of(BigDecimal value) {
        return new Quantity(value);
    }

    public static Quantity of(Integer value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return Quantity.of(bigDecimal);
    }

    public static Quantity of(Long value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return Quantity.of(bigDecimal);
    }

    static Quantity from(BigDecimalCalculator calculator) {
        return new Quantity(calculator);
    }

    public static Quantity ofNonFormat(String value) {
        var bigDecimal = new BigDecimal(value);
        return Quantity.of(bigDecimal);
    }

    public static Quantity ofFormatted(String value) {
        return ofFormatted(value, decimalFormat);
    }

    public static Quantity ofFormatted(String value, DecimalFormat decimalFormat) {
        try {
            var number = decimalFormat.parse(value);
            var bigDecimal = new BigDecimal(number.toString());
            return Quantity.of(bigDecimal);
        } catch (ParseException ex) {
            throw new NumberFormatException("Quantity could not parse value = " + value);
        }
    }

    @Override
    public Quantity plus(Quantity... other) {
        return Quantity.from(this.calculator.plus(other));
    }

    @Override
    public Quantity plusAll(List<Quantity> others) {
        return Quantity.from(this.calculator.plusAll(others));
    }

    @Override
    public Quantity minus(Quantity... other) {
        return Quantity.from(this.calculator.minus(other));
    }

    @Override
    public <U extends Multiply<U>> Quantity multiply(U other) {
        return Quantity.from(this.calculator.multiply(other));
    }

    @Override
    public <U extends Multiply<U>, R extends CalculatorBase> R multiply(U other, Function<BigDecimal, R> newInstance) {
        var calc = this.calculator.multiply(other);
        return newInstance.apply(calc.getCalcValue());
    }

    @Override
    public <U extends Divide<U>> Quantity divide(U other, int scale, RoundingMode roundingMode) {
        return Quantity.from(this.calculator.divide(other, scale, roundingMode));
    }

    @Override
    public <U extends Divide<U>, R extends CalculatorBase> R divide(
            U other, int scale, RoundingMode roundingMode, Function<BigDecimal, R> newInstance) {
        var calc = Quantity.from(this.calculator.divide(other, scale, roundingMode));
        return newInstance.apply(calc.getCalcValue());
    }

    public <U extends Divide<U>> Percentage divideToPercentage(U other) {
        return this.divideToPercentage(other, this.getScale(), this.getRoundingMode());
    }

    public <U extends Divide<U>> Percentage divideToPercentage(U other, RoundingMode roundingMode) {
        return this.divideToPercentage(other, this.getScale(), roundingMode);
    }

    public <U extends Divide<U>> Percentage divideToPercentage(U other, int scale, RoundingMode roundingMode) {
        var value = this.calculator.divide(other, scale, roundingMode).getCalcValue();
        return Percentage.of(value.multiply(BigDecimal.valueOf(100)));
    }

    @Override
    public Integer getScale() {
        return this.calculator.getScale();
    }

    @Override
    public RoundingMode getRoundingMode() {
        return this.calculator.getRoundingMode();
    }

    @Override
    public BigDecimal toBigDecimal() {
        return this.calculator.toBigDecimal();
    }

    public String toNonFormat() {
        return this.calculator.toBigDecimal().toPlainString();
    }

    public String toFormatted() {
        return this.toFormatted(decimalFormat);
    }

    public String toFormatted(DecimalFormat decimalFormat) {
        return decimalFormat.format(this.toBigDecimal());
    }

//(hashCodeとequalsは省略)

}

Percentage

パーセンテージ(率)用の基本クラス。
除算は確実にありそうなので 全部載せ。
ちょっと手こずったのは率をどうやって扱うか(百分率にするか、Decimalにするか)というところ。
今回は、あえて(?)面倒くさそうな「百分率」にしてみました*3

public class Percentage implements Plus<Percentage>, Minus<Percentage>, Multiply<Percentage>, Divide<Percentage> {

    private static final DecimalFormat decimalFormat = new DecimalFormat("#0.000");

    // 小数表記で保持しているので、百分率の有効桁が3桁としたい場合、Scaleとしては2桁加算
    private static final Integer DEFAULT_SCALE = 5;
    private static final RoundingMode DEFAULT_ROUND_MODE = RoundingMode.HALF_UP;

    // 計算しやすいように少数値で保持
    private final BigDecimalCalculator calculator;

    private Percentage(BigDecimalCalculator calculator) {
        this.calculator = calculator;
    }

    public Percentage(BigDecimal value) {
        var scale = value.scale();
        var decimal = value.divide(BigDecimal.valueOf(100), scale + 2, RoundingMode.DOWN);
        this.calculator = BigDecimalCalculator.builder(decimal)
                .scale(DEFAULT_SCALE).roundingMode(DEFAULT_ROUND_MODE).build();
    }

    /**
     * 百分率の値からインスタンスを生成します.
     *
     * @param value 値(百分率)
     * @return 生成したインスタンス
     */
    public static Percentage of(BigDecimal value) {
        return new Percentage(value);
    }

    /**
     * 百分率の値からインスタンスを生成します.
     *
     * @param value 値(百分率)
     * @return 生成したインスタンス
     */
    public static Percentage of(Integer value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return Percentage.of(bigDecimal);
    }

    /**
     * 百分率の値からインスタンスを生成します.
     *
     * @param value 値(百分率)
     * @return 生成したインスタンス
     */
    public static Percentage of(Long value) {
        var bigDecimal = BigDecimal.valueOf(value);
        return Percentage.of(bigDecimal);
    }

    static Percentage from(BigDecimalCalculator calculator) {
        return new Percentage(calculator);
    }

    /**
     * 百分率表記からインスタンスを生成します.
     * <br>
     * 税率や利率のように、パーセント表記からインスタンスを生成したい場合に使用します.<br>
     * 例えば、4.75%の場合は,
     * <br> {@code Percentage.ofPercentage("4.75") } と記述します.
     *
     * @param value 百分率表記の文字列
     * @return 生成したインスタンス
     */
    public static Percentage ofPercentage(String value) {
        var bigDecimal = new BigDecimal(value);
        return Percentage.of(bigDecimal);
    }

    /**
     * 小数点表記からインスタンスを生成します.
     * <br>
     * 税率や利率を小数点で扱ったというインスタンスを生成したい場合に使用します. <br>
     * 例えば、30%の場合は,
     * <br> {@code Percentage.ofDecimal("0.3") } と記述します.
     *
     * @param value 小数点表記の文字列
     * @return 生成したインスタンス
     */
    public static Percentage ofDecimal(String value) {
        var bigDecimal = new BigDecimal(value);
        var calculator = BigDecimalCalculator.builder(bigDecimal)
                .scale(DEFAULT_SCALE).roundingMode(DEFAULT_ROUND_MODE).build();
        return new Percentage(calculator);
    }

    @Override
    public Percentage plus(Percentage... other) {
        return Percentage.from(this.calculator.plus(other));
    }

    @Override
    public Percentage plusAll(List<Percentage> others) {
        return Percentage.from(this.calculator.plusAll(others));
    }

    @Override
    public Percentage minus(Percentage... other) {
        return Percentage.from(this.calculator.minus(other));
    }

    @Override
    public <U extends Multiply<U>> Percentage multiply(U other) {
        return Percentage.from(this.calculator.multiply(other));
    }

    @Override
    public <U extends Multiply<U>, R extends CalculatorBase> R multiply(U other, Function<BigDecimal, R> newInstance) {
        var calc = this.calculator.multiply(other);
        return newInstance.apply(calc.getCalcValue());
    }

    @Override
    public <U extends Divide<U>> Percentage divide(U other, int scale, RoundingMode roundingMode) {
        var calc = this.calculator.divide(other, scale, roundingMode);
        return Percentage.from(calc);
    }

    @Override
    public <U extends Divide<U>, R extends CalculatorBase> R divide(U other, int scale, RoundingMode roundingMode, Function<BigDecimal, R> newInstance) {
        var calc = this.calculator.divide(other, scale, roundingMode);
        return newInstance.apply(calc.getCalcValue());
    }

    @Override
    public Integer getScale() {
        return this.calculator.getScale();
    }

    @Override
    public RoundingMode getRoundingMode() {
        return this.calculator.getRoundingMode();
    }

    @Override
    public BigDecimal getCalcValue() {
        return this.calculator.getCalcValue();
    }

    @Override
    public BigDecimal toBigDecimal() {
        return this.calculator.getCalcValue()
                .multiply(BigDecimal.valueOf(100));
    }

    public String toDecimal() {
        BigDecimal result = this.getCalcValue();
        return result.toPlainString();
    }

    public String toNonFormat() {
        return this.toBigDecimal().toPlainString();
    }

    public String toFormatted() {
        return this.toFormatted(decimalFormat);
    }

    public String toFormatted(DecimalFormat decimalFormat) {
        return decimalFormat.format(this.toBigDecimal());
    }

//(hashCodeとequalsは省略)
}

計算後の戻り値の型

戻り値の型は、計算元(左)の具象クラスの型による限定ができます。

つまり

量 = 量 × 率

こんな感じで戻り値の型を限定することをしたかったんです。
算数と数学の違いみたいな感じで焼け野原が出来そうな気もしますが、ここで「数値」を返却しないことで型への詰め替えを是正できます。

また、乗算と除算は戻り値の型を任意に変えたいことがあるので、そういったことにも対応できるようにしています。

それでもカバーできない場合は、実装クラス側に専用のメソッドを準備するという感じで実装していけば良いと思います。

以下は、実装の参考までにユニットテスト抜粋

@Test
public void testPlus1() {
    var quantity1 = Quantity.of(100);
    var quantity2 = Quantity.of(200);
    var actual = quantity1.plus(quantity2);
    assertEquals(300, actual.toInt());

    // 型の異なるモデルは加算不可(コンパイルエラーになる)
    // quantity1.plus(Price.of(2));
}
@Test
public void testDivideToPercentage() {
    var quantity1 = Quantity.ofNonFormat("10");
    var quantity2 = Quantity.ofNonFormat("2");

    // 内部的な数値をラップしただけなので、こうなってしまう。
    // 尺度の違うモデルを扱う場合は、個別にメソッドを準備して対応する方が良い
    var actual1 = quantity1.divide(quantity2, Percentage::of);
    assertEquals(Percentage.of(5), actual1);

    // 戻り値の型 Percentage のためのメソッドを準備
    var actual2 = quantity1.divideToPercentage(quantity2);
    assertEquals(Percentage.of(500), actual2);
}

GitHub(コード全量)

GitHub - vermeerlab/domains at bigdecimal

さいごに

初めはインターフェースのdefaultに実装をして継承をベースにしたのですが、乗算と除算のところで躓きました。
戻り値の型を状況によって変えたいことを鑑みて、継承より委譲の方が見通しが良いだろうという判断に至りました*4

*1:という形の時間切れ

*2:MITライセンス

*3:案の定、面倒でしたが良い素振りにはなったように思います

*4:加算と減算は良かったので、そこだけ残しても良かったのですが 継承と委譲の混在は避けることにしました

Swagger/OpenAPIのメモ

SpringFox

SpringFox by springfox

メモ

ぱっと調べると SpringでSwaggerは SpringFox くらいの 感じだったし、既存のプロジェクトでも使っていたから採用はしたものの、最新バージョン(3_0_0)にすると SpringBoot2.6系にしたら 起動が出来なかった。

swagger-starter3.0 not support springBoot2.6.x version · Issue #3934 · springfox/springfox · GitHub (他にも類似のissueがあった気もするけど見つけられなかった)

メンテナンスも止まっているみたい(最終コミットが 2020年10月)

Commits · springfox/springfox · GitHub

positon の順番が効かないとかもあるし、このまま使い続けるのは良くないかも。。

リンク

とはいえ、色々と参考になる(調べるときのヒントになる)かもしれないので気になる情報はメモしておく。

SpringFoxでのSwaggerのリクエスト・レスポンス説明の共通化 | GROUP DEV BLOG | TECHNO DIGITAL

springdoc-openapi

OpenAPI 3 Library for spring-boot

メモ

SpringFoxよりも こっちを使うようにした方が良いかも、、 ということで まずは情報収集。 少なくとも Swagger用ライブラリで SpringBootのバージョンアップの弊害になっているのは困る。
私のプロジェクトは始まったばかり。今なら間に合う。

リンク

SpringFoxからspringdoc-openapiに移行してみた #SpringBoot - Qiita

OpenAPI 文書のカスタマイズ

Documenting a Spring REST API Using OpenAPI 3.0 | Baeldung

@RestControllerAdvice and @ResponseStatus(HttpStatus.OK) overriding default response for @RestController methods · Issue #565 · springdoc/springdoc-openapi · GitHub

Configure default response Message · Issue #381 · springdoc/springdoc-openapi · GitHub

springdoc-openapiでOpenAPI形式のAPIドキュメントを生成する - BullよりElk

Springdoc Openapiで全APIに認証の設定(security scheme)を指定する方法 #SpringBoot - Qiita

OpenAPI(Jakarta EE/MicroProfile OpenAPI)

リンク

みんなのためのMicroProfile OpenAPI - 赤帽エンジニアブログ

OpenAPI (Swagger) 3.0 で Bearer トークンの使用を定義する | Articles | Riotz.works

Eclipse MicroProfile JWT Authentication API :: Payara Enterprise Documentation

MicroProfile OpenAPIによるAPI管理(Part2):MicroProfile OpenAPI annotationを利用しドキュメント化する|富士通技術者ブログ~Javaミドルウェア~ : 富士通

コードサンプル

https://github.com/extact-io/msa-rms-platform/blob/main/platform-fw/src/main/java/io/extact/msa/rms/platform/fw/webapi/server/CommonOpenApiModelReader.java

Payara-Examples/microprofile/openapi-example at dc53549a804017f7690450b9f897f4c7f9aabf07 · payara/Payara-Examples · GitHub

openapi-ext/openapi-examples/basic-example/pom.xml at main · microprofile-extensions/openapi-ext · GitHub

OpenAPI Generator

OpenAPI Generatorに適したOpenAPIの書き方 - ZOZO TECH BLOG

パッケージ構成の考察(3)

はじめに

過去2回のパッケージ構成について

vermeer.hatenablog.jp

vermeer.hatenablog.jp

約3年ぶりに改めて考察してみようかと。
DDD関連に影響は受けていると思いますが別物です。
3層+ドメイン を基本とした構成です。

なぜ改めて見直すのか?

Reactなどの開発や参考になる情報も増えてきたり、サーバーサイドについてもWebAPIを作成したり、Spring関連*1の勉強もしたりするの中で、以前の整理では ちょっと良くないなと思うところが自分で読み返しても感じたため。
特に WebAPI(RESTful API)については全く考えていませんでした。

パッケージ構成図

https://raw.githubusercontent.com/vermeer-1977-blog/blog-parts/main/package-%20examination/img/package.drawio.svg

ブラウザで見るには小さいのでリンクからsvgファイルを取得してください。
https://raw.githubusercontent.com/vermeer-1977-blog/blog-parts/main/package-%20examination/img/package.drawio.svg

シーケンス

https://raw.githubusercontent.com/vermeer-1977-blog/blog-parts/main/package-%20examination/img/layer-sequence.drawio.svg

https://raw.githubusercontent.com/vermeer-1977-blog/blog-parts/main/package-%20examination/img/layer-sequence.drawio.svg

所感的な補足

baseとapps

分類として横断的機能と業務遂行の中心となるパッケージを分けました。
非機能要件と機能要件を分けるというのも類似です。
技術要素によったパッケージとして coreでも良いのですが中心というイメージではないかな、と。
また意図としてはshareも近い気がしますが、そういう横断的ではなくて箱庭の枠みたいなイメージで同列性を感じない表現が近しいイメージです。

複数のサービスを参照するものの分類は設けない。

複数のサービスの参照じゃなくなったときに命名やパッケージを変更するというのは、なんか違うかな、と。 そういう「複数のサービスを利用している」「別のサービスからも利用されている」というのは、コード分析(JIGみたいな)で取得して把握すれば必要十分だろうということで設けないことにしました。

RepositoryはDomain以外の場所に配置(Application層 or 独立させる)

ドメインオブジェクトから参照はしないから。 何か関数的な操作をRepositoryに「させる」ことはあっても、Repostoryがドメイン内の操作を「する」ことはない。 もし「ある」場合は、ドメインからIO(副作用)があることになりかねない。
逆に非常に簡易的な参照系システムの場合 Presentation層から直接 Repositoryを使用することもアリかなと思ってもいます(トランザクション境界が不要だから)。
となると、RepositoryはPresentation層から使用も出来るし、Application層からも使用できる場所に配置させるのが妥当ではないかな?という結論に至りました。

Service内でのif分岐はあり

構成図には記載していませんが、かつては、if分岐すらも無しに出来ないかな?と思ったことがあります。
ですが、UMLユースケースでも 代替フローがあるので、さすがにやりすぎだと今は思っています。

Serviceの命名は異論あるかも

個人的にはサービスは動詞がアリだと思うのですが ちょっと万人向けじゃないかもなぁと思っています。
とはいえ ユースケース記述に近い実装のやりやすさを考えると、クラス名もユースケース記述に近い「動詞」が良いんじゃないかなぁと。 ということで、ここは以前の考えのままにしました。

Repositoryに重複データ検証用の問い合わせを設ける

1つの理由は上述の「RepositoryはDomain以外に配置」です。

またドメインサービスの例として重複データ検証があると思います。 重複検証がエンティティに実装する振舞いには不適当だからという文脈からいう理解をしています。 つまり「永続情報から何をもって重複判定を行うのか?」という知識を どこまでコード側(Domain)で扱うか?というところだと思います。 僕は「Repositoryのメソッド名による表明で必要十分」という整理に落ち着きました。 たとえば、Repositoryを経由して問い合わせる先がDBではなく外部APIだったらどうでしょうか? そもそもそういった問い合わせ先の具体的な詳細を隠蔽することがRepositoryを用いたDIPの目的であると考えています。

さて、そうするとドメインサービスはいらないの?という問いが出てきます。
僕としては そもそも僕自身がドメインサービスの理解を誤っていた、というのが結論です。 ドメインサービスは

  • 状態を持たない関数として実装する
  • 複数ドメインオブジェクトを組み合わせて表現されるルールを扱う
  • 複数のドメインオブジェクトを扱うにあたって、どちらのドメインオブジェクトにロジックを配置するのか悩ましいときの解決方法(つまり、どちらにもロジックを配置しない)

こう理解をすると、IOは不要でも違和感はありません。

きっかけは 以下の指摘で理解が進んだ気がという感じです。

なお、僕はドメインサービスという名称はアプリケーションサービスもあって分かりにくいので「ルール(Rule)」としています。

Factoryを省略

集約ルートにメソッドを準備する、クラスとして独立させる、ということは目的と規模に応じて適宜判断するものであると考えました。
何かしら必ず作成するようなものではないので、構成図からは外すことにしました。

ライセンス

構成図は MITなので ご自由に使用してください。
Draw.ioがあれば 修正も出来ます。
なお 説明、パッケージオブジェクト、下地の3つのレイヤーにしてロックしているので、編集時には適宜 レイヤーのロックを解除してください。

参考

GitHub - system-sekkei/isolating-the-domain: architecture sample using : Spring Boot gradle, Spring MVC, Thymeleaf, and MyBatis

Atomic Designをやめてディレクトリ構造を見直した話|食べログ フロントエンドエンジニアブログ|note

さいごに

Draw.io を svg形式で保存できるのは本当に便利。

*1:Spring Bootとか Spring Securityとか

Draw.ioをGitHub管理して画像を埋め込む

はじめに

ブログなどのMarkdownドキュメントにフロー図などを書いた画像を埋めたいなぁと思っているときに、Draw.ioで画像拡張子(png/svg)で編集情報を保持したままで管理できるということを知りました。
ということは、ひょっとして埋め込み画像を直接編集して Gitなどでバージョン管理も出来るのでは?と思い至り試してみました。

何が嬉しいの?

  • ドキュメントに埋め込んだ画像の更新のためにエクスポート・アップロードをしなくて良くなる。
  • Draw.ioはローカルで編集できるのでGit管理ができる。

環境

書かないこと

  • Draw.ioの具体的な使い方

やってみた

Draw.ioファイルを作成

新規ファイル作成するときに拡張子を.dioで作成したら Draw.ioファイルとして認識してくれます。
何か描きます。

f:id:vermeer-1977:20220109122033p:plain

画像形式に変換

[ファイル] > [Convert ...] > [.drawio.svg]
変換したら、svgファイルとしてVS Code上表示されますが 改めてVS Code内で ファイルを開きなおすと ちゃんとDraw.ioファイルとして表示されます。

変わったのは拡張子だけ、という感じ(実際は画像ファイルに Draw.io用の情報が埋め込まれている)

f:id:vermeer-1977:20220109122314p:plain

GitHubに上げて確認

ちゃんと画像として読み込めていることも確認。

f:id:vermeer-1977:20220109130015p:plain

ブログなどに埋め込む

埋め込むURLとして[Raw] からリンクを取得

https://github.com/vermeer-1977-blog/blog-parts/raw/main/drawio-in-blog/test.drawio.svg

以下は、更新編集前のブログにリンクを貼った状態。
ちょっとごちゃついているけど、こんな感じで画像としてブログでも確認できる。

f:id:vermeer-1977:20220109131053p:plain

Draw.ioファイルを更新してGitHubに上げる

以下の画像は上述の手順で更新した GitHub管理下のDraw.io(svg)

https://raw.githubusercontent.com/vermeer-1977-blog/blog-parts/main/drawio-in-blog/test.drawio.svg

上記の通り、自動で更新される。

参考

https://tech.fusic.co.jp/posts/2021-06-15-vscode-draw-io-integration/

https://zenn.dev/kaakaa/articles/qiita-20200728-ff9ab5933cc0fb6fd8d8

さいごに

既存のDraw.ioファイルを変換しても同じことが出来ると思います。

これまでは GoogleDriveで お絵描きをして公開設定をしてリンクを貼っていましたが、これでブログ(に限らず)Git管理下に置けそうです。
Draw.ioはオフライン編集が出来るので、色々とやれることは多そうです。

シーケンス図やクラス図を書くときに、PluntUMLを使ってテキストベースでGit管理をして、それをそのまま画像変換するWebサービスみたいなものがあるのは見たことがある気がします。
もちろんそういうアプローチも良いと思いますが、お絵描き系は やっぱりGUIで描画できるのが直観的で私はDraw.ioの方が好きです*1。 あと、Draw.ioはXML形式で構造的にデータを保持しているのでプラグインとかアドオンとか何か情報取得をするときにやりやすそうな印象も受けました。

*1:PluntUML 毎度毎度 入門と中退を繰り返して一向に履修も卒業も出来る気がしないヨワヨワです

LocalStorageを扱うHooks

何が嬉しいの?

Windowをまたがった情報共有ができる。
ただし多用すると状態管理が散乱するので避けた方良い。

実装

import { useEffect, useState, useRef } from 'react';

const useCrossWindowState = <T>(
  stateKey: string,
  defaultValue: T,
): [state: T, setState: (state: T) => void] => {
  const [state, setState] = useState<T>(defaultValue);
  const isNewSession = useRef(true);

  useEffect(() => {
    if (isNewSession.current) {
      const currentState = localStorage.getItem(stateKey);
      if (currentState) {
        setState(JSON.parse(currentState));
      } else {
        setState(defaultValue);
      }
      isNewSession.current = false;
      return;
    }
    try {
      localStorage.setItem(stateKey, JSON.stringify(state));
    } catch (error) {}
  }, [state, stateKey, defaultValue]);

  useEffect(() => {
    const onReceieveMessage = (e: StorageEvent) => {
      const { key, newValue } = e;
      if (key === stateKey) {
        setState(JSON.parse(newValue ?? ''));
      }
    };
    window.addEventListener('storage', onReceieveMessage);
    return () => window.removeEventListener('storage', onReceieveMessage);
  }, [stateKey, setState]);

  return [state, setState];
};

export default useCrossWindowState;

使い方
useStateと同じ感じ。

const [volume, setVolume] = useCrossWindowState<number>('audioVolume', 15);

参考

react-cross-windows-state - CodeSandbox

さいごに

TypeScript対応をするときの型を探すのが地味に手間。
でもanyに逃げない*1

*1:今回だとStorageEvent