【第2回】ドメイン駆動設計のための オブジェクト指向プログラミングに参加しての個人的メモ

【第2回】ドメイン駆動設計のための オブジェクト指向プログラミングに参加して

超個人メモだけど、良い経験になったのでブログにメモを残します。

見てる人いるかわかりませんが、ユーザ定義型の数が多かったと発表したのは私です。 15-6かな?って言いましたが、19個ありました。 今日のテーマに沿ってやったつもりだったんですが、みんな全然着眼点が違かった…。 とても勉強になりました。

序章

  • モデルとは、人間が頭のなかに理解しているものをさす

    • それを言葉や図などで表すのと同じように、コードでも表現すると言う発想をする
  • モデルをコードで表現する、というのは今日のキーワード

    • 型!
  • 問題領域の例として、アソビューのサービスを取り上げる

    • それをどうやってモデル化するかについて取り組んでみる
  • UMLが厳密なものという意識を持っている人がいるかもしれない

    • DDDの文脈ではUMLはラフスケッチ用
    • コミュニケーションに使いやすいから使っているだけ

キーワード

  • 型をプリミティブ型でどうやって表現しているのか

    • StringやLocalDateなどの標準クラスを見るととても勉強になるよ
  • BigDecimalは型の教科書としては良くないよ

  • 独自の方を定義する仕組み

    • class
    • interface
    • enum
    • (package)は厳密には型ではないけどそれを取りまとめる役目に使える

アンチパターン

  • 基本データ型への執着
    • これをやると、メソッドが長くなったり引数が増えたりクラスが巨大化したりする
      • これが他の不吉な臭いに伝搬する根本原因(か?)
    • 逆にユーザ定義型に執着するほど、オブジェクト指向の旨味が出てくるとも言える
  • 処方箋

    • 値オブジェクト
    • コレクションオブジェクト
    • 区分オブジェクト
  • 執筆した本では局所化すると言ってるが...

    • 実際には変更の分散は起きる
      • しかし、推測可能だったりツールが変更の影響箇所を教えてくれたりとか、変更の難易度は下がる

型について

  • 基本データ型をラップするとは?

  • ユーザ定義型がいい理由

    • 正しい
    • 表現力
    • Find Usage
  • 正しさ

    • 基本データ型

      • 汎用的
      • 値の範囲
      • 可能な操作
      • 特定の目的のためには、間違った範囲、間違った操作を含んでいる
    • ユーザ定義型

      • 値の範囲を用意に合わせて制限
      • 可能な操作を用途に合わせて限定
      • 間違いが減る
      • 安心・安全
  • 表現力

    • 基本データ型
      • いろいろな用途に同じ型を使う
      • 意図が不明
      • 10個とか多い単語で説明されてもわからない
    • 意図が伝わらない

    • ユーザ定義型

      • 用途が明確になる
      • 引数の型、メソッドの型、シンプルのメソッド名
      • 型をレビューすればコード内容が推測できる
    • ドキュメント性がある
    • 引数と返りの型をユーザ定義にすると、意図がわかりやすくなる
    • 増田流レビュー

      • else文がある場所やネストが深い箇所を探してピンポイントにレビューする
      • ユーザ定義型を使ったコードだとメソッドまでみれば、その人が業務をわかっているかどうか判断できる
        • 怪しいのはなんでこの型が入ってるの?何が正解なの?という感じで業務を取り違えている可能性があるので警戒する
    • Find Usage

  • ドメインに特化したユーザ定義型

  • ユビキタス言語などから型を探す
  • 汎用の型を用途限定にラップして型にする
  • 既存アイデアの流用/カスタマイズ
    • 即効性はないが、知識を積み重ねておくと後から効いてくるかも?
  • 型の振る舞いの設計パターン
    • 判定(同地、大小、最大/最小)
    • 加工(型変換、短縮形、合成)
    • 計算(四則演算)
  • 他のところから考えた型を別の指標で検算してみることもある

