ぺんぎんらぼ

お笑いとマンガ好きなしょぼしょぼWeb系エンジニアの日記です。たまに絵を描きます。

お笑いとマンガ好きなしょぼしょぼWeb系エンジニアの日記です

Spring Tips - DI 依存性の注入のメリット

Spring Frameworkに限らず、Javaフレームワークといえば、この「DI - 依存性の注入」というものが用意されていて、このDIを使用することが前提になっています。

このDI、みなさん理解していますか?

  • フレームワークの作法だから、なんとなくDIを使ってる。
  • DIを使うことのメリットがわからない。
  • newすればいいんじゃないの?

こんな風に思っている方、いませんか?
語りつくされている感はありますが、あらためて、DIのメリットについて難しい話は抜きで考えてみましょう。

「依存性の注入」ってなに?

DIは「依存性の注入」と説明されることが多いです。耳タコです。
では、「依存性の注入ってなに?」と聞かれて、答えることができますか?意外と答えられない人が多いと思います。
Javaの用語を無理やり日本語にして、逆にわかりにくくなるケースです。マツキは、この「DI = 依存性の注入」と「Serialize = 直列化」が特にわかりにくい日本語だと思ってます。英語のままでいいじゃん!!

では、分かりやすく、「依存性」と「注入」をそれぞれ理解していきましょう。

「依存性」ってなに?

これは、あるプログラムが別のプログラムに依存していることを表しています。
分かりやすく、具体例で説明しましょう。

DIを使うとき、interfaceクラスを定義して、そのインターフェースクラスの実装クラスを実装することが多いと思います。理由は後で説明しますが、この作法どおり、インターフェースクラスを作ります。

public interface 乗り物 {
    double 距離から時間を取得(double 距離);
}

そして、実装クラスを実装します。

public classimplements 乗り物 {
    private static int 時速 = 40;

    @Override
    public double 距離から時間を取得(double 距離) {
        return 距離 / 時速;
    }
}

作成したインターフェースクラス「乗り物」を使用する「時間」クラスを作ります。

public class 時間 {
    public double 乗り物でかかる時間(double 距離) {
        乗り物 使う乗り物 = new 車();

        double かかる時間 = 使う乗り物.距離から時間を取得(距離);

        return かかる時間;
    }
}

DIを使用していない、普通のJavaプログラムです。
ポイントは以下の一行に集約されています。

乗り物 使う乗り物 = new 車();

「乗り物」インターフェースで変数を宣言して、「車」実装クラスのインスタンスを代入しています。
java.util.Listを使うときの作法と同じですね。

List list = new ArrayList();

インターフェースクラスで変数を宣言して、実装クラスのインスタンスを代入します。
これは、実装を簡単に切り替えることができるようにする、デザインパターンになります。
例えば、「乗り物」インターフェースを実装した「自転車」実装クラスを作って、インスタンスの代入を次のように変えるだけで、「車」から「自転車」に変更することができます。

乗り物 使う乗り物 = new 自転車();

このインスタンス生成部分以外は、「乗り物」が「車」なのか「自転車」なのかは意識する必要がないのです。

では、「依存性」に話を戻します。この「時間」クラスは「乗り物」インターフェースを使って、距離から時間を求めています。なので、「時間」クラスは「乗り物」インターフェースに依存しています。そして、「車」実装クラスのインスタンを生成しているので、「車」実装クラスにも依存しています。

「時間」クラスは「乗り物」インターフェースと「車」実装クラスに依存しているのです。これが依存性です。

「注入」ってなに?

「依存性の注入」の「注入」は、「インスタンスの代入」と考えてよいです。先ほどの「時間」クラスをSpringのDI風に書き換えると次のようになります。

public class 時間 {
    @Autowired
    private 乗り物 使う乗り物 ;

    public double 乗り物でかかる時間(double 距離) {
        double かかる時間 = 使う乗り物.距離から時間を取得(距離);

        return かかる時間;
    }
}

「乗り物」インターフェースのフィールドを宣言して、@Autowiredアノテーションを指定しています。
@Autowiredアノテーションを指定すると、DIの仕組みで、そのフィールドに実装インスタンスが代入されるのです。
つまりは、このインスタンスの代入が注入なのです。

代入される実装のほうは、そのクラスのインスタンスがDIで管理されるよう、@Componentアノテーションを指定します。

