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

JavaEEを主にシステム開発をしながら思うところをツラツラを綴る

Pluggable Annotation Processing API Sample(実践編3)

vermeer.hatenablog.jp

vermeer.hatenablog.jp

実践1で挙げた課題について対応しました。

コマンドの記述が繁雑
実行ライブラリのクラスパスにprocessor-command.xmlを配置しないといけないません。つまり利用側が自分のresources配下に機能一覧を追記したprocessor-command.xmlを配置しないといけません。最低でも提供側が定義ファイルに記述する内容をルートパッケージのJavaDocなどに記載したり、手引きに書いておかないと、何をどうしたら良いのかわかりません

Pluggable Annotation Processing API Sample(実践編1) - システム開発で思うところ

今回の対応でprocessor-command.xmlが任意のオプショナルなものになりました。

利用の手引き

  1. Annotation Processorが実行できるpom.xmlの設定
  2. Command Class を実装
  3. コンパイル時にAnnotation Processorにより実行される(※)

※ プロジェクトのクラスパスリソース直下のprocessor-command.xmlにより、実行時の制御ができます(任意).


pom.xml 記述

利用ライブラリとして、またAnnotation Processorを実行する設定をするためにpom.xmlを編集します.

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>

    <!-- Maven Repository on GitHub  start -->
    <git.branchName>mvn-repo</git.branchName>
    <git.repositoryOwner>vermeerlab</git.repositoryOwner>
    <git.repositoryName>maven</git.repositoryName>
    <!-- Maven Repository on GitHub  end-->
</properties>

<repositories>
    <!-- Maven Repository on GitHub  start -->
    <repository>
        <id>org.vermeerlab</id>
        <url>https://raw.github.com/${git.repositoryOwner}/${git.repositoryName}/${git.branchName}/</url>
        <snapshots>
            <enabled>true</enabled>
            <updatePolicy>always</updatePolicy>
        </snapshots>
    </repository>
    <!-- Maven Repository  on GitHub  end-->
</repositories>

<dependencies>
    <dependency>
        <groupId>org.vermeerlab</groupId>
        <artifactId>annotation-processor-core</artifactId>
        <version>0.3.0</version> <!-- target version -->
    </dependency>
</dependencies>

<build>
    <plugins>

        <!-- for annotation processor start-->
        <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.6.0</version>
            <configuration>
                <source>${maven.compiler.source}</source>
                <target>${maven.compiler.target}</target>
                <!-- Disable annotation processing for ourselves. -->
                <compilerArgument>-proc:none</compilerArgument>
                <compilerArgs>
                    <arg>-Xlint</arg>
                </compilerArgs>
                <showDeprecation>true</showDeprecation>
            </configuration>
        </plugin>
        <!-- annotation processor end -->
    </plugins>
</build>

Command Class の実装

AnnotationProcessorがorg.vermeerlab.apt.command.ProcessorCommandInterface、またはorg.vermeerlab.apt.command.PostProcessorCommandInterfaceを実装したクラスをコンパイル時に検索して実行します.