勉強会で与えられた課題

  • スライドの表を実装するにあたり必要となりそうな独自の型を定義してモデルと実装を一致させる(ために思考してみる)
    • コードで書けるときにはコードでいきなり書いちゃえ、コードでかけないときは図などに頼る
    • 英語が思いつかないなら、日本語でもいいかもね

エピローグ

  • たとえ1行だとしても、ロジックが出て来たらオブジェクトに抽出しておくと吉。

    • どんなコードでも正しく動いてしまうと後から書き換えるのは心理的に辛い
  • 現場に導入しづらい?

    • 前段階として、リファクタリング
      • 大きなクラスを分割してみるアプローチがいいと思うよ
  • テスト駆動開発でいってた良いこと
    • 『やりすぎて見てからちょうど良いところが分かるよ』
      • どのへんで折り合いつけるのかわかってくるので、一度やりすぎてみるとよい

以上!あー、楽しかった。

gradleで生成される依存ライブラリの並び順をlibrary名でソートする方法

は、以下の記述をbuild.gradleに追加してください。

eclipse {
    classpath {
        file {
            whenMerged { classpath ->
                def libs = classpath.entries.findAll { it.kind == 'lib' }
                libs = libs.collect { lib ->
                    def baseDir = project.projectDir.getAbsolutePath().replace('\\', '/')
                    if (lib.path.startsWith(baseDir)) {
                        lib.path = lib.path.replace(baseDir, ".")
                    }
                    return lib
                }
                libs.sort(new Comparator<Library>() {
                            public int compare(Library lib1, Library lib2) {
                                String basename1 = new File(lib1.path).getName()
                                String basename2 = new File(lib2.path).getName()
                                return basename1.compareTo(basename2)
                            }
                        })

                def others = classpath.entries.findAll { it.kind != 'lib' }
                classpath.entries = others + libs
            }
        }
    }
}

ただし、eclipseにgradleプロジェクトとして認識させると.classpathを使わずにプラグインの機能を使うようになってしまうため並び順が制御できなくなる。

gradleプロジェクトとしてeclipseに認識させていない場合、以下が不便です。

  • 依存追加時に./grdalew cleanEclipse eclipseで依存ライブラリを取得する必要がある
  • Eclipseからgradleタスクを実行できない(私はCLI派なので困らないけど)
  • Spring Tools使うためには有効化の操作しなきゃいけない

Docker on Centos7 on Vagrant with Chef

概要

掲題の通りのローカル開発環境を構築してみた。

コンセプト

目的

  • RabbitMQやRiakはライブラリでノード指定時に、config.addresses("192.168.33.10","192.168.33.11")みたいな書き方をするので、それをローカル環境でやりたい
  • MySQLは更新系はMasterで、参照系はSlaveなんてケースあるだろうから、Master/Slave構成をローカル環境でやりたい

動かしてるミドルウェア

何をやったのか

後述する参考サイトで、クラスタリングやら固定IPの割付やらは実現されている。
それぞれのdocker-compose.ymlをちょっと修正して、chefのrecipe化して、vagrant upだけで環境を構築できるようにしたところがミソ。
chefでdockerインストールしてdocker-compose upを実行してたりするところもやや難しかった。

成果物

github.com

参考にしたサイト

docker containerへの固定IPの割付

qiita.com coderwall.com

ミドルウェアクラスタリング

RabbitMQ

github.com

Riak

https://hub.docker.com/r/basho/riak-kv/

MySQL

github.com

終わり

誰かの役に立つことを願う。
recipe等の解説は、万が一誰かからリクエストがあれば追記する。

Spring AMQPでオレオレErrorHandlerを使ってみるテスト

@RabbitListeneを使っている時にデフォルトのErrorHandlerではなくて、独自処理を実装したErrorHandlerを使う方法を調べてみたので自分用メモ。

設計も汚いし、とにかく動くところまでしか確認してない。