@Component
public classimplements 乗り物 {

DIのメリット

本題のDIを使った時のメリットです。

依存を減らせる

「時間」クラスの依存性をDIを使う前と使った後で比較してみましょう。
DIを使う前は、「乗り物」インターフェースと「車」実装に依存していました。DIを使うことで、実装への依存がなくなり、「乗り物」インターフェースへの依存のみになりました。
依存性が下がると、依存先の変更の影響を受けにくくなることがメリットですが、もともとインターフェースクラスを利用しているので、DIを使用したからといっても依存先の変更の影響を受けにくくなったわけではありません。
依存を減らしたメリットは、この後に説明する「実装の切り替えが容易になる」につながるメリットです。

実装の切り替えが容易になる

実装乗り切り替えが容易となるよう、インターフェースを使用していますが、DIを使用すると、さらに実装の切り替えが容易になります。 例えば、テスト時はスタブ実装に切り替えたい、ということがあると思います。DIを使わない場合、テスト時だけnew実装クラスを切り替えるコードを書く必要があります。

環境変数「profile」の内容によって実装を切り替えるのであれば、以下のようなコードになります。

乗り物 使う乗り物;
if (System.getenv("profile").equals("test")) {
    使う乗り物 = new テスト用の乗り物();
} else {
    使う乗り物 = new 車();
}

このようなコードを切り替えが必要なすべての箇所に実装する必要がありますし、プログラム中にテストのためのロジックが入り込むのは良くありません。

Spring FrameworkのDIでは、実装クラスを容易に切り替える仕組みが用意されています。

DIする側は、普通にDIするコードを書くだけです。

@Autowired
private 乗り物 使う乗り物 ;

DIされる実装が複数あった場合に、どれがDIされるかを決定するための定義が必要になります。それぞれの実装に@Profileアノテーションでプロファイル名を定義します。

@Component
@Profile("default")
public classimplements 乗り物 {
@Component
@Profile("test")
public class テスト用の乗り物 implements 乗り物 {

後はプログラムの実行時に環境変数や、プロパティファイルに定義されたspring.profiles.activeの内容によって、DIされる実装クラスが切り替わります。
この仕組みを使うと、実装の切り替えロジックを実装する必要はなくなりますし、複数のインターフェースの実装を一括して切り替えることが容易になります。

オブジェクトのライスサイクルの管理をDIがやってくれる

オブジェクトのライフサイクルとは、そのオブジェクトが生成されてから破棄されるまでのことです。
「そんなのnewしてから、参照が外れてGCされるまで」と答える人もいるでしょう。間違いではありませんが、ライフサイクルをプログラムで制御するケースもあります。
分かりやすいものでは、シングルトンパターンのオブジェクトです。オブジェクトが必要になったとき、newして新しいインスタンスを生成するのではなく、あらかじめ生成されている一つのインスタンスを共有しよう。というものです。

このシングルトンパターンのライフサイクルを使用する場合、シングルトンとなるクラスでnewを禁止し、getInstanceのようなシングルトンのインスタンスを取得するメソッドを用意する必要があります。そして、シングルトンとなるクラスを利用する側も、newでインスタンスを生成するのではなく、getInstanceメソッドを呼び出してインスタンスを取得するように実装します。

DIを使用すると、オブジェクトのライフサイクルを管理してくれますし、ライフサイクルに関する実装を省略することができます。

DIする側は、普通にDIするコードを書くだけです。

@Autowired
private 乗り物 使う乗り物 ;

DIされる側の実装は、ライフサイクルに関する実装は不要で、そのクラスのオブジェクトのライフサイクルを定義するだけです。

このライフサイクルのことを「スコープ」と言い、Spring Frameworkでは、スコープの定義を省略すると、シングルトンになります。(明示的にシングルトンであることを定義することもできます)

Springでは、@Scopeアノテーションでスコープを定義します。

シングルトンスコープであれば、次のようになります。

@Component
@Scope("singleton")
public class テスト用の乗り物 implements 乗り物 {

DIのたびに新しいインスタンスを生成(new)するのであれば、次のようになります。

@Component
@Scope("prototype")
public class テスト用の乗り物 implements 乗り物 {

DIがライフサイクルの管理をしてくれるので、ライフサイクルに応じた実装が不要であることがわかります。
ライフサイクルの実装が不要になり、かつ、ライフサイクルの変更が容易になることがメリットとなります。

まとめ

以上がDIの利点です。
最後に簡単にメリットのまとめとユースケースをまとめます。

実装の切り替えが容易になる

実行環境やテスト時など、一部のロジックの切り替えが容易になります。
例えば、月末に特殊な処理が動くようなロジックをテストしようとすると、月末まで待つか、PCの日付を変更する必要がありますが、日付の取得をロジックに切り出し、そのロジックを切り替えることで特定の日付を常に返すことが可能になります。

ライフサイクルの管理をしてくれる

デフォルトでは、DI管理されているインスタンスはシングルトンになります。これは無用なメモリ消費やインスタンスを都度、生成する実行コストを削減する効果があります。
半面、シングルトンでは状態を持つことができません。クラスにインスタンスのフィールドを定義しすると、そのフィールドもすべての処理で共有されてしまうためです。
開発をしていて、インスタンスに状態を持たせる必要が出たときに、そのクラスのスコープを変更するだけで、ライフサイクルが切り替わるので、最小のプログラム変更でスコープを変更することができます。

Spring BootでテストするWebアプリケーション① - Gradleプロジェクトの編集

Spring Bootシリーズ。今回はユニットテストです。

Springでは、テスト用のコンポーネントが用意されていて、Spring Bootではテストの実装も楽に実装できるようになっています。
今回はWebアプリケーションのコントローラをSpring Test + JUnit 5でユニットテストを実装します。

ユニットテスト(単体テスト)の単位

テスト対象とする単位をどうするか。テスト計画を立てるときに、この単位を決める必要があります。ユニットテストのユニットの意味は「単位」なので、定められた単位ごとにテストをする。これがユニットテストです。

メソッド単位

Javaであれば、一番小さい単位はメソッドですね。このメソッドをテスト単位とする、と解説しているサイトや参考書が多いように感じます。

publicメソッド単位

もう少し粗い単位として、publicメソッド単位とすることもあります。同一クラス内の共通処理をprivateメソッドで切り出した場合、そのメソッドは、呼び出し元のpublicメソッドの一部としてまとめてテストしてしまおう、というものです。

各層のコンポーネントのpublicメソッド単位

さらに粗い単位として、Web三層アプリケーションの各層のpublicメソッド単位とするものです。アプリケーション層のコントローラクラス、ドメイン層のサービスクラス、インフラストラクチャー層のリポジトリークラスのpublicメソッドをテスト対象の単位とします。

処理単位

そして一番粗い単位として、一連の処理を単位とするものです。Web三層アプリケーションであれば、コントローラクラスからサービスクラス、リポジトリークラスまでの呼び出しを一つの単位として、まとめてテストしてしまう方法です。

どのテスト単位を使うべきか

単位の粒度が粗くなるほど、テストの内容も粗くなります。
また、粗くすると、一つのテストケースに対するプログラムの範囲が広がるので、プログラムの修正で影響が及ぶテストケースが増えて、テストケースの修正範囲が広がります。
利点としては、単位が細かいほど、ユニットテストの実装工数が高くなります。

Web三層アプリケーションであれば、単位を「各層のコンポーネントのpublicメソッド単位」にするのが、テスト精度、実装工数のバランスが良いと感じます。

SIerの実際の現場では、工数優先で「処理単位」でテストをしている現場が多いのが残念ですが・・・

依存ライブラリの定義

では、テストを実行するために必要な依存ライブラリをbuild.gradleに定義します。

plugins {
    id 'java-library'
    id 'org.springframework.boot' version '2.4.5'
}
apply plugin: 'war'
apply plugin: 'eclipse-wtp'
apply plugin: 'io.spring.dependency-management'

sourceCompatibility = '1.8'
targetCompatibility = '1.8'

compileJava.options.encoding = 'UTF-8'
compileTestJava.options.encoding = 'UTF-8'

eclipse.wtp {
    component.contextPath = 'penguin-web'
    facet {
        facet name: 'jst.java', version: '1.8'
        facet name: 'wst.jsdt.web', version: '1.0'
        facet name: 'jst.web', version: '3.1'
    }
}

repositories {
    jcenter()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'

    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'junit', module:'junit'
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'org.mockito:mockito-inline:3.6.28'
    testImplementation 'net.java.dev.jna:jna-platform:5.8.0'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

test {
    useJUnitPlatform()

    testLogging {
        showStandardStreams = true
        events = ['started', 'skipped', 'passed', 'failed']
        exceptionFormat = 'full'
    }
}

今回追加したところを一つ一つ見ていきましょう。

1. テストライブラリの追加

依存関係に「spring-boot-starter-test」を追加することで、テストで必要なライブラリがまとめて追加されます。

    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'junit', module:'junit'
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }

mavenリポジトリのサイトを確認すると、この依存関係でいくつかのライブラリが追加されることがわかります。

f:id:penguinlabo:20210314024941p:plain

アサーションのライブラリである「AssertJ」や「Hamcrest」、モック化のライブラリである「mockito」、そして、JUnit本体である「JUnit Jupiter」が追加されることがわかります。

Spring Boot Starter Testのバージョン2.2から、JUnit 4の依存が削除され、代わって、JUnit 5 (Jupiter)の依存が追加されました。
しかし、「AssertJ」の依存関係に、JUnit 4の依存関係が含まれているため、JUnit 4のライブラリも追加されてしまいます。
JUnit 4を使わなければいいだけの話しなんですが、テストメソッドに使用する@TestアノテーションJUnit 4とJUnit 5の両方にあって、誤ってimportすると事故の元なので、excludeでJUnit 4のライブラリを除外指定しています。

2. モックライブラリの追加

依存関係に「spring-boot-starter-test」を追加することで、モック化のライブラリである「mockito」が追加されます。
しかし、追加される「mockito-core」だけでは、finalメソッドやstaticメソッドのモック化はできません。「mockito-inline」ライブラリを依存関係に追加することで、finalメソッドやstaticメソッドのモック化が有効化されます。

    testImplementation 'org.mockito:mockito-inline:3.6.28'

ただし、「mockito-inline」ライブラリを追加した状態で、テストクラスをJRE (Java Runtime Environment)で実行すると、次のような例外が発生します。

Caused by: org.mockito.exceptions.base.MockitoInitializationException: 
Could not initialize inline Byte Buddy mock maker.

It appears as if you are running on a JRE. Either install a JDK or add JNA to the class path.
Java               : 16
JVM vendor name    : AdoptOpenJDK
JVM vendor version : 16+36
JVM name           : OpenJDK 64-Bit Server VM
JVM version        : 16+36
JVM info           : mixed mode, sharing
OS name            : Windows 10
OS version         : 10.0

    at org.mockito.internal.creation.bytebuddy.InlineByteBuddyMockMaker.<init>(InlineByteBuddyMockMaker.java:234) ~[mockito-core-3.6.28.jar:na]

このエラーを解消する方法は、例外メッセージにある通り、テストクラスをJREではなくJDK (Java Development Kit)で実行するか、JNA (Java Native Access)ライブラリをクラスパスに追加する必要があります。

実行方法に制約を持たせたくはないので、JNAを追加して、JREでもJDKでも実行できるようにします。

    testImplementation 'net.java.dev.jna:jna-platform:5.8.0'
3. テストランチャーの追加

テストを起動するときに使用するテストランチャーを依存関係に追加します。
テストコードの実装時ではなく、テストの起動時のみ必要なライブラリなので、依存関係をtestRuntimeOnlyで指定しているところに注意が必要です。

    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
4. gradle test実行時の設定

これまでの設定で、eclipseJUnitを動かすことは可能ですが、gradle testコマンドでJUnitを動かすときは、以下の設定が必要になります。

test {
    useJUnitPlatform()

    testLogging {
        showStandardStreams = true
        events = ['started', 'skipped', 'passed', 'failed']
        exceptionFormat = 'full'
    }
}

もう少し、細かく見ていきましょう。

以下の設定は、gradle testでJUnitを使用することを指定しています。この設定がないと、JUnitのコードは実行できないので、必須の設定となります。

    useJUnitPlatform()

以下の設定は必須ではありませんが、gradle testを実行するのであれば、入れておいたほうが良いでしょう。
この設定がないと、gradle testを実行してもコンソールはテストが成功したか失敗したかの情報しか表示されず、テスト結果の詳細は、build/reports/tests/testに出力されるテストレポートを確認することになります。

    testLogging {
        showStandardStreams = true
        events = ['started', 'skipped', 'passed', 'failed']
        exceptionFormat = 'full'
    }

showStandardStreams = true

テスト実行時の標準出力、標準エラー出力がコンソールに表示されるようになります。

events = ['started', 'skipped', 'passed', 'failed']

各テストメソッドの開始。スキップ、成功、失敗がコンソールに表示されるようになります。
結果だけが必要であれば、'passed'と'failed'だけを指定します。

exceptionFormat = 'full'

例外発生時にすべての情報が表示されるようになります。

build.gradleの変更を反映

build.gradleを編集したので、編集結果をeclipseに反映しましょう。
プロジェクトのトップ(プロジェクト名の部分)、もしくはbuild.gradleの上で右クリックして、表示されたメニューの[Gradle] - [Gradleプロジェクトのリフレッシュ]をクリックします。

次回予告

今回は、Spring Bootアプリケーションのユニットテストを実装・実行するために必要なライブラリの追加や、実行環境の整備をしました。
次回から、いよいよユニットテストを実装します。はじめはコントローラーのユニットテストを実装します。
では、次回の「Spring BootでテストするWebアプリケーション② - コントローラーのテスト」でお会いしましょう!!

EclipseでGit超入門 - リモートブランチの追加

Gitを使うのは初めて。EclipseからGitを使うことになった。そんなあなたのためのEclipseでGit超入門です。
難しいことは説明せず、必要なことだけを解りやすく説明していきます。

eclipseにリモートブランチを追加

f:id:penguinlabo:20210307173712p:plain

Gitは、Subversionなどの以前のバージョン管理システムと比較して、ブランチの管理が容易であることから、いくつかのブランチを作成して、ブランチを切り替えながら開発をすることが多くなりました。

今回は、eclipse上で、Gitサーバに新たに作成されたブランチに切り替える方法を解説します。

リモートブランチはどこ?

では、eclipseからリモートブランチを見る方法はご存じでしょうか?
真っ先に思いつくのは、Gitビューの「リモート・トラッキング」ですね。実際、ローカルブランチにファイルを取り込むときは、この「リモート・トラッキング」を使用します。

f:id:penguinlabo:20210307175124p:plain

そうすると、追加されたリモートブランチも、この「リモート・トラッキング」に表示されそうです。

f:id:penguinlabo:20210307175250p:plain

リモート・トラッキング ≠ リモートブランチ

リモート・トラッキングはリモートブランチとは別物なんです。
リモートブランチを追跡(トラッキング)しているものが「リモート・トラッキング」になります。
そして、この「リモート・トラッキング」はローカルリポジトリ、つまり、Gitサーバではなく、皆さんのPCの中にあるものです。

f:id:penguinlabo:20210307203958p:plain

この図のように「リモートブランチ」と「ローカルブランチ」の仲介役が「リモート・トラッキング」になります。
feature/#3ブランチは、リモートリポジトリにあるだけなので、まずは、このfeature/#3ブランチをトラッキングする「feature/#3のリモート・トラッキング」を作成する必要があります。

リモート・トラッキングを追加

Gitビューの対象リポジトリで右クリック、表示されたメニューから[リモート] - [フェッチ]をクリックします。

f:id:penguinlabo:20210307205905p:plain

「ソースGitリポジトリー」ダイアログが表示されます。
特に変更は必要ありません。「構成済みリモート・リポジトリー」が選択されている状態で「次へ」ボタンをクリックします。

f:id:penguinlabo:20210307210550p:plain

「参照指定のフェッチ」ダイアログが表示されます。
「作成/更新仕様の追加」の「ソース参照」をプルダウンすると、リモートブランチが一覧されるので追加するブランチを選択して、「仕様の追加」ボタンをクリックします。

f:id:penguinlabo:20210307211856p:plain

ブランチが「フェッチの仕様」一覧に追加されるので、「更新の強制」と「'origin'構成に仕様を保存」チェックボックスをチェックして、「完了」ボタンをクリックします。

f:id:penguinlabo:20210307212436p:plain

リポジトリーツリーのリモート・トラッキングに追加したブランチに対応するリモート・トラッキングが追加されます。

f:id:penguinlabo:20210307212809p:plain

リモートブランチをチェックアウト

あとは、リモートブランチをチェックアウトして、ローカルブランチを作成・切り替えするだけです。

チェックアウトするリモートリポジトリのリモート・トラッキングで右クリックし、表示されたメニューの「チェックアウト...」をクリックします。

f:id:penguinlabo:20210307213219p:plain

「リモート追跡ブランチのチェック・アウト」ダイアログが表示されるので、「新規ローカル・ブランチとしてチェック・アウト」ボタンをクリックします。

f:id:penguinlabo:20210307213445p:plain

「ブランチの作成」ダイアログが表示されます。
特に変更の必要はないので、そのまま「完了」ボタンをクリックします。

f:id:penguinlabo:20210307213657p:plain

リポジトリーツリーのローカルにチェックアウトしたブランチが表示され、ブランチアイコンにチェックマークがついています。

f:id:penguinlabo:20210307213844p:plain

これで、リモートブランチからローカルブランチを作成とブランチの切り替えが完了となります。

2020年はリモートワークのおかげで社会人になってから一番幸せな年だった

f:id:penguinlabo:20201231213445j:plain

あと数時間で2020年が終わろうとしている・・・。

今年のビッグイベントはやはり「コロナ」でしたね。

2020年1月、新型のインフルエンザは武漢だけの話で、
2月になっても会社の同僚とは「大丈夫なんじゃないの?」みたいな会話をしていました。
まさかここまでの、歴史に残る感染症になるとは思いもよりませんでしたね。

東京都では4月に感染症者が100名を超え「緊急事態宣言」を出しましたが、
晦日の今日、なんと東京都の新規感染者数は1300人・・・。
政府も汚職事件が多いわ、コロナ対策は愚策が多いわで、
世の中的には明るい年ではありませんでしたね。

さて、マツキの一年はというと。

  • 会社で使っている端末がWindows -> Macになった。
  • 転職してからずっとお世話になっていた上司が変わった。
  • その上司とAWSのイケイケなイベントに参加した。

ん?!
書き出して気づく、大したことしてねぇぇ!!

まぁ自分自身は大した変化や成長はなかったのですが、やはり環境は大きく変わりましたね。
自分の会社も、4月に緊急事態宣言を受けて全社リモートワークが導入されたのですが、
そのおかげで生活の質は格段に向上しました。

たくさん寝て、本や漫画をたくさん読めて、お料理も頑張って。
旦那もリモートになったおかげで、それまで平日はいつもピリピリし疲弊していたのですが、少し余裕がうまれたようで、夫婦仲もよくなったりしました。
(その後、旦那の会議中に、私の生活音が入るとかで怒られたりもしていますが、それでも出社している時よりは随分とよくなりました。。)

コロナ後も、リモートワークが続くといいな・・・。

2020年下半期 面白かったおすすめマンガ10選

漫画が好きだ。 以下略。
恒例の2020年下半期『面白かったマンガランキング ベスト10』をまとめました。

1. 勇者が死んだ!

ギャグ × バトル × ファンタジーの傑作!
主人公は特別なスキルを何も持たない農夫。
この農夫が作った落とし穴に、勇者が落ちて死んでしまいーという所から物語はスタートします。

魅力的な仲間がどんどん増えていく感じはハガレンに似てます。
ストーリーもしっかりしているのに、ギャグも抜かりなく面白い。。。
何回も声をあげて笑いました。

物語はとうとうクライマックス。
最後まで目が離せません!
ぜひ読んでほしい作品です。

2. ここは今から倫理です。

タイトルの通り、「倫理」をテーマにした漫画。
倫理学の教師である主人公と、思春期真っ只中で悩み多き高校生達との触れ合いを通し、倫理の真髄に触れていきます。

プラトンとかキルケゴールといったBigNameの名言が随所に挿入されており、
またこの挿入の仕方が、絶妙なんです。
作者がちゃんと勉強されていて、自分なりに彼らの思想を汲んでから使っている感じがします。

(思想系の漫画といえば、世界の名著を漫画にしたシリーズあったな。。。 ニーチェの何かを買ったが、字面だけで中身なかった。。。)

3. 盾の勇者の成り上がり

ここ最近、異世界転生モノが人気を博しています。
マツキもこの1年で10作品ほど読んだ中で、おすすめしたいのはこちらの作品!

剣、槍、弓、盾の4種類の勇者が伝説として語り継がれている世界に、"盾"の勇者として転生してしまった主人公。
3人の勇者は国全体をあげてもてはやされるが、攻撃のできない"盾"の勇者は、転生早々迫害を受けることに。

主人公は不遇の中で旅を続け、信頼できる仲間を増やしていき、更なる異世界の勇者達とも出会いーーと、
物語がどんどん展開していくので、目が離せません。

2020年末時点で、17巻まで出ています。

4. 不滅のあなたへ

たとえるなら、火の鳥の現代版。

不死身の身体を持つ生命体フシ。
フシは自分が出会い、かつ死んだ人間の身体に変身することができる。
(例えば、モグラに出会う→そのモグラが死ぬ→そのモグラに変身できるようになる)

ここ10年来、軽いノリで人がポコポコ死ぬ漫画が量産されるようになったな、、と感じます。
この作品でも登場人物が結構なくなりますが、作品が醸す死生観の重みは歴然の差です。

2020年末の今も連載中(最新巻14巻)でこれからどんな展開になるのか楽しみです。

5. プラチナエンド

中学卒業の日、絶望の淵にいた主人公は自殺を図る。
そこを偶然目撃した天使は、主人公を助け、特別な力を与える。
と同時に、主人公は全部で13人いる「神候補」の一人となり、他の神候補と闘うことに。

デスノートバクマン!に続く、
原作:大場つぐみ×漫画:小畑健の第3作品目です。

6. ホームルーム

主人公は優等生のいじめられっ子女子高生(ポニーテール黒髪眼鏡)。
いじめられっ子の主人公を助ける、人気英語教諭の通称ラブリン。
ラブリンを狙う、若い女養護教諭
同性愛者でブスの主人公の親友。
この親友に密かに恋するイケイケ男子学生。

イジメを主題にした学園モノかと思いきや、全然違いました。
普通を装っている登場人物たちの精神世界の描写が面白い&素晴らしい。

7. 累 かさね

累(1) (イブニングコミックス)

累(1) (イブニングコミックス)

主人公の少女累(カサネ)は、その醜い容姿のために、街を歩けば嘲笑され、忌み嫌われ、学校ではイジメの対象になっていた。しかし、彼女はかつて一世を風靡した大女優の娘でもあった。 大女優であった母親と似ても似つかない醜女の主人公。実は、母親には隠された秘密があったのだ。

序盤から先の展開が読めず、ハラハラしながら一気読みしました。 全14巻。コミックスの表紙が妖艶で美しい。

2018年に映画にもなっていたんですね。

8. アポカリプスの砦

読んだのは去年くらいなんですが・・・ 「そういえば一度もランキングに入れてない!!しまったー!!」 ということで今回ランクイン。 (連載自体も2011-2015年とまぁまぁ古い漫画です)

主人公の前川君(性格:気弱)は冤罪で少年院施設に入れられてしまい、ここで後に仲間になる3人の少年と出会います。 この4人組のバランス(見た目とキャラ付け)がまたいい感じです。
少年院にいる間に世界はゾンビだらけになってしまいます。
少年院内部での抗争、世界機関による救出?、外界でのワクチンの開発。 仲間でウラァァオラァァァで乗り切ります。

9. 君のことが大大大大大好きな100人の彼女

女の子の描写が最高にうまいです。一人ひとりがまじで可愛いんです。

タイトルの通り、登場する女の子全員が主人公のことを好きになります。
そして、主人公はその全員と付き合っていきます。 この設定だけ聞くと、男子向けの糞漫画かと思われるでしょう。
けど、女性の自分が読んでいても嫌な描写がないから不思議です(水着とかはあるが)。
美少女好きの同人作家とかは、二次創作意欲がガンガンに刺激されるであろう漫画。

10. チェンソーマン

悪魔が存在する世界。主人公「デンジ」とチェンソーの悪魔の「ポチタ」の仕事は、悪魔を駆除する「デビルハンター」だが、ある日「ゾンビの悪魔」に殺されてしまう。しかし、ポチタがデンジの心臓となって、デンジは復活。「チェンソーの悪魔」へと変身する力を手に入れる。 ストーリーが面白くて、次話が気になって止まらない!・・・という感じではないのですが、 少年誌の王道感が気に入ったのでランクイン。

では、次は2021年上半期のランキングでお会いしましょう。

Spring Bootで作るWebアプリケーション⑦ - 相関バリデーション

前回では、Spring Bootを使ったWebアプリケーションで入力されたデータをバリデーション(入力チェック)する方法を解説しました。
前回のバリデーションは項目単位のバリデーションで、単項目チェックや単項目バリデーションと呼ばれるものでした。しかし、実際にアプリケーションでは、複数の項目にまたがったチェックロジックを必要とすることが多くあります。
今回は、入力された複数のデータにまたがってチェックをする、相関チェックや相関バリデーションと呼ばれるものの実装方法を解説します。

相関バリデーションとは

画面から入力された複数のデータにまたがって、データの正当性を検証することを相関チェックや相関バリデーションと呼びます。

例えば、郵便番号の主番号は3桁であること、子番号は4桁であること。これは、各項目ごとにバリデーションすればよいので、前回に解説した単項目チェックで検証できます。
しかし、郵便番号の入力自体の省略可能にした場合、主番号と子番号の両方が未入力であることをチェックする必要があります。

今回、解説する相関バリデーションを使用すると、複数の入力データにまたがるチェックだけでなく、さまざまなチェックを実装することができます。

  • 複数の入力データにまたがるチェック
  • 入力データとデータベースなど、サーバ内に保存されたデータの組み合わせチェック
  • 複雑なロジックを必要とするチェック

準備

相関バリデーションの実装前に、これまで作成したアプリケーションを少し修正します。

名前の入力項目を「苗字」と「名前」に分け、画面も別の入力項目を設けます。

ProfileForm .javaを修正して、nameフィールドをfirstName、lastNameフィールドの2つに置き換えます。

package penguin.web.controller.form;

import javax.validation.constraints.Size;

public class ProfileForm {

    @Size(min = 3, max = 15)
    private String firstName;

    @Size(min = 3, max = 15)
    private String lastName;

    private Integer age;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

入力画面のname.htmlも同様に、名前の入力項目を「苗字」と「名前」の2つの項目に置き換えます。

<!DOCTYPE html>
<html lang="ja" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Name</title>
<style>
.input-error {
    border: 1px solid red;
}
</style>
</head>
<body>
<form th:action="@{/profile/age}" th:object="${profileForm}" method="post">
    <p>
        <label for="last_name">苗字を入力してください</label>
        <input type="text" name="last_name" id="‘last_name" th:field="*{lastName}" th:errorclass="input-error">
        <span style="color: red;" th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}"></span>
    </p>
    <p>
        <label for="first_name">名前を入力してください</label>
        <input type="text" name="first_name" id="first_name" th:field="*{firstName}" th:errorclass="input-error">
        <span style="color: red;" th:if="${#fields.hasErrors('firstName')}" th:errors="*{firstName}"></span>
    </p>
    <p>
        <input type="submit" value="次へ">
    </p>
</form>
</body>
</html>

バリデーションの実装

では、相関バリデーションの実装に進みます。
ここでは例として、新しく追加した「苗字」と「名前」の文字数の合計が20文字以内であることを検証するバリデーションを実装します。

相関バリデーションの実装方法はいくつかあります。

方法1 - フォームBeanに検証メソッドを実装

フォームBeanに検証メソッドを追加して、そのメソッド内に検証ロジックを実装します。

追加する検証メソッドは、以下のルールに従って実装します。

  • @AssertTrueアノテーションをメソッドに指定する。
  • メソッドは、存在しないフィールドのgetterメソッドとして実装する。
  • getterメソッドなので、メソッド名は「is」もしくは「get」で始まる必要がある。
  • 復帰値の型はbooleanにする。
  • バリデーションの結果、正当であればtrue、不当であればfalseを復帰値とする。

追加するメソッドは以下のようになります。

    @AssertTrue(message = "苗字と名前は合計20文字以内で入力してください。")public boolean isValidName() {                                             ➋
        return (firstName != null ? firstName.length() : 0)
                + (lastName != null ? lastName.length() : 0) <= 20;   ➌
    }

➊ 検証メソッドに@AssertTrueアノテーションを指定します。message属性にエラーメッセージを指定することができます。
➋ 検証メソッドをgetterメソッドとして追加します。復帰値の型はbooleanです。ここで注目してほしい点は、存在しないvalidNameフィールドのgetterメソッドとして実装していることです。
➌ チェックロジックを実装します。ここでは苗字の文字数と名前の文字数が20文字以下であれば正当としてtrue、20文字を超える場合は不当としてfalseを復帰値とします。

バリデーションの実装は以上です。ここまでの実装で実行するとわかりますが、このバリデーションでエラーになった場合、画面上でもエラーとなりますが、エラーメッセージが表示されません。理由は、この方法で検出されたエラーはフィールドに紐づくエラーになるためです。今回の実装では、存在しないvalidNameフィールドのエラーとなるので、画面上にエラーメッセージが表示されません。

入力画面のname.htmlを以下のように変更することで、存在しないvalidNameフィールドのエラーのメッセージを表示させることができます。

    <span style="color: red;" th:if="${#fields.hasErrors('validName')}" th:errors="*{validName}"></span><p>
        <label for="last_name">苗字を入力してください</label>
        <input type="text" name="last_name" id="‘last_name" th:field="*{lastName}" th:errorclass="input-error">
        <span style="color: red;" th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}"></span>
    </p>

➊ 存在しないvalidNameフィールドでエラーが発生した場合、エラーメッセージを表示します。

f:id:penguinlabo:20201123182733p:plain

この方法の特徴

この方法は実装が非常に簡単ですが、コードの見やすさや保守面で不利な面があります。

  • フォームBeanにバリデーションロジックが入り込む。定義と実装はなるべく疎結合にすべきなので、フォームBeanは定義だけを含めるべき。
  • バリデーションの結果が、存在しないフィールドのエラーとして割り当てられる。

特に、前者のフォームBeanにバリデーションロジックが入り込む点は、コードが見にくくなる原因になるので、避けたいところです。

方法2 - バリデーションアノテーションを自作

これまで、バリデーションアノテーションはBean Validationで用意されていたものを使用しました。

penguinlabo.hatenablog.com

バリデーションアノテーションは自作することも可能です。バリデーションアノテーションを自作して、相関バリデーションを実装してみます。

バリデーションアノテーションを自作する場合、アノテーション自体と、アノテーションに紐づくバリデーションを実装することになります。

アノテーションの実装は以下の通りです。

@Target(value = ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NameSizeValidator.class)public @interface NameSize {
    String message() default "苗字と名前は合計{min}文字から{max}文字で入力してください。";  ➌

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    int min() default 0;                            ➍

    int max() default Integer.MAX_VALUE;            ➎
}

➊ 相関バリデーションに使用するアノテーションなので、指定可能な場所はクラスに限定します。
➋ バリデーションの実装クラスを指定します。バリデーションの実装方法は、この後に解説します。
➌ バリデーションのエラーメッセージを定義します。
➍ バリデーションの属性として最小値を定義します。
➎ バリデーションの属性として最大値を定義します。

次に、バリデーションを実装します。

public class NameSizeValidator implements ConstraintValidator<NameSize, ProfileForm> {  ➊
    private int min;
    private int max;

    @Override
    public void initialize(NameSize annotation) {                                       ➋
        min = annotation.min();
        max = annotation.max();
    }

    @Override
    public boolean isValid(ProfileForm value, ConstraintValidatorContext context) {     ➌
        int size = (value.getFirstName() != null ? value.getFirstName().length() : 0)
                + (value.getLastName() != null ? value.getLastName().length() : 0);

        return size >= min && size <= max;                                              ➍
    }
}

➊ ConstraintValidatorを実装したクラスを作成します。総称型の1つ目はアノテーションのクラス、2つ目はバリデーション対象のクラスを指定します。
➋ initializeメソッドをオーバーライドします。引数でアノテーションを受け取るので、アノテーションの属性から最大値、最小値を取得します。
➌ isValidメソッドをオーバーライドします。引数でバリデーション対象のオブジェクトを受け取るので、そのオブジェクトの内容を検証するロジックを実装します。
➍ 苗字と名前の長さの合計が最小値以上、かつ最大値以下であることを検証します。

アノテーションとバリデーションすることで、自作したバリデーションアノテーションを使用する準備ができました。

自作したバリデーションアノテーションをフォームで使用することで、バリデーションが実行されます。

@NameSize(min = 6, max = 20)public class ProfileForm {

    @Size(min = 3, max = 15)
    private String firstName;

    @Size(min = 3, max = 15)
    private String lastName;

➊ 作成したバリデーションアノテーションをクラスに指定します。アノテーションに定義されている属性により、最小値、最大値を指定します。

クラスに指定するバリデーションアノテーションなので、エラーはフィールドには紐づかず、グローバルエラーとなります。

name.htmlを修正して、グローバルエラーのメッセージも表示されるように修正します。

    <p style="color: red;" th:each="error : ${#fields.globalErrors()}" th:text="${error}"></p><p>
        <label for="last_name">苗字を入力してください</label>
        <input type="text" name="last_name" id="‘last_name" th:field="*{lastName}" th:errorclass="input-error">
        <span style="color: red;" th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}"></span>
    </p>

➊ グローバルエラーを表示します。グローバルエラーは複数、存在できるものなので、th:each属性でグローバルエラーの数分、エラーを表示させます。

この方法の特徴

実装したコードを見てもわかる通り、バリデーションの実装と、バリデーションを使うときの定義が完全に分離されていることがわかります。
フォームの実装は、次の一文が追加されるだけです。

@NameSize(min = 6, max = 20)

このアノテーションを見れば、名前の最小文字数が6文字、最大文字数が20文字であることの検証がされることがわかります。
フォームに余計な要素が入り込むことがなく、とても分かりやすいコードになります。

ただ、このバリデーションの実装方法も、マッチした使用方法でないと、コードの可読性や保守性の低下につながります。

今回の実装例では、ProfileFormクラスの苗字、名前のフィールドに特化したバリデーションになっており、ほかのクラスでは使用できない汎用性のないバリデーションになっています。
このアノテーションに対象のフィールドを指定する属性を追加することで、バリデーションの汎用性を上げることができます。

@MultipleSize(field = {"firstName", "lastName"}, min = 6, max = 20)

方法3 - Springバリデーションを実装

方法1、方法2は、Bean Validationの仕組みを利用したものでした。Spring Frameworkでは、独自のバリデーションの仕組みが用意されています。
ここでは、このSpringバリデーションの仕組みを利用したバリデーションの実装方法を説明します。

バリデーションとしては、org.springframework.validation.Validatorを実装したクラスに検証ロジックを実装します。

@Componentpublic class ProfileValidator implements Validator {      ➋

    @Override
    public boolean supports(Class<?> clazz) {             ➌
        return ProfileForm.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {  ➍
        ProfileForm form = ProfileForm.class.cast(target);

        String firstName = form.getFirstName();
        String lastName = form.getLastName();
        int size = (firstName != null ? firstName.length() : 0) + (lastName != null ? lastName.length() : 0);

        if (size < 6 || size > 20) {                      ➎
            errors.reject("", "苗字と名前は合計6文字から20文字で入力してください。");  ➏
        }
    }
}

➊ @Componentアノテーションを指定して、DI管理対象とします。
➋ org.springframework.validation.Validatorを実装します。
➌ supportsメソッドをオーバーライドします。引数でクラスが渡されるので、そのクラスがバリデーション対象か否かをbooleanで返却します。
➍ validateメソッドをオーバーライドします。引数でバリデーション対象のオブジェクトを受け取るので、そのオブジェクトの内容を検証するロジックを実装します。
➎ 苗字と名前の長さの合計が最小値以上、かつ最大値以下であることを検証します。 ➏ バリデーションの結果、エラーの場合、引数のErrorsオブジェクトにエラーを設定します。rejectメソッドの第1引数はプロパティファイルに定義されたエラーメッセージのキー名を指定します。ここでは、プロパティファイルからエラーメッセージを取得しないため、空文字を指定しています。第2引数はプロパティファイルからエラーメッセージが取得できなかった時のエラーメッセージとなります。

Errorsオブジェクトのrejectメソッドで設定したエラーは、グローバルエラーとして登録されます。htmlの修正は、方法2で説明したグローバルエラーの表示方法により、エラーメッセージが表示されるようになります。

次に、このバリデーションを有効にします。これまでは、フォームBeanにアノテーションでバリデーションを指定していましたが、Springバリデータの場合、コントローラークラスでバリデーションを指定します。

@Controller
@RequestMapping("profile")
@SessionAttributes("profileForm")
public class ProfileController {

    @Autowired
    private ProfileValidator profileValidator;      ➊

    @InitBinder
    public void initBinder(WebDataBinder binder) {  ➋
        binder.addValidators(profileValidator);     ➌
    }

➊ 作成したバリデータのインスタンスをDIにより取得します。
➋ @initBinderアノテーションを指定したメソッドを定義します。引数にWebDataBinderを指定します。
➌ WebDataBinderのaddValidatorsメソッドの引数に作成したバリデータのインスタンスを指定して実行します。

この方法の特徴

アノテーションによるバリデーションは、一つのチェックにつき一つのアノテーションを指定しました。
Springバリデーションでは、フォーム単位でのチェックになります。例では、org.springframework.validation.Validatorのvalidateメソッドの実装で一つの検証を実装していますが、複数の検証を実装することが可能です。Errorsオブジェクトのrejectメソッドを呼び出した回数分、エラー情報が登録されます。

アノテーションによるバリデーションはフォームBeanの実装を見れば、そのフォームに対するバリデーションを把握することができますが、Springバリデーションを使用すると、バリデーションの内容を把握するために、フォームBeanにバインドされたSpringバリデーションの実装を確認する必要が出ます。

どのように使い分けるか

バリデーションの方法として、3つの方法を紹介しました。

一番汎用性がある方法は、最後に説明したSpringバリデーションになります。が、すべてのバリデーションをSpringバリデーションで実装すると、コード量は増えますし、バリデーションの内容が把握しずらくなります。

それぞれのバリデーションの特徴から、ケースバイケースで使用することで、見通しの良いコードになります。

方法1 - フォームBeanに検証メソッドを実装

この方法の利点は、なんといっても実装がシンプルで実装量も少なめなこと。
ただ、この方法で、一つのフォームに複数のバリデーションを実装すると、フォームBeanのコードがカオスな状態になります。
この方法でバリデーションを実装することは強くお勧めしません

方法2 - バリデーションアノテーションを自作

バリデーションがフォームから隔離され、バリデーションとして部品化されるので、作成するアプリケーション全体で使われるバリデーションを実装するケースに向いています。
複数のフォームで繰り返し定義されるようなフィールドのバリデーションは、この方法で実装すると、アプリの生産性と保守性の向上が望めます。

方法3 - Springバリデーションを実装

フォーム単位で定義するバリデーションなので、フォームごとにバリデーションを実装する必要があるケースに向いています。
既存のBean Validationアノテーションや、自作したバリデーションアノテーションで検証できない、特殊なケースのみ、この方法で実装するようにしましょう。

最後に

「Spring Bootで作るWebアプリケーション」シリーズはこれで終了です。毎回、書きたいことが盛りだくさんで、長文になってしまいました。
これだけでアプリケーションを作れるわけではなく、Webアプリケーションの最低限の実装の開設となっています。
今後の記事でデータベースアクセスや、認証・認可など、アプリケーションで必須となるであろう要素をSpring Frameworkで実装する方法を紹介していこうと思っています。