2つのインターフェースの違い

  • ラウンド毎に処理(ProcessorCommandInterface
  • ラウンド毎の処理に加えて、最終ラウンドに処理(PostProcessorCommandInterface

proseccor-command.xml の設定(任意)

ファイルが存在しない場合(デフォルト)

  • コンソールに実行コマンドは表示しません
  • 全ての.classファイル および、jarファイルを検索対象とします

ファイルが存在する場合

processor-command.xmlの設定により、以下の対応が可能です.

  • 実際に実行しているコマンドクラスを確認をコンソールに出力
  • 適用したくないコマンドクラスの除外
  • クラスパス配下のファイルだけを対象にして、jarを対象外
  • 検索対象のjarファイルを指定

詳細は xmlコメントを参照してください.

<?xml version="1.0" encoding="UTF-8"?>
<!--
Annotation Processor で実行するコマンドを登録する設定ファイルです.
コマンドクラスは
org.vermeerlab.apt.command.ProcessorCommandInterface
を実装してください.

全ラウンドで まとめて行うコマンドは
org.vermeerlab.apt.command.PostProcessorCommandInterface
を実装してください.
-->
<root>
    <!--
    実行コマンドリストを標準出力制御を設定してください.
    出力する場合は true.
    デフォルトは false(出力しない)
    -->
    <displayCommandList>true</displayCommandList>

    <!--
    実行対象外とするコマンドクラスの完全修飾名を設定してください.
    -->
    <excludeCommands>
        <excludeCommand>
        </excludeCommand>
    </excludeCommands>

    <!--
    コマンドクラスの検索対象としてクラスパス配下のJarファイルを読み込み有無を設定してください.
    Jarファイルを読み込む場合は true
    -->
    <scanJarFile>true</scanJarFile>

    <!--
    コマンドクラスの検索対象とするクラスパス配下のJarファイルを設定してください.
    -->
    <scanTargetJarFiles>
        <scanTargetJarFile>
        </scanTargetJarFile>
    </scanTargetJarFiles>

</root>

例えば、どんなコマンドが実行されているのか、確認するために いったん <displayCommandList>trueにして確認をした上で、不要なコマンドを <excludeCommands>で除外をすると無駄がありません。
その上で、<scanTargetJarFiles>で使用するコマンドのjarだけを検索対象とすればコンパイル時の性能影響も最小限に留めることができます。

Code

BitBucket バージョン 0.3.0

さいごに

当初、諦めていたことが出来たというだけでも個人的には満足です。それに加えて 使用者が自分だけでも暗黙の手順が不要になったことを素直に嬉しく思います。

次回は、今回のライブラリを利用した具体的な事例として、JavaPoetを使用してJavaコードを生成するライブラリについて記事が書ければと思っています。

ClassPath配下の資産を検索する

vermeer.hatenablog.jp

で、processor-command.xmlに実行コマンドを依存ライブラリ全般を把握した上で登録をしないといけない、という仕組みについて、自分としても「良くない」と思っていたところです。
当初はクラスローダー周りが良く分からないということで後回しにするつもりだったのですが、自分にとっても使いにくいということで、先に取り組むことにしました。この対応をしたことでprocessor-command.xmlは、よりオプショナルなものになり利便性が上がったように思います。

はじめに

ツールではなく、拡張を前提としたライブラリです。
Jarファイルおよび Jarにアーカイブされているクラスも検索対象です。

利用方法

pom.xml

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>

    <!-- Maven Repository on GitHub  start -->
    <git.branchName>mvn-repo</git.branchName>
    <git.repositoryOwner>vermeerlab</git.repositoryOwner>
    <git.repositoryName>maven</git.repositoryName>
    <!-- Maven Repository on GitHub  end-->
</properties>

<repositories>
    <!-- Maven Repository on GitHub  start -->
    <repository>
        <id>org.vermeerlab</id>
        <url>https://raw.github.com/${git.repositoryOwner}/${git.repositoryName}/${git.branchName}/</url>
        <snapshots>
            <enabled>true</enabled>
            <updatePolicy>always</updatePolicy>
        </snapshots>
    </repository>
    <!-- Maven Repository  on GitHub  end-->
</repositories>

<dependencies>
    <dependency>
        <groupId>org.vermeerlab</groupId>
        <artifactId>vermeerlab-classpath-scanner</artifactId>
        <version>0.2.0</version> <!-- target version -->
    </dependency>
</dependencies>

基本

xxxFinder#findの戻り値の型が、xxxScannerの型です.

ジェネリックを使用した理由

必ずしも戻り値がPathであることが最良ではなかったため.
具体的には、絶対パスのファイル表記を使用するのではなく、取得したファイルをクラスの完全修飾名として編集して、 検索結果を文字列として扱った方が後続処理にて扱い易いユースケースがあったため.

ClassPath

デフォルトの検索クラス(ClassPathFinder)は、ファイル名に対して「.class」の後方一致のみを条件として検索します.

ClassPathScanner<Path> scanner = new ClassPathScanner<>();

JarPath

クラスパス配下のJarファイル内のファイルを検索します. デフォルトの検索クラス(JarPathFinder)は、ファイル名に対して「.class」の後方一致かつ「$」をファイルパスに含まないことを条件として検索します.

JarPathScanner<Path> scanner = new JarPathScanner<>();

応用(検索条件の拡張)

※実際に上述の過去記事のプロジェクトにて行った拡張です.
パッケージフォルダ

ClassPath

呼出先の実装
class ProcessorCommandClassFinder<SCAN_RESULT_TYPE> extends ClassPathFinder<SCAN_RESULT_TYPE> {

    TypeAssignableFrom assignableFrom;

    public ProcessorCommandClassFinder() {
        this.assignableFrom = TypeAssignableFrom.of(ProcessorCommandInterface.class);
    }

    /**
     * {@inheritDoc }
     */
    @Override
    @SuppressWarnings("unchecked") //Generic Cast
    public Stream<SCAN_RESULT_TYPE> find(Object rootPath) throws IOException {
        Stream<SCAN_RESULT_TYPE> pathStream = super.find(rootPath)
                .map(filePath -> {
                    String classFilePath
                           = this.assignableFrom.substringClassFilePath(this.rootPath().toString(), filePath.toString());
                    return (SCAN_RESULT_TYPE) classFilePath.substring(0, classFilePath.length() - 6);
                });
        return pathStream;
    }

    /**
     * {@inheritDoc }
     */
    @Override
    protected boolean match(Path path, BasicFileAttributes attrs) {
        if (super.match(path, attrs) == false) {
            return false;
        }
        return assignableFrom.isValidAbsoluteFilePath(this.rootPath().toString(), path.toString());
    }
}
やっていること
  1. 取得したファイルの絶対パスから、ルートとなるパス文字列と「.class」を除外して、クラスの完全修飾名として編集をする
  2. 編集したクラス名からProcessorCommandInterface.classを継承したクラスか判定をする(TypeAssignableFrom
ポイント

SCAN_RESULT_TYPEのキャスト元がStringであること

(SCAN_RESULT_TYPE) classFilePath.substring(0, classFilePath.length() - 6);
呼出元の実装

ProcessorCommandScanner#scan

ClassPathScanner<String> classScanner = new ClassPathScanner<>(new ProcessorCommandClassFinder<>());
ポイント

戻り値の型が基本ではPathだったところが、このケースではString.

JarPath

呼出先の実装
class ProcessorCommandJarFinder<SCAN_RESULT_TYPE> extends JarPathFinder<SCAN_RESULT_TYPE> {

    TypeAssignableFrom assignableFrom;

    public ProcessorCommandJarFinder() {
        this.assignableFrom = TypeAssignableFrom.of(ProcessorCommandInterface.class);
    }

    /**
     * {@inheritDoc }
     */
    @Override
    protected boolean fileFilter(JarEntry jarEntry) {
        return super.fileFilter(jarEntry)
               ? this.assignableFrom.isValidClassFileName(jarEntry.getName())
               : false;
    }

    /**
     * {@inheritDoc }
     */
    @Override
    @SuppressWarnings("unchecked") //Generic Cast
    protected SCAN_RESULT_TYPE toResultValue(JarEntry entry) {
        return (SCAN_RESULT_TYPE) entry.getName();
    }
}
やっていること
  1. クラス名(entry#getName)からProcessorCommandInterface.classを継承したクラスか判定をする(TypeAssignableFrom
  2. クラス完全修飾名を返却する
ポイント

SCAN_RESULT_TYPEのキャスト元がStringであること

return (SCAN_RESULT_TYPE) entry.getName();
呼出元の実装
ProcessorCommandJarPathScanner<String> jarScanner = new ProcessorCommandJarPathScanner<>(
        new ProcessorCommandJarFinder<>(),
        configXml);
ポイント

戻り値の型が基本ではPathだったところが、このケースではString.


まとめ

Jarファイル名の検索条件を変更する(JarPathScanner#jarFilter)など、xxxScannerおよびxxxFinderの各メソッドを拡張が可能です。
拡張ポイントになりそうな検索、編集についてはメソッドをprotectedにしているので用途に応じた拡張してください。

Code

参考

参照可能なクラスのリストを取得したい - argius note

任意のjarファイルから条件に合ったクラスをロードする - Qiita

Jarファイルメモ(Hishidama's java-archive Memo)

さいごに

今回のライブラリは自作ライブラリを複数組み合わせて構築しています。これまでであれば「すべてのコードをローカルにコピーしてください」という手順が必要だったのですが、 GitHubに作成したMavenリポジトリを使うようにすることで、そのあたりの手順がスッキリしたように思います。

作り方は、以下で紹介しています。公開する前提のライブラリであればMavenCentralよりも手続きが少ないのでお勧めです。 vermeer.hatenablog.jp

次回は、ClassLoaderを使うことでコマンドクラスの読み込みが改善できた版のAnnotationProcessorについて書こうと思います。

GitHubにMavenリポジトリをつくる

Mavenプロジェクトを細かく分割することを前提にしたら、私が作成しているものは公開リポジトリが無いと利用者側*1にとって凄く不便だなと思って調べてみました。

はじめに

さくっとは出来ないだろうなぁとは思いましたが案の定かなり苦戦しました。自分なりに 初めから やり直したので大丈夫な手順だとは思います*2

環境

Windows10、Java8、NetBeans8.2

なぜGitHub

コードの管理は今のところ基本的にBitBucketを使っています。その中でMavenリポジトリGitHubにしました。理由は知見量が多かったことと、当初、BitBucketでやろうとしたのですが良く分からなかったというのが一番の理由です。あと後付け的なところはありますが、公開する資産についてはGitHubの方が多くの人にとって馴染みがあるかな?というところもあってGitHubMavenリポジトリの置き場にすることにしました。

構築の流れ

格納先となるリポジトリを作成する

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

  • ブランチを作成

ブランチ名は「mvn-repo」

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

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

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

「save」ボタンを押すのを忘れずに

認証キーを作成する

Webで調べるとユーザー名とパスワードを設定するやり方が多かったように思いますが、OAuthによる認証の方がセキュリティー的にみて良いと思います。

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


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


Token description は「maven-pass」。ただのラベルなので何でも大丈夫です。

チェックボックスpublic_repo user:email だけ。必要最低限は この2つをチェックすれば良いようです*5

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


認証キーが生成されます。

後から確認は出来ませんので ここでメモしておくのを忘れずに。忘れたときは再生成することになるので気を付けましょう。

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

pom.xmlを編集する

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.vermeerlab</groupId>
    <artifactId>maven-git-sample</artifactId>
    <version>0.1.0</version>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <github.global.server>github</github.global.server>
        <git.branchName>mvn-repo</git.branchName>
        <git.repositoryOwner>vermeer-1977</git.repositoryOwner>
        <git.repositoryName>maven</git.repositoryName>
        <git.isMerge>true</git.isMerge>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-deploy-plugin</artifactId>
                <version>2.8.2</version>
                <configuration>
                    <altDeploymentRepository>internal.repo::default::file://${project.build.directory}/${git.branchName}</altDeploymentRepository>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.github.github</groupId>
                <artifactId>site-maven-plugin</artifactId>
                <version>0.12</version>
                <configuration>
                    <!-- git commit message -->
                    <message>Maven artifacts for ${project.version}</message>
                    <noJekyll>true</noJekyll>
                    <outputDirectory>${project.build.directory}/${git.branchName}</outputDirectory>
                    <branch>refs/heads/${git.branchName}</branch>
                    <includes>
                        <include>**/*</include>
                    </includes>

                    <repositoryName>${git.repositoryName}</repositoryName>
                    <repositoryOwner>${git.repositoryOwner}</repositoryOwner>

                    <!-- true:履歴を残す false:直近バージョンのみ repositoryに残る -->
                    <merge>${git.isMerge}</merge>

                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>site</goal>
                        </goals>
                        <phase>deploy</phase>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.0</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.source}</target>
                    <compilerArgs>
                        <arg>-Xlint</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>

    </build>

    <distributionManagement>
        <repository>
            <id>internal.repo</id>
            <name>Temporary Staging Repository</name>
            <url>file://${project.build.directory}/${git.branchName}</url>
        </repository>
    </distributionManagement>
</project>
  • ローカル保存先の distributionManagementurl の指定でリポジトリ名と同じフォルダを指定しています。任意のフォルダ名で良く、特に意味を持たせる必然は無いけれど pom内に複数個所指定するので 記述誤りを防ぐために同名にしました。

  • git.isMergetrueであればアップロードした資産の履歴を残します。SNAPSHOTの場合は履歴を残すけど、そうじゃなくなったら履歴は不要というような指定をしたい場合に使用する想定かな?と思っています。

settings.xmlを編集する

...\.m2\settings.xmlに格納されています。

NetBeansMavenプロジェクトだったら、以下のものを編集。見た感じだとプロジェクト毎に存在するように見えますが 全てのプロジェクトで共通の資産です。どのプロジェクトのものを編集してもちゃんと反映されます*6

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
    <servers>
        <server>
            <id>github</id>
            <password>hogehogehogehogehogehogehogehogehogehoge</password>
        </server>
    </servers>
</settings>

hogehoge… は先に取得した認証キーを設定してください。

Maven資産をアップロード

mvn deploy コマンドを実行します。


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


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


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


アップロードされたリポジトリのアドレスは

GitHub - vermeer-1977/maven at mvn-repo

ちなみに通常のビルドではアップロードはされません。

簡易クラスを作成

次の工程で参照するためのクラスを追加します。

public class Sample {

    public void testPrint() {
        System.out.println("maven-test-sample");
    }
}

改めて deploy します。

リポジトリ資産を取り込む

public class Main {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        new org.vermeerlab.maven.git.sample.Sample().testPrint();
    }

}

アップロードをした資産の取り込むための設定。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.vermeerlab</groupId>
    <artifactId>maven-git-client</artifactId>
    <version>0.1.0</version>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <github.global.server>github</github.global.server>
        <git.branchName>mvn-repo</git.branchName>
        <git.repositoryOwner>vermeer-1977</git.repositoryOwner>
        <git.repositoryName>maven</git.repositoryName>
        <git.isMerge>true</git.isMerge>
    </properties>

    <repositories>
        <repository>
            <id>github-maven</id>
            <url>https://raw.github.com/${git.repositoryOwner}/${git.repositoryName}/${git.branchName}/</url>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>always</updatePolicy>
            </snapshots>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>maven-git-sample</artifactId>
            <version>0.1.0</version>
        </dependency>
    </dependencies>

</project>

この流れだとローカルリポジトリを参照してしまいます。念のためローカルリポジトリの資産を削除して確認をすると良いと思います。ログをみたら、きちんとダウンロードをしていることが確認できます。

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

基本は以上です。

Git(コード)

vermeer_etc / maven-git-sample — Bitbucket

vermeer_etc / maven-git-client — Bitbucket


応用編(親子pomで共通化)

親pomを持つことで重厚なpom記述を全てのプロジェクトに記載しなくて良くなります。

同一ユーザーの別リポジトリにアップしたい

artifact単位でフォルダは作成されますが、意味のある単位としてリポジトリを分けておきたいケースがあります。その場合は、子pomの git.repositoryName を指定します。

子pomで指定

    <properties>
        <git.repositoryName>(変更したいリポジトリ名)</git.repositoryName>
    </properties>

同一ユーザーの別組織のリポジトリにアップしたい

リポジトリ名だけでなく、ライブラリ管理用のOrganizationを作成すると、製品毎に配布資産をグループ化することができます。

子pomで指定

    <properties>
        <git.repositoryOwner>(変更したい組織名)</git.repositoryOwner>
        <git.repositoryName>(変更したいリポジトリ名)</git.repositoryName>
    </properties>

別ユーザーのリポジトリにアップしたい

別ユーザー=認証キーが異なるアカウント管理配下のリポジトリを意図しています。例えばオーナーは別の人だけど配布資産のアップロード権限を持っている状態です。このケースはタグによる上書きだけでなく、対象権限の認証キーも併せて指定する必要があります。

★★あくまで理論上の話です*7。★★

別ユーザーや別組織のリポジトリにアップするためには、アップロード権限のある認証キーが必要です。

認証キーは別途入手できている前提として、いちいちsettings.xmlを書き換えるのは面倒です。ということで切り替えるというのが良いと思います。

  • settings.xmlidと認証キーを追記
<server>
    <id>github</id>
    <password>hogehogehogehogehogehogehogehogehogehoge</password>
</server>

<!-- 追記 -->
<server>
    <id>github-blog</id>
    <password>fugafugafugafugafugafugafugafugafugafuga</password>
</server>

  • 子pom
    <properties>
        <github.global.server>github-blog</github.global.server>
        <git.repositoryOwner>(変更したい組織名)</git.repositoryOwner>
        <git.repositoryName>(変更したいリポジトリ名)</git.repositoryName>
    </properties>

もし基本構造が全く同じであれば指定は不要ですが リポジトリ名など明示的に変更が必要なところがあれば それも指定してください。

間違ってリモートリポジトリにアップしない工夫

とりあえず現時点で私が思いつく懸念事項と対策案です。

  • 履歴を残す

上述していますがgit.isMergeは基本的にtrueにして子pom側で上書きをしないようにすることを推奨します。履歴を持つ分、リポジトリの容量が増えてしまいますが 間違ってアップロードしたときに履歴があれば戻すことが可能です*8

  • github.global.serverの指定は子pomで明示的にする

親pomで宛先を指定しておくと子pomの記述量は減るのですが、上述の事例にあるような切り替えを検討した場合、いくつかの偶然が重なると(各種名称が重複していた&ユーザーとしても更新権限を持っていた、など)、間違った宛先のリポジトリを更新してしまうことが懸念されます。親pomには無効な値を記述しておけば、未指定の場合はアップロードが出来ないのでリスクは減るかと思います。また子pomに本記述があることが先頭で分かるので「これは公開リポジトリにアップロードする対象の資産だな」ということが明示的に分かりやすいように思います。

ただ、切り替えは基本的にしないというのであれば、親pomに記述した方が良いと思います。仕事が増えるとミスの原因も増えます。

子pom

<plugins>
    <plugin>
        <groupId>com.github.github</groupId>
        <artifactId>site-maven-plugin</artifactId>
    </plugin>
</plugins>

のところを基本的にコメントアウトしておけば 間違ってdeploy を実行してもアップロードはされません。リポジトリに資産をアップする頻度にもよりますが、頻繁でないのであれば、こういう運用もありだと思います。

注意事項

親pomのビルドを忘れずに

子pomからローカルリポジトリを参照させたいので、子pomによるビルドの前に親pomのローカルリポジトリの作成またはコードを`git clone'してビルドをするのを忘れないようにしましょう。

ローカルリポジトリの指定を忘れずに

ローカルリポジトリの相対位置をrelativePathで指定しています。こうしておかないと子pomから親pomの参照ができないためエラーになります。今回は同一グループなので、このくらいの記述ですが別グループだと、こうはいかない可能性があります*9。基本的に親pomプロジェクトは同一グループにつくると思いますので、ご自身のグループを適当に作って親と子の資産を1つのグループとして扱うと良いでしょう。

    <parent>
        <groupId>org.vermeerlab</groupId>
        <artifactId>maven-git-parent</artifactId>
        <version>0.1.0</version>
        <relativePath>../</relativePath> <!-- この指定 -->
    </parent>

Git(コード)

  • 親pom

vermeer_etc / maven-git-parent — Bitbucket

  • 子pom

vermeer_etc / maven-git-child — Bitbucket


参考

Github を Maven リポジトリとして使う -

Github上に私設Mavenリポジトリをつくる - M12i.

GitHub の Pages を maven リポジトリとして使用するときのアカウント情報をセキュアにする(OAuth2Token) | KK.Kon の徒然メモ書き

http://synergian.github.io/wagon-git/usage.html

さいごに

これでオレオレリポジトリが気軽に作れるようになりました。
目的としていた「細かいMavenプロジェクトを作っても、利用したいときにコードのダウンロードで地獄を見ない」ということが実現できそうです。

本題とは関係ないですが、目次を初めて使ってみました。これは良いですね。

*1:自分自身も含めて

*2:やり直して思うところとしてはGitHub側の認証キーのところが原因だったかも。もしくはGitPageの作成かな?

*3:作ってから思いましたが紛らわしいので違う名前にした方が分かりやすかったかも

*4:あぁ、いきなり分かりにくい

*5:上手くいかなかったのは、user:email にチェックしていなかったからダメだったかも

*6:反映というか「同じ」なんですけどね

*7:GitHub上で別ユーザーの作成も考えましたが、今回のためだけに作成するのは ちょっと違うかな と思ってやめました

*8:ミスをしない工夫も大事ですが、ミスはするもの という前提を仕組みに入れておくと心理的に安心です

*9:試してはいませんが理屈上はそのはずです

DDD Alliance! 現場で役立つシステム設計の原則 Night! に参加してきました

ddd-alliance.connpass.com

書評も書かせていただいた「現場で役立つシステム設計の原則 」の著者である増田さん(@masuda220 ‏)の本には書けなかったこと、というのを聞いてきました。

2017/08/30(水) DDD Alliance! 現場で役立つシステム設計の原則 Night! 〜「現場で役立つシステム設計の原則」刊行記念!〜 #DDDAlliance - Togetterまとめ

vermeer.hatenablog.jp

会の内容としては、半分書評LT、半分増田さんのお話という構成でした。

書評LT*1

www.slideshare.net

www.slideshare.net

speakerdeck.com

speakerdeck.com

www.slideshare.net

LTそのものへの感想というのは、ちょっと難しいのですが、書籍の礼賛に終始することなく、これは良い、これはどうだろう?とそれぞれのスピーカーの経験を感じられるものでとても良かったと思います。

Java勢としては @irofさんの登場が個人的に嬉しかったですね*2。あと「この本は読みやすいし、そうそうと分かった気になってしまう。でもそれだと良くない。ちゃんと自分で読み込まないとダメ」というのは同意です。*3

本に書かなかったこと(増田さんより)

www.slideshare.net

全般

執筆にあたって「自分のやっていないことを書かない」を踏まえていたとのこと。そうなんですよね、机上の空論ではなく実践の足跡を記した書籍なので 読みやすいんですよね。

テストは?

私はテストを書いていない

潔い解答でした(笑)。

ちゃんとテストはやるよ(笑)

E2Eで(ヒューリスティックテストを十分やって)品質は担保しているとの見解でした。要件の充足という意味だと それも解なんですよね。私も自分のシステム(分析・設計・実装・利用が私だけ)だと状態遷移図くらいを書いて、状態整理をしたら、あとはドキュメント作っていないですし。それが良いか悪いか、については契約云々があるので一概に正しいというつもりはありませんが、面倒なドキュメントを作らないで済むなら作りたくはないというのは本音です。

増田さんも そういう人なのかもしれませんが、私がまだSIerだったころ「あなたがシステムに触れるとバグが出る!!触らないでください!!」とメンバーに良く言われました。「いや、これが僕のお仕事なので…」といって問答無用でバグを検出しまくっていましたが(笑)。

リファクタリングの邪魔に…

ここは難しいところですが、リファクタリングを安全にするためにはテストがあると安心なんだけど、内部構造を大幅に見直すレベルのことをやっているときにテストコードがあると、テストコードメンテナンスで工数が結構必要になるんですよね。私なりの落としどころはtwitterでも言ったのですが

なんですよね。

そういうことを踏まえつつ、網羅性の担保をすべきかどうかは別にして、E2Eのテストツールについては、上手に導入できたら内部実装のリファクタリングも やりやすくなるから良いかもしれませんね*4

Entity、Aggregate、Repository、Factoryは?

DDD本ではないので…

そうですね。簡易DDD本ではなく、あくまで「増田さんの足跡と今の整理」というスタンスの書籍なので。個人的には掘り下げたらもっと面白かったかもなぁ、と思うところもありますが、過去のスライドで補完しているので、これはこれで1つの整理かな、と思います。

サービスの固有名詞はユビキタス言語にすべき?

コードの意図の説明に有効だったら

すべき、という制限事項というよりも「それが一番わかりやすかったら、その言葉を使うのが一番いい。一番わかりやすい言葉でサービスを構築するのがいい」ということを おっしゃっていたように思います。

文字列はプレゼンテーションの関心事では?

関心事は近くにあったら変更しやすい

終始書籍でも関心事と実装の距離感については扱っていたように思います。これは賛否あるところだとは思いますが、実際のところ @nunulkさんも言っていましたが、私もcssのclassをドメイン的なクラスに実装していて、見通しは良いと個人的には思っています。

国際化対応は?

要件が発生していないし仕組みでそこまで負担なく対応もできると思っている

自分がやっていないことを、さもやっているようには言わない、という指針通りというところでしょうか。個人的には「国際化対応も加味した仕組みで構築されている」という響きに惹かれてしまいますが、国際化対応をすることが常に正しいというのは確かに偏っているというか教科書的かもしれないという考え方もあるかもしれないですね。初回リリースのサービス利用ターゲットが日本人を想定していて、その上で評価が良かったら、国際化対応を別スプリントで実施するというのも1つの考え方かもしれません。少なくとも日本語対応のみに絞れは、その分、開発スピードは早くなるので。あと、増田さんが言っていたように「表示文言のみの変更」であれば、業務ロジックの変更とは異なり シンプルなので ドメイン破壊は起きえないでしょう。そういう割り切りもありますよ、というところでしょうか。

SQLでupdateしないの?

どっちにしても過去の事実は「消えている」よ

むしろ更新は不自然とすら仰っていました。トランザクション内での振る舞いという意味だと、そうなんですよねぇ。ダーディーリードなど どうなるんだろう?と、実装依存で気になるところが無いわけではないけれど、そのあたりについて、私はそこまで詳しくないので なんとも言えないところです。

おそらく許容されないと思いますが、後述で紹介する奥野さんの話を聞いたときに「ん?ってことは削除も更新も事実の追記なわけだから、DBへの操作はインサートだけにすべきってことかな?」とすら思いました。ちなみに容量と性能に問題が無ければ、今でも そう思っています。

NotNull縛りって厳しくない?

RDBの思想としてはNullは正しくないよ

リレーショナルで考えると、そうなるんだろうなぁ、と私も思います。ある時点までなかった関連(つまりNull)を持ち込むのは良くないっていうのは 以前 セミナーで奥野 幹也さんが言っていて それが設計原則ではあるんですよね。少なくとも私は そう理解しました。新たな関心事の追加というのであれば、カラム(要素)の追加ではなく、テーブル(関心事)の追加になりますよね、という整理です。

www.slideshare.net

join書こうよ・書けるようになろうよ・IDEで楽もできるよ

たぶん、書かない・書けないということを前提に増田さんに質問をぶつけたわけではないと、私は思っています。指摘された方はJoinのコストのことを気にされていたのではないかな?と思います。私としては「非正規化は性能問題が観察されてからで良いと思う」という意見です。追加したい関心事が本来は存在すべき情報だったけど漏れていたということとであればカラム追加(つまりデフォルト値がある)、そうではなく新たな概念であればテーブル追加というのが私の理解です。少なくとも「たった1つの要素で1対1だからカラム追加で良いよね」ということであれば、テーブルの面積が大きくなりやすいアンチパターンな気がします。「たったこれくらい」はアリの穴の香りがするフレーズと思っています。

RestAPI細かすぎ

最小単位を作る原則の徹底

役割がシンプルな部品を作ることと、部品の組み合わせによりサービスを構築することがあるとしたら、まず前者を満たしましょうというのが、書籍において統一した思想というところでしょうか。WebAPIは外部インターフェースなので個人的には最小単位の担保はサービス側で良いかなぁという印象ですが実経験が数少ないので何とも言えません。ただ最小単位でAPIを作っておき、利用側からの性能面や利便性の具体的な要望に応じて組み合わせるという設計思想であれば「良かれと思って作ったけれど使われないサービス」は少なくなるかな、とは思います。

ドメインエキスパートとの会話は難しい

そもそも難しい。いないこともある。

別にDDDに限らず、アジャイルウォーターフォールに限らず、エキスパートは忙しいので会話することは難しいし、新しいサービスだったらエキスパートすらいないというのは納得(新しくなくてもいないこともある)。エキスパートがいないことを前提に振る舞う方が現実的というのも納得。

プロト操作はユーザーにとって嬉しいことになっている?

プロトを操作してもらうことは、開発側による仕様確認のお願い事ではなく、「触りたいな」「これが実現されたら、〇〇が良くなるだろうな」ということを早く体験したいとユーザー自身が思っていますか?ということかな。

書籍の内容は現場で生かせそう?

75%が好意的なのに驚き

121名のアンケートで75%が好意的だったのは驚きだったとのことです。書籍による紹介が「原理原則はこうであり、それ以外はダメ」というような大上段に構えたものではなく「このくらいからなら、できるかも」とイメージしやすい書籍だったと思います。*5

データベース中心(発言力大)だとDDDは難しい?

難しいかもしれないですね

そうなんですね。まだ 私は その違いが分かっていないようです。似て非なるとは思いますが、値オブジェクトなど踏み込めるところもあるかな?とは思いますが、たぶん、そういうところではないんでしょうね。

まとまったサンプルありますか?

実験場所があります。

実験場所ですので、これが正解というわけではありませんよ。

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

増田さんが心掛けていること

小さな実験をする

失敗をする方が多いからこそ、小さな実験をしていきましょう。無駄なことはありませんよ。

やりすぎてみる

@irofさんがやったみたいに、色々とやりすぎくらいのことをやってみたら分かってくることもあります。

時間はかかる、小分けに・少しずつ

簡単にフィードバックや結果が出ると思わないこと。自分自身も含めて、周囲も変化するためには時間が必要。

自分自身も少しずつ変っているもの。

成果物(コード)で測定を。昨日のコードよりも今日のコードの方が良くなっていることを実感できていますか?

信頼される人になろう

裁量範囲が広くなるのは良いこと。

QA

テスト実装無しでリファクタリングって大丈夫ですか?

上述(リファクタリングの邪魔に…)の通り。

値オブジェクトって他のシステムでも流用できるのでは?

経験的に言えるのは意外と完全一致というのは少ないと思うということ。似て非なるものなので、それだったら流用など考えずに その場その場で作った方が良い。

Entityについてもう少し話を…

Entityは2つの関心事(識別・集約ルート)がある。識別はDBの主キーという意味ではなく人の関心事の発見という意味で大事。集約ルートについては、なんでもかんでも1つに集約するのは反対。(関連によって分割するのが大事?)

数値・数量の計算の流用は?

値オブジェクトではなく、類似計算ロジックの共通化については あまりしていない。コードの重複を気にしすぎずベタで実装している。(「ロジックの重複の排除」よりも「ドメインの重複の排除」に注力しているということでしょうか?)

ドメインエキスパートはプロダクトが分かっているわけでない。落としどころは?

まず、一応、1つに集約しているインターフェース(なんでも管理画面)については分割を提案する。その上で見えるところ(外部インターフェース)はドメインエキスパートの考えを優先しても、バックヤードのサービスを細かく分割するなどしてバランスをとっている。

手続き型からオブジェクト指向に思考を切り替える方策は?

計算ロジックを値とロジックで1つにまとめて外に出す、というように小さいまとまりを少しずつクラスにして外に出すことを繰り返していくと徐々にコードが変わってくる。 また、変更の単位に注目して、それをドメインとして まとめるところを繰り返していくと良いのでは?

お勧め本の紹介

今回のセミナーの趣旨とはずれますが、この書籍が面白かった方や「システム開発かぁ、一通りプログラムは書けるようになったけど、さて…」という方に私のお勧めの書籍を紹介しておこうと思います。

以前の記事になりますが、若手の方が目を通しておいたらよろしいのでは?と思う書籍です。

vermeer.hatenablog.jp

これに追加して、悶々と考えるネタになる本として、以下が個人的にお勧めです。私自身が開発プロセスというチームにいたときに、システムアーキテクトってなんなんだろう?どういうことを考えないといけないんだろう?フレームワークってどうやって考えていくものなんだろう?と思ったときに手にした本です。正直、好みの分かれる本だと思います。

著者の萩原正義さんのお話をJJUG CCC 2013 Fall(R1-4 分散データ技術の再考)にて聞いたことがあります*6。すごく難しかったです。でも聞いているだけでなんだか賢くなった気になりました(笑)

さいごに

増田さんにご挨拶できて良かったです。自分の書評を改めて読んで少々突っかかり気味な表現など失礼なところもあったと思っていて せめて得体のしれない匿名な状態は解消をしておきたかったので。

あと今回の会には全く関係ないのですが、学生時代の先輩に偶然会ったのには驚きました。卒業以来くらいになるでしょうか。会計システムの会社に就職をされたのは記憶していたので、今もそっち系なのかな?と思いましたが、twitterMotohiro Imura (@imunew) | Twitter)を拝見させていただいたところ、かなり毛色の違う分野のようです。そして結構コアな感じのことをされているようで凄いと思います。

良い刺激を受けた日になりました。いやー、良い日でした!

*1:公開されている資料で、私が見つけたもののみです

*2:私は、@irofさんといえば「コードは読み物」というフレーズとともにインプットされています

*3:ちなみに、私も夏の実家への帰省の往復で2回読んで、書評にあたって付けた付箋のところを読んで、みたいなことをしました。

*4:E2Eテストツールを使ったことはないので塩梅が分かりませんが、網羅性の保証をすべてE2Eテストツールで満たそうとすると、それはそれで地獄が待っている気がします

*5:少なくとも鈍器こと エヴァンスDDD本は…

*6:実際には、ずいぶん後から気が付いたことなのですが。本棚の整理をしている時に「あれ?この名前はどこかで見たことがある気がする」と調べたら「あーあの人か!」と。

Pluggable Annotation Processing API Sample(実践編2)

前回の続き

vermeer.hatenablog.jp

今回はAnnotationProcessorを開発する際のテストのやり方です。一部、自分のライブラリの機能を使うところがありますが、それを除けば基本的に汎用的なものだと思います。

はじめに

前回の続きを想定しているので、以下のコードをクローンなりしてローカルのMavenリポジトリが作成されていることを前提としています。

Git

vermeerlab / apt-core / source / — Bitbucket

vermeerlab / apt-extend-sample / source / — Bitbucket

vermeerlab / apt-extend-sample-client / source / — Bitbucket

Maven

テストに必要なプロジェクトを追記します。
このプロジェクトがあることで任意のJavaコードをAnnotationProcessorの処理対象として指定することが出来るようになります。

<dependencies>
    <!-- for annotation processor start-->
    <dependency>
        <groupId>com.google.testing.compile</groupId>
        <artifactId>compile-testing</artifactId>
        <version>0.10</version>
        <scope>test</scope>
    </dependency>
    <!-- annotation processor end -->
</dependencies>

生成したコードの検証

テストコード

例示のサンプルは
vermeerlab / apt-extend-sample / source / — Bitbucket のものです。

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

@Test
public void 生成コードの検証() {
    Truth.assert_()
            .about(JavaSourceSubjectFactory.javaSource())
            .that(JavaFileObjects.forResource(Resources.getResource(
                    "test/Sample.java" ← 生成元になるコードのパス
            )))
            .processedWith(new AnnotationProcessorController(false)) ← 実行するAnnotationProcessor
            .compilesWithoutError()
            .and()
            .generatesSources(JavaFileObjects.forResource(Resources.getResource(
                    "test/SampleFactory.java" ← 生成されるコードの期待値と同じコードのパス
            )));
}

実行するAnnotationProcessor

テストや疎通確認時点ではAnnotationProcessorControllerの引数をtrueにしてDebugUtil#printで標準出力すれば、生成したコードの内容を確認することが出来ます。確認が終わればfalseにすれば標準出力はされなくなります。

補足

私は1つのコントローラーから任意のコマンドを実行するという方式なのでAnnotationProcessorControllerを指定しますが、一般的には生成処理毎にAnnotationProcessorを作成してサービスローダーに登録すると思います。その場合は、processedWithの引数に作成したProcessorを指定してください。

期待値となる生成コード

標準出力で生成されるコードの確認は可能ですが期待値検証にはなっていません。同値のコードを準備しておくことで継続性を担保できます。

なのですが実際には地味に手間取りました。ということで私の手順です。

  1. 期待値となるコードの「ファイルだけ」を作成する
  2. AnnotationProcessorController(true)にする
  3. テストを実行して生成コードを標準出力する(テストはエラー) f:id:vermeer-1977:20170819193958p:plain

  4. 意図通りの結果が出力されているか確認して意図通りになるまで生成ロジックを修正する。

  5. 標準出力されたコードを期待値となるコードファイルにコピー&ペーストしてコンパイルエラーが無いことを確認する。

  6. IDEではなくテキストエディタなどで「直接」コピー&ペーストして保存する

  7. テストが成功で終わることを確認する

私がはまったところ

期待値比較は完全一致

IDEで自動整形されていても内容が正しければ検証にて正常と評価されると思っていました。 テキスト比較で評価しているようで体裁も含めて完全に一致していないといけませんでした。

完全一致しているはずなのに…

はじめの一行目の空白行が足りなかったため標準出力の内容をテキストエディタでコピー&ペーストしているのにテストエラーが解消されないという事象でした。標準出力している内容をテキストエディタでコピー&ペーストしてもテストエラーになる場合は1行目に空白行を入れるといった「標準出力では気が付きにくい違い」がある可能性があります。

その他

格納場所はsrc/test/resources配下にクラスのパッケージと同じフォルダを作成して格納します。インプットとアウトプットともにパッケージ構成次第では同じ場所に格納されるので命名ルールに注意が必要です。

さいごに

こんな感じで手間はかかりましたが、以上が私の考えるAnnotationProcessorのテスト手順になります。
生成後に期待するコードを作成すると言いつつ、標準出力されている内容のコピー&ペーストというのも変な話ではありますが、そもそも生成したいコードはコード編集ロジックを組み立てる前に検討しています。そういう意味では検証結果に そのまま使用はしないけれども 期待するコード相当のものは作成しています。特にJavaPoetのようなライブラリを使ってコード編集をする場合、事前に検討もせず構築するのは結構無謀だと思います*1

追記

2017/8/19

ブログ記載時の記述に一致するブランチを作成してリンクを修正。

*1:私自身、このくらいなら出来るだろうと思ってJavaPoetでコード生成をしようとして、「あれ?どんなクラスを作るつもりだったっけ?」と無駄に時間を使ってしまいました。

Pluggable Annotation Processing API Sample(実践編1)

以前の記事の続きです。

vermeer.hatenablog.jp

予定としては この流れで作っておきたいツールがあるので それを作り切るまで 続けたいと思っています。

はじめに

開発環境

Java8、Maven、Netbeans8.2

コード

全てのリポジトリは以下のプロジェクトに格納しています。

https://bitbucket.org/account/user/vermeerlab/projects/AN

目指したところ

  • ライブラリ群として沢山のAPIを作成しておいて必要なものだけを使用するような仕組みがあると嬉しい
  • 統一した処理が出来るようにインターフェースを準備する

APIの作成と登録(基本)

基本となる機能や使い方についての説明です。

Git

vermeerlab / apt-core / source / — Bitbucket

実行するAPIを作成

実行クラスパスとしては「テスト」内なので違和感があるかもしれませんが、テスト実施と基本ライブラリにサンプルを持っておきたいと考えた結果、このような構成になっています。

対象を特定するAnnotationを作成

マーカーとなるAnnnotationインターフェース(TestScan.class)を作成します。このインターフェースを記載している要素が処理対象となります。

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface TestScan {

}

実行するコマンドクラスの作成

インターフェース:ProcessorCommandInterfaceを実装したクラスを作成します。ラウンド毎に実行するコマンドです。

public class ProcessorCommand implements ProcessorCommandInterface {

    @Override
    public Class<? extends Annotation> getTargetAnnotation() {
        return org.vermeerlab.apt.command.test.TestScan.class;
    }

    @Override
    public void execute(ProcessingEnvironment processingEnvironment, Element element, Boolean isDebug) {
        CommandValidator validator = CommandValidator.of(TestScan.class);
        validator.validate(TestScanValidation.of(element));

        ProcessingEnvironmentUtil util = ProcessingEnvironmentUtil.of(processingEnvironment);
        DebugUtil debugUtil = new DebugUtil(isDebug);
        debugUtil.print(util.getPackagePath(element));
        debugUtil.print(util.getTree(element).toString());

        TypeMirror typeMirror = element.asType();
        Boolean isSameType = util.isSameType(typeMirror, Object.class);
        debugUtil.print("isSameType=" + isSameType);
    }
}

メソッド:getTargetAnnotationの戻り値として設定した値が処理対象となるAnnotationインターフェースです。
今回はTestScan.classです。

メソッド:executeProcessorラウンドにて実行します。
今回は検証を行います。Javaコードの生成は行っていません*1

最後に実行するコマンドクラス(任意)の作成

インターフェース:PostProcessorCommandInterfaceを実装するクラスを作成します。
本クラスの作成は任意です。

用途としてはAnnotationで処理全体の結果を踏まえて何かをしたい時に使用します。

サンプルでは処理件数を標準出力しています。

public class PostProcessorCommand implements PostProcessorCommandInterface {

    private Integer count = 0;

    @Override
    public Class<? extends Annotation> getTargetAnnotation() {
        return org.vermeerlab.apt.command.test.TestScan.class;
    }

    @Override
    public void execute(ProcessingEnvironment processingEnvironment, Element element, Boolean isDebug) {
        this.count++;
    }

    @Override
    public void postProcess(ProcessingEnvironment processingEnvironment, Boolean isDebug) {
        DebugUtil debugUtil = new DebugUtil(isDebug);
        debugUtil.print("TestScan.class count = " + this.count.toString()
    }
}

検証クラス(任意)を作成

インターフェース:ValidationInterfaceを実装するクラスを作成します。
本クラスの作成は任意であり、またヘルパークラス用のものなので使用も任意です。

用途としてはAnnotationで取得した要素(Element)に対して何かしらの検証を行う際に使用する統一インターフェースです。

public class TestScanValidation implements ValidationInterface {

    private final Element element;

    private TestScanValidation(Element element) {
        this.element = element;
    }

    public static TestScanValidation of(Element element) {
        return new TestScanValidation(element);
    }

    @Override
    public ValidationResult validate() {
        ValidationResult result = ValidationResult.create();
        if (this.element.getSimpleName().toString().equals("ErrorTestTarget")) {
            result.append(ValidationResult.of(ValidationResultDetail.of("TestScanValidationError")));
        }
        return result;
    }
}

コマンドクラスのexecute()内でCommandValidator#validateの引数にすることで、複数の検証クラスを一度に実行できるヘルパーに使用することが出来ます。したがって、自分で検証の仕組みを設けたい場合は、使う必要もありません。

サンプルではクラス名によって検証を行っています。*2

上述のコードだと以下の部分です。

CommandValidator validator = CommandValidator.of(TestScan.class);
validator.validate(TestScanValidation.of(element));

validator.validateの引数は複数指定できるので、elementの種類毎に色々な検証をしたい場合にイテレーターパターンで検証をします。というか、それだけのヘルパーです。

作成したAPIを登録

実行クラスパスとして今回はテストクラスパス内のresources配下のprocessor-command.xmlに実行するコマンドとして作成したコマンドクラスを指定します。

メインパッケージのresourcesprocessor-command.xmlが存在しますが、これはライブラリの基本構造を示すということと、基本ライブラリをそのままForkして拡張することを鑑みた対処です。したがってファイル内に実行するコマンドクラスの指定をする記載もしていません。

コンパイル

コンパイルによりAnnotationProcessorが動いたように思えますが、実際はテストクラスにて実行されただけです。
今回のケースではコード生成をしていないのでtarget配下にコードが作成されることはありません。

APIの作成と登録(拡張)

基本となるライブラリを使用して独自のAnnotationProcessorライブラリを作成してみたいと思います。

実際に作ったコマンドは前回の記事でやったこととほぼ同じことをするものにしました*3

説明は基本編との差分のみにします。

Git

vermeerlab / apt-extend-sample / source / — Bitbucket

Maven

基本ライブラリを依存ライブラリとして追加します。
加えてAnnotationProcessorを使うときにお約束になるコンパイル時の指定を追記します*4

<dependency>
    <groupId>org.vermeerlab</groupId>
    <artifactId>annotation-processor-core</artifactId>
    <version>0.1.0</version>
</dependency><plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.6.0</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <!-- Disable annotation processing for ourselves. -->
        <compilerArgument>-proc:none</compilerArgument>
        <showDeprecation>true</showDeprecation>
    </configuration>
</plugin>

実行するAPIを作成

APIの作成と登録(基本)』ではテスト内に作成しましたが、今回はきちんとメインパッケージに実装をします。

コマンド実行クラス:SampleCommand.java

処理対象アノテーションTargetClass.javaTargetField.java

作成したAPIを登録

テストのためにテストパッケージのprocessor-command.xmlに登録をしています。

メインパッケージのresourcesprocessor-command.xmlを作成する必要もありませんし、もしファイルがあったとしても作成したコマンドを追記する必要はありません。

コンパイル

  • 生成のトリガーとなるクラス
package test;

import org.vermeerlab.annotation.processor.sample.TargetClass;
import org.vermeerlab.annotation.processor.sample.TargetField;

@TargetClass
public class Sample {

    @TargetField
    private final String _name;

    @TargetField
    private final String _desc;

    public Sample(String name, String desc) {
        _name = name;
        _desc = desc;
    }

    public String getName() {
        return _name;
    }

    public String getDesc() {
        return _desc;
    }
}
  • 生成されたクラス
package test;

import java.lang.String;

public final class SampleFactory {
  public static Sample create(String _name, String _desc) {
    return new Sample(_name,_desc);
  }
}

APIを利用するクライアント

作成したAPI群を使用するクライアント側プロジェクトです。

作成した拡張API群をフレームワークとして配布して それを使う側の手順になります。

Git

vermeerlab / apt-extend-sample-client / source / — Bitbucket

事前条件

拡張API群をMavenコンパイルしてローカルにMavenリポジトリが存在していること*5

Maven

pom.xml(関連個所のみ。お約束も忘れないようにということで明記)

<dependency>
    <groupId>org.vermeerlab</groupId>
    <artifactId>annotation-processor-extend-sample</artifactId>
    <version>0.1.0</version>
</dependency><plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.6.0</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <!-- Disable annotation processing for ourselves. -->
        <compilerArgument>-proc:none</compilerArgument>
        <showDeprecation>true</showDeprecation>
    </configuration>
</plugin>

実装

AnnotationProcessorの処理対象となるAnnotationインターフェース(@TargetClass)が設定されている生成トリガーとなるコードは、拡張APIのテストコードとほぼ同じなので省略します。

生成する予定のクラスを参照しているクラス(GeneratedUser.java)はリポジトリからクローンした直後は、コンパイルしていないということで まだコードが生成されていないので エラーになっています。

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

コンパイル

  • generated-sources/apt配下にクラスが作成されて コンパイル前にエラーになっていたクラスからエラーが消えました。

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

さいごに

メリット

使い方がシンプル

クライアント側はライブラリの依存だけとAnnotationProcessorを動かすための記述をすれば良いだけです。

ブラックボックスではない(と思う)

Processorのコマンドクラスをクライアント側で指定するので どういう操作をしようとしているのか分かるのでブラックボックスでは無いと思います。機能の提供側としては機能一覧としてprocessor-command.xmlに記載する内容を提示しておいて、利用側が必要なものを選択する、または不要なものを外すことで対応可能です。

デメリット

コマンドの記述が繁雑

実行ライブラリのクラスパスにprocessor-command.xmlを配置しないといけないません。つまり利用側が自分のresources配下に機能一覧を追記したprocessor-command.xmlを配置しないといけません。最低でも提供側が定義ファイルに記述する内容をルートパッケージのJavaDocなどに記載したり、手引きに書いておかないと、何をどうしたら良いのかわかりません*6

解消案としては、基本ライブラリのProcessorCommandManagerにてxmlからコマンドクラスのパス(文字列)を取得しているので、ハードコーディングをするということが挙げられるでしょうか。 *7

次回

今回はコマンド作成と登録と起動をメインに整理しました。次回はテストについて投稿したいと思っています。なお説明に使用するテストコードはすでに今回のプロジェクト内に入っているものを使う予定です。

追記

2017/8/19

ブログ記載時の記述に一致するブランチを作成してリンクを修正。

*1:基本となるライブラリへの依存ライブラリを最低限にしておきたかったためです

*2:単にテストケースの網羅性を高くするためだけのものです

*3:したがって、あえてコードの主要な記載はコピー&ペーストにしています

*4:こちらを忘れて毎回 コードが生成されているのにクラスが参照できなくて「何でだ!!」と頭を抱える

*5:本記事を上から順番に実施していたら出来ていると思います

*6:このあたりを良い感じにできれば良かったのですがクラスローダー周りの実装力が無かったため断念しました。

*7:個人的には このあたりについて自分なりの落としどころを考えて もう少し対応したいと思っていますが、とりあえず作ろうと思っているものを一通り作ってから改めて取り組もうと思っています。

【書評】現場で役立つシステム設計の原則

読みました。断片的に収集していた増田亨さん(@masuda220)の知識を理解するのに良い本だと思います。もしDDDについて調べていて増田さんのスライドなどを見て「もう少し詳しく知りたいかも」と思った人は読むことをお勧めします。

良かった

ロジックをenumを使って表現する

P60:
EnumStrategyを管理するという言い方になるのかな?確かにこういうのは分類と実装を意味ある塊に整理出来るので良さそうです。

調べたら、こういうのもありました。
【enum】メソッドの定義(3)−strategyパターンを使う方法 - THE HIRO Says

メソッドは必ずインスタンス変数を使う

P81:
インスタンス変数を使わない理由があるとしたら、そこには更なるクラス設計が出来るかもしれないと思って疑ってみる。

確かに引数だけを使ってインスタンス変数を使わないメソッドを作り始めると神Utilみたいなのを作りやすいです。また整理のやり方としては1つにまとめるべきかもしれないけど、往々にして「じゃぁ、これもこの概念にまとめておいた方が良いよね」となって設計の粒度が属人化することもあります。クラスは抽象的な概念で まとめるよりも関連するデータ(プロパティ)を中心に設計するという指針にしておけば「気がついたら巨大になっていた」「設計者によってクラスの粒度が大きく異なっている」というのは是正されるかもしれません。

P83:メソッドが全てのインスタンス変数を使うようになる
使うように「する」でも良いかもしれないです。環境変数の取得などもあるので「絶対」ではないですが、使っていない理由を整理していくと結果として「これは別クラスにしておこう」ということになるように思います。

スコープは可能な限りパッケージスコープに

P85:
IDEの補完においても余分な候補が出なくなるので公開窓口となるクラスやメソッド以外はパッケージスコープにしておくことについては賛成です。ただ、ドメインオブジェクトはフラットにどのレイヤーからでも参照することを前提に作ると考えると、パブリックが基本になりそうな気がしますが。

ドメインモデルとデータモデルは何が違うのか

P100:
カチッと分かったという感じではないけれど、何となく言わんとしようとしていることは分かったという感じです。1つ言えることは本書を読むまでの自分は どちらかというとデータモデルありきの発想だったので、何となくでも意識できるようになったのは良いことだと思います。

サービスクラスを分ける

P160:
参照系と登録系に分けるという発想は持っていませんでした。副作用のあるサービスと、副作用のないサービスはWebAPIのところでも触れられているように、ちゃんと分けておくと影響範囲調査などするときなど楽になるので、今後の実装で取り入れていきたいと思いました。

記録の変更を禁止する

P184:
以前、PostgreSqlの内部の仕組みの説明を聞いた記憶では、DBログとしては全て追記しておき それをサマリーする、というような話だったので 気が付いていないだけで低レベル実装としては更新では無くて追加をしています。また障害時もログ(履歴)+リカバリーポイントから復元します。DB自体がそういうアーキテクトであることから鑑みても変更というよりもコト履歴の赤黒追記でそれをサマリーするというのには賛成です。
ただ履歴のシーケンスの管理については別途考察が必要になる可能性は残るように思います。例えばサーバーを跨ったマイクロサービスなどの場合、順序性の保証をするためには何かしらの仕組みが必要で、その払い出し性能がネックになることは起こりえます。少なくとも全ての払い出しを1つのテーブルでやろうとしたら大量のリクエストでパンクします。接続ユーザーグループ毎にパーティションを作るとか そういう仕組みの検討が必要になる可能性があるかも、という話です。まぁそこまで非機能要件が厳しい場合、更新であっても別の検討が必要にはなっていると思いますが。

「記録の変更を禁止する」の一番の抵抗勢力は経験豊かな(?)DBAかもしれません。「マスタは頻繁に更新されないし、手で直すだけで対応できるんだから簡単でしょ?大丈夫大丈夫、更新日付のカラムを書き換えるべしっていうルールがあるから。ちょっとした修正なのにレコード追加とかシーケンス付番とか作業が増えるだけで事故の元だよ」と。当然そういう人は削除フラグ(もしくは削除日)も大好きです。

テーブル設計を優先して…

P192:
テーブル設計を優先してオブジェクトをそれに合わせるアプローチはロジックの整理に失敗する、というは、ずっとモヤモヤしていたところだったのでスッキリしました。

具体的には マッピング=形だけのDTO=あんまり良くないことでは?というのと、テーブルの関心事とドメインの関心事は違うから似て非なるものだから気にしすぎなくてよいのでは?というループです。考察の切欠はテーブル定義からドメインモデルを自動生成すると良いのでは?とボンヤリ考えていたところから。ただ自動生成をしても良いけれど、逆にその生成物があるためにドメインの検討の邪魔になるのでは?とも考え始めて…(ループ)。
私としては、違うものととらえるべきだということに整理が出来たので満足です*1

個人的な好み

書籍と私の好みの違いの整理。

状態遷移はStateパターン

P63:状態の遷移のルールをenumで管理

Stateパターンの方が個人的には好みです。

Enumだと状態遷移を全体として俯瞰して表現できるという考えもあると思うけど実際に見てみて私には分かりにくいものでした。状態遷移は状態遷移図とステートマシン図を使って検討・設計をしてテストで保証するというのが好みです。少なくとも、このEnumクラスでユーザーと仕様を共有するのはツライです *2。 このあたりは「状態を管理するクラス」を選択するのか「自分の次の状態は自分で管理する」のか、どちらを好ましいと思うのか、という違いかもしれません。前者は全体の俯瞰ができるメリットがあり、後者はクラスの責務が凝集しているけど全体の俯瞰は補助資料が必要です。

形式的な資料はかえって危険

P140:(P248にて補足あり)
形式的なドキュメントに「議事録」が入っているのは疑問です。意思決定や合意形成のアウトプットとして議事録は簡易的ではあるがエビデンスになりうるものと考えているからです。もちろん形式的なドキュメントの作成は私自身も必要最低限にしたいと思っているし、勘弁してほしいとすら思っています。ただし議事録・課題管理・QA表など経緯を把握できるドキュメントは後から参画したメンバーに仕様確定の経緯を深めてもらうために便利なので手抜きはしない方が良いです。とくにユーザーとの合意は特別扱いしておきたいです。ホワイトボードがそれにあたるというのであれば議事録相当の合意事項を箇条書きなりにして最終合意事項としてスナップを取っておかないと、打合せ非参加者が見た場合、重要なポイントが不明瞭になる可能性があります。 少なくとも 議事録の体裁にこだわる必要は無いとは思うけれど、ホワイトボードに書かれた検討メモや構成図をもって合意形成の確定とするのであれば、第三者が見ても誤認しないようなレベルの「まとめ」はしておいた方が良いでしょう。 *3

書籍の意図も、ホワイトボードがあるから議事録不要ではないとは思っています。ただ、「議事録作るの無駄」と整理すると事故が起きた時にシンドイですよ、ということを私は言いたかっただけです。ちなみに私は開発メンバーとの意思疎通をするときには、A3の紙か、ホワイトボードに書きながら話して、打合せが終わったら印刷して共有、というのを良くやっていましたので、その便利さは良く知っているつもりです。ただしその記載も責任の所在も全て「リーダーである私のみ」という前提でした。検討資料のインデックスはホワイトボード資料のファイル名か「私の記憶でいつくらいに、その話をしたか辿って探す」でした。開発チーム内であれば それでも良いですが、ユーザーとの合意事項を それと同等に捉えるのは個人的には抵抗があります*4

契約による設計

P165:
防御的プログラミングでのやり過ぎ懸念は分からなくはないけれど、そうではない契約による設計がイコールでシンプルになるというのは疑問です。ユースケース記述を例にしても、自分のユースケースを実行する事前条件の定義は、自分で一義的にするのが筋が良いと思いますし、仕様が漏れ出ていかないと思います。呼出側としても「何が返ってくるかわからないという前提でから様々な検証コードを書きます」とあるけれど、それは呼出側として後続処理を行うにあたって必要な「事前条件」の検証をしないといけないだけでは?と思います。
Nullの扱いについては、確かに課題ですが、それはプロジェクト全体で統一することであって、それがイコールで防御よりも契約に、とはイマイチつながらない気がする*5。個人的にはサービスの戻り値の型(つまりドメインオブジェクト)+例外+JavaDocというインターフェースを持って、基本的な約束事は満たされるようになると思っています。
ひょっとしたら私の理解が誤っている可能性がありますが そんな風に思いました。

あと「例外を使うのは、通常の使い方ではあまり起きない場合に限ります」も同様に疑問です。例外は通常云々ではなく「事前条件不正」(事前条件だけではないですが)というような主たる関心事を満たせない場合のルートだと思っています。そうじゃないと古式ゆかしきリターンコードで対応するのかな?という印象しかないです*6。BeanValidation(入力値検証)も事前条件だと考えますが検証不正時は非チェック例外で表現します。それを「通常の使い方ではあまり起きない場合」というのは違うのではないでしょうか?

書籍の例として「ゼロ除算」を挙げていましたが、ゼロを格納した変数(クラス)が除算に使用されるかどうかを呼出元は知りません。知っている=仕様が漏れ出していることになるのではないでしょうか? もし 呼出先としてゼロがもし渡されたときに どうしたら良いか分からず、呼出元に対応を強制するというのであればチェック例外で呼出元に対処を明確に促すべきではないでしょうか*7。もしくは非チェック例外&JavaDocで表現するというやり方もあります。*8

2017/8/19 追記
増田さんの趣旨もしくは その一端が以下のtwitterで言っていることだとしたら、私の「契約による設計」の書評はズレているように思います。いずれにしても、このあたりは具体的な実装および実現技術を踏まえないとイメージも難しい気がします。ボンヤリ「こういうことかな?」というのはあるのですが、私は言語化できていないです。

2017/8/28 追記
入力値検証はBeanValidationにしてドメインロジックに混入しないことにするってことかな?

分析者が実装まで行う

P285:品質保証
「分析と設計が一体となった開発」についてのアンサーとして(かなり端折っていますが)分析者が実装まで行うというのは同意です*9。 それとは別アプローチとして、実験的にこういうのもあるのでは?と思うことを追記しておこうと思います。例えばペアプロ(どちらも実装ができる人+どちらかが仕様への理解が明るい)とかモブプロ(要件は分かるが実装は出来ない人も参加することを想定しているので厳密なモブプロとは違うけれど)とか、ユーザー自身も含めた分析者とドメインの理解が浅いけど手は動かせる実装者が一体になって開発するというような「仕組みの構築」も一案としてあるかなと思いました。
例えば、モブプロを通じてリファクタリングをしないと、開発スピードが だんだんと遅くなるという体験の共有も大切かもしれません。

だんだん開発スピードが遅くなっていくのをどうやってとめたら良かったんだろう? - Mitsuyuki.Shiiba

とはいえ、私が言っている案の方がSIerの現場だと非現実的な気がしていますが(ペアプロは出来るかな?)。

ちょっと残念?

勝手に私が期待していて物足りなかったところ。

画面とドメイン

P211:
「複数の関心事が混在している「何でも画面」を提供する場合は、ビュー専用のオブジェクトを…」について画面とドメインの連携で書かれていると思っていました。タスクベース画面はウィザード操作的であり業務習熟度が上がった担当者にとっては まどろっこしい入力になります。また高い習熟度は「関心事」として捉えられる範囲が広がるので「何でも画面」と一括りにもできないと思います。とはいえ全部のデザインについて1冊の書籍で網羅することを求めるのも違う気がするので、あくまで「個人的に物足りなかった」という感想です。

重箱の隅

本質に関係のないところです。

コーディング作法

Javaっぽくないかもと思ったりしたところ。この辺の作法は好みもあります。

if文の書き方

メソッドの行数を減らすために「{}」を使っていないような印象を受けました。スコープの範囲を明確にした方がバグが生まれにくいので個人的には好きではない書き方です。実際事故も起きています。行数を少なくすることは手段であって目的ではないと思います。

AppleがiOS7.0.6で修正したSSLバグの簡単な解説 - Qiita

3行は「{}」のあるif分岐すら実装できません。また「1行で書いてみました!」と過剰に行だけは少ないけれど、メソッドの仕事としては沢山の事をしている実装をされても困ります*10

私としては10行以内を目標に20行くらい、多くても50行というのが現実的なように思います。

参考:

もちろん行数を増やしたところで、上述の懸念は残ります。私は集中できる時間帯というのを「息を止めていられる時間」と考えています。なので後輩たちにメソッド実装をしてもらうときの基準として、上述の行数とあわせて「メソッドは軽く息を止めて実装の意図がすんなり分かるくらいの規模で」というようにしていました。コードが複雑でも短ければ許容できるし、長くても単純なボイラーだったら無理に分割しなくても良いかな、と。当然、個人差はありますが「コードは読み物」という意識は持ってもらえるかなと思っています。

2017/8/19 追記

増田さんのブログ引用
if文のブロックは必ずメソッドに抽出して(省略)
else句は原則使わないようにしているので、ほとんどのif文は、このような一行形式で記述でき(省略)
手ごたえを感じている書き方です。

条件ロジックは分割することで複雑度を減らしている&3行くらい、というこで私が事例に挙げたようなバグは混入し辛いということになるでしょうか。私としては本件は「重箱の隅」よりも「個人的な好み」に分類した方が良いところだったかな、と読み返して思いました。

ブロックの書き方

P58:
クラスのブロックはJava風だけど、static初期化ブロックが.Net風というか、初期化ブロック風。static初期化ブロックと初期化ブロックでは初期化の順番が違うので紛らわしいコーディングはしない方が良いと思います。*11

参考: 【Java】初期化ブロックについて - TASK NOTES

Enumの列挙子が小文字

P60:
Enumの列挙子が小文字始りになっていてpublicプロパティを直接参照している印象を受けます。列挙子は定数的な扱いなので全て大文字の方が好みです。

参考: Java列挙型メモ(Hishidama's Java enum Memo)

誤記?

P111
判断/加工/処理

P144
判断/加工/計算処理

おそらく「判断/加工/計算」の誤記だと思います。

ドメインを参照するレイヤー図

P150: ドメインロジックはアプリケーション層だけから使う、という整理だったかな? 少なくとも増田さんの以前のスライドでは、そうなっていなかったように思います。

www.slideshare.net

2017/8/19 追記

増田さんのブログ引用
処理の流れのイメージとしてこっちのほうがわかりやすいと思って、ドメインモデルへの矢印を、アプリケーション層だけに絞ってみました。

個人的には3層から使用できるということがドメインモデルのポイントだと思っていたのと、増田さんの3層からの矢印スライドを見て「ドメインってそういうことなのか~」と理解が深まったということもあって妙に拘ってしまいました。

さいごに

批判的なことも書きましたが総じて良い本だと思います。OJTの名のもとに師事する人(メンター)が現場の人だけだったという人は 一度 目を通しておくと良いと思います。また、なんとなくですが 3年くらい経験を積んだ人が読むと良いと思います。

増田さん自身による書評のまとめ
たくさんの書評、ありがとうございます | システム設計日記

*1:ということで自動生成ツールの作成は当面しない

*2:とはいえ本書籍はEmunの使い方集ではなく、こういう整理をしましょう、という本だと思うので あまり突っ込むのも野暮な気もする。

*3:少なくとも、私の経験では 最終アウトプットで合意を取っているからOKでしょ、というプロジェクトは経緯や考察への配慮が不足していて、結果として炎上プロジェクトになっていました。また、議事録・メールなどのエビデンスにより相手方の承認履歴があったことで資金回収が出来たという個人的な経験によるところも大きいです。

*4:別にお客様は神様です などと思いませんが

*5:lombokアノテーションもあるし

*6:であったとしても、本質的に満たそうとしている事前条件不正を呼出元に通知するという目的は同じです

*7:チェック例外の良し悪しは横に置いておきます

*8:私は非チェック例外派なので後者を選びますが

*9:というか、設計から実装まで出来るメンバーが集まっているのであればオブジェクト指向云々に関係なく、それなりに高い品質を担保できるとは思います。実際、初見メンバーで各社から集まってもらったチーム(自社で開発技術がある要員は実質私だけ)での開発をしましたが、設計力と実装力が一番ある人をプロト開発に、実装力はあるけれど経験年数が若い人をコアドメインの設計者に、そして私は方式検討と仕様課題と実装課題の検討にのみ注力することで、超短期間の開発であったけれども どうにか乗り切ることが出来た経験はあります。

*10:MSXの1画面プログラムみたいな。凄いとは思うけど写経してゲームはしたけど、ちっとも意味は分からなかった。Cのポインタ使いも同様。複雑な正規表現も同じく。ただ正規表現については、自分がもっと勉強をしないといけないと思います。

*11:実際は整形についてはIDE任せなので問題はないけれど