package hello;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.ConditionalRejectingErrorHandler;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.ErrorHandler;

@SpringBootApplication
@EnableRabbit
public class Application implements RabbitListenerConfigurer {

    @Autowired
    ConnectionFactory connectionFactory;

    @Bean
    MessageListenerAdapter listenerAdapter(Receiver receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }

    @Bean
    public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() {
        DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
        factory.setMessageConverter(jackson2Converter());
        return factory;
    }

    @Bean
    public MappingJackson2MessageConverter jackson2Converter() {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        return converter;
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setErrorHandler(errorHandler());
        registrar.setContainerFactory(factory);
        registrar.setMessageHandlerMethodFactory(myHandlerMethodFactory());
    }

    public ErrorHandler errorHandler() {
        return new MyHandler();
    }

    @Slf4j
    static class MyHandler implements ErrorHandler {

        ErrorHandler errorHandler = new ConditionalRejectingErrorHandler();

        @Override
        public void handleError(Throwable t) {
            log.error("==do something for error==");
            errorHandler.handleError(t);
        }

    }

}

@Service
@Slf4j
class Receiver {

    @RabbitListener(queues = "spring-boot")
    public void receiveMessage(SampleDto dto) {
        log.info("Received <" + dto.toString() + ">");
    }

}

@Data
@NoArgsConstructor
@AllArgsConstructor
class SampleDto {

    private String key;
    private String value;
}

build.gradleはこんな感じ

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.4.0.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'

jar {
    baseName = 'messaging-rabbitmq-oreore'
    version =  '0.1.0'
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile("org.springframework.boot:spring-boot-starter-amqp")
    compile group: 'org.projectlombok', name: 'lombok', version: '1.16.10'

    testCompile("junit:junit")
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.3'
}

ext.mainClass  = 'hello.Application'

あとテストように作ったpublisherも貼っておく

package hello;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitMessagingTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.stereotype.Service;

@SpringBootApplication
public class Application implements CommandLineRunner {

    public static void main(String[] args) {
        SpringApplication.exit(SpringApplication.run(Application.class, args));
    }

    @Bean
    Queue queue() {
        return new Queue("spring-boot", true);
    }

    @Bean
    DirectExchange exchange() {
        return new DirectExchange("direct");
    }

    @Bean
    Binding binding(Queue queue, DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).withQueueName();
    }

    @Bean
    public MappingJackson2MessageConverter jackson2Converter() {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        return converter;
    }

    @Autowired
    private Sender sender;

    @Override
    public void run(String... args) throws Exception {
        for (String i : args) {
            ObjectMapper mapper = new ObjectMapper();
            SampleDto dto = null;
            try {
                dto = mapper.readValue(i, SampleDto.class);
                sender.sendToRabbitmqAsJson(dto);
            } catch (Exception ex) {
                sender.sendToRabbitmqAsString(i);
            }
        }

    }
}

@Service
class Sender {

    @Autowired
    private RabbitMessagingTemplate rabbitMessagingTemplateAsJson;

    @Autowired
    private RabbitMessagingTemplate rabbitMessagingTemplateAsString;

    @Autowired
    private MappingJackson2MessageConverter mappingJackson2MessageConverter;

    public void sendToRabbitmqAsJson(final SampleDto dto) {

        this.rabbitMessagingTemplateAsJson.setMessageConverter(this.mappingJackson2MessageConverter);

        this.rabbitMessagingTemplateAsJson.convertAndSend("direct", "spring-boot", dto);

    }

    public void sendToRabbitmqAsString(final String message) {

        this.rabbitMessagingTemplateAsString.convertAndSend("direct", "spring-boot", message);

    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
class SampleDto {

    private String key;
    private String value;
}

rasbperry pi3でNAS構築(とついでにbonding)

ご無沙汰してます。

サーバの勉強にもなるかなと思ってraspberry pi3を手に入れて自作(かつ自宅)NASサーバを構築してみました!

このraspberry pi3なんですけど、無線LAN付きになったんですよ。

USBドングル買わなくても無線化できるので少し財布に優しいー!

さて、そのNASサーバの構築ですがざっくりこんな流れです。

  1. raspberry piをSSHできるようにする
  2. USB HDDをマウントする
  3. smbインストールしてNASにする
  4. 有線LAN+無線LANのbondingの設定をする

この中の(1)〜(3)の手順は検索すればraspberry pi2のものですが情報がザクザク(でもないかもしれないけど)出てくるのでは書くのはやめるとして、、、

(4)の『bondingの設定をする』に絞って手順をご紹介します。

bondingの設定自体はraspberry pi bondingで検索すると、 以下のページが出てくるので、主にこの2つのページを参考に設定しました。

補助記憶: Raspberry PiでUSB-Wifiアダプタを使う

d.hatena.ne.jp

しかーし、紹介されたとおりに設定してみても紹介されている通りに動作しない… どう、動作しないかというと

  1. 上記のページで書かれている通り、bond0にだけIPが振られるわけじゃなかった(eth0とwlan0にもIP振られちゃうけど、無視して設定進めたらbondingは動作した)
  2. LANケーブルさしてないとsshが繋がらない
  3. LANケーブル挿すとping返ってくるけど通信速度が変わらない

という具合です。

tcpdump見たわけじゃないんですが、有線LAN抜くと動作しないってことは、 bonding用のインターフェース(bond0)に割り付けたIPに対するリクエストの返りが有線LANのインターフェース(eth0)から出ていってるんじゃないかと推測して ルーティングテーブル周りの設定変更を試してみたら、うまく動作した!っていうメモです。

たぶん、正しい手順の解決策ではないと思いますが、自分用のメモ(と、他に困ってる人の参考)に参考になったページのリンクを残しておきます。

似たような現象を探してて見つけたページがこちら。

ja.stackoverflow.com

このページを見て、ルーティングテーブルか何かがおかしいのでは、と気づき ip ruleで検索してでてきた

d.hatena.ne.jp

このページに倣って設定したら、動作しました。

こんな感じ!

Before(といいながら有線LAN抜いた時の速度です。スクショ撮り忘れた…)

f:id:kogayushi:20160429151525p:plain

After

f:id:kogayushi:20160429151620p:plain

特定のクラスを継承したクラス一覧を出力する方法

掲題の通り、EclipseとかNetBeansの力を借りずにコードを書いてリストを出してみました。

guavaのClassPath使えばこんな感じで簡単に出せるようです。

package tester;

import com.google.common.reflect.ClassPath;

import java.io.IOException;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.apache.struts.action.Action;

/**
 *
 * @author yushi.koga
 */
public class Main {

    public static void main(String... args) {
        try {
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            Set<Class<?>> allClasses = ClassPath.from(loader)
                    .getTopLevelClassesRecursive("my.package").stream()
                    .map(info -> info.load())
                    .collect(Collectors.toSet());
            allClasses.stream().filter((actionClass) -> (Action.class.isAssignableFrom(actionClass))).forEach((actionClass) -> {
                System.out.println(actionClass.toString());
            });

        } catch (IOException ex) {
            Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

RTrim最速っぽいのを簡単に調べてみた

自宅の環境(CPU:Core2 Duo E6750 メモリ4G)ではStringUtils#stripEndが最速らしい。 しかし、会社のPCではguavaが最速でした。

検証したのは、

  • 処理時に(メソッド呼ばれる毎に)で毎回Patternのインスタンス生成からやる
  • Patternの変数をstaticで持たせてインスタンス生成のコストを減らしたもの
  • StringUtils#stripEnd
  • CharMatcher#trimTrailingFrom

のどれが一番早いか!です。

結果はこちら。

検証コード

package speedcheck;

import com.google.common.base.CharMatcher;

import org.apache.commons.lang.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 *
 * @author yushi.koga
 */
public class SpeedCheck {

    static Pattern staticPatternForRtrim = Pattern.compile("\\s+$");
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {

        String sampleText = "\"aaaaaa\"    ";
        int max = 25000000;

        String staticTrimedText = null;
        System.out.println("original text start from here ->" + "\"aaaaaa\"    " + "<-to end is here");
        System.out.println("loop times : " + max);
        System.out.println("=======================================");
        long staticStarted = System.currentTimeMillis();
        for (int i = 0; i < max; i++) {
            Matcher staticMatcher = staticPatternForRtrim.matcher(sampleText);
            staticTrimedText = staticMatcher.replaceAll("");
        }
        long staticFinished = System.currentTimeMillis();
        System.out.println("staticStarted      at " + staticStarted);
        System.out.println("staticfinished     at " + staticFinished);
        System.out.println("staticPassed time  is " + ((staticFinished - staticStarted)));
        System.out.println("staticTrimedText   is " + staticTrimedText + "<- end here");
        System.out.println("=======================================");


        String insideTrimedText = null;
        long insideStarted = System.currentTimeMillis();
        for (int i = 0; i < max; i++) {
            insideTrimedText = Pattern.compile("\\s+$").matcher(sampleText).replaceAll("");
        }
        long insideFinished = System.currentTimeMillis();
        System.out.println("insideStarted      at " + insideStarted);
        System.out.println("insidefinished     at " + insideFinished);
        System.out.println("insidePassed time  is " + ((insideFinished - insideStarted)));
        System.out.println("insideTrimedText   is " + insideTrimedText + "<- end here");
        System.out.println("=======================================");


        String commonsTrimedText = null;
        long commonsStarted = System.currentTimeMillis();
        for (int i = 0; i < max; i++) {
            commonsTrimedText = StringUtils.stripEnd(sampleText," ");
        }
        long commonsFinished = System.currentTimeMillis();
        System.out.println("commonsStarted     at " + commonsStarted);
        System.out.println("commonsFinished    at " + commonsFinished);
        System.out.println("commonsPassed time is " + ((commonsFinished - commonsStarted)));
        System.out.println("commonsTrimedText  is " + commonsTrimedText + "<- end here");
        System.out.println("=======================================");


        String charMatcherTrimedText = null;
        long charMatcherStarted = System.currentTimeMillis();
        for (int i = 0; i < max; i++) {
            charMatcherTrimedText = CharMatcher.is(' ').trimTrailingFrom(sampleText);
        }
        long charMatcherFinished = System.currentTimeMillis();
        System.out.println("quavaStarted       at " + charMatcherStarted);
        System.out.println("quavaFinished      at " + charMatcherFinished);
        System.out.println("quavaPassed time   is " + ((charMatcherFinished - charMatcherStarted)));
        System.out.println("quavaTrimedText    is " + charMatcherTrimedText + "<- end here");
        System.out.println("=======================================");
    }

}

結果

Executing: gradle :run
Arguments: [-PmainClass=speedcheck.SpeedCheck, -c, D:\workspaceForNetBeans\speedcheck\settings.gradle]

:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:run
original text start from here ->"aaaaaa"    <-to end is here
loop times : 25000000
=======================================
staticStarted      at 1448538448194
staticfinished     at 1448538458284
staticPassed time  is 10090
staticTrimedText   is "aaaaaa"<- end here
=======================================
insideStarted      at 1448538458285
insidefinished     at 1448538479871
insidePassed time  is 21586
insideTrimedText   is "aaaaaa"<- end here
=======================================
commonsStarted     at 1448538479871
commonsFinished    at 1448538480760
commonsPassed time is 889
commonsTrimedText  is "aaaaaa"<- end here
=======================================
quavaStarted       at 1448538480760
quavaFinished      at 1448538483150
quavaPassed time   is 2390
quavaTrimedText    is "aaaaaa"<- end here
=======================================

BUILD SUCCESSFUL

Total time: 35.862 secs