【第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に認識させていない場合、以下が不便です。
Docker on Centos7 on Vagrant with Chef
概要
掲題の通りのローカル開発環境を構築してみた。
コンセプト
目的
- RabbitMQやRiakはライブラリでノード指定時に、
config.addresses("192.168.33.10","192.168.33.11")
みたいな書き方をするので、それをローカル環境でやりたい - MySQLは更新系はMasterで、参照系はSlaveなんてケースあるだろうから、Master/Slave構成をローカル環境でやりたい
動かしてるミドルウェア
- Rabbit MQ
- Riak
- MySQL
何をやったのか
後述する参考サイトで、クラスタリングやら固定IPの割付やらは実現されている。
それぞれのdocker-compose.ymlをちょっと修正して、chefのrecipe化して、vagrant upだけで環境を構築できるようにしたところがミソ。
chefでdockerインストールしてdocker-compose upを実行してたりするところもやや難しかった。
成果物
参考にしたサイト
docker containerへの固定IPの割付
各ミドルウェアのクラスタリング
RabbitMQ
Riak
https://hub.docker.com/r/basho/riak-kv/
MySQL
終わり
誰かの役に立つことを願う。
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)〜(3)の手順は検索すればraspberry pi2のものですが情報がザクザク(でもないかもしれないけど)出てくるのでは書くのはやめるとして、、、
(4)の『bondingの設定をする』に絞って手順をご紹介します。
bondingの設定自体はraspberry pi bondingで検索すると、 以下のページが出てくるので、主にこの2つのページを参考に設定しました。
補助記憶: Raspberry PiでUSB-Wifiアダプタを使う
しかーし、紹介されたとおりに設定してみても紹介されている通りに動作しない… どう、動作しないかというと
- 上記のページで書かれている通り、bond0にだけIPが振られるわけじゃなかった(eth0とwlan0にもIP振られちゃうけど、無視して設定進めたらbondingは動作した)
- LANケーブルさしてないとsshが繋がらない
- LANケーブル挿すとping返ってくるけど通信速度が変わらない
という具合です。
tcpdump見たわけじゃないんですが、有線LAN抜くと動作しないってことは、 bonding用のインターフェース(bond0)に割り付けたIPに対するリクエストの返りが有線LANのインターフェース(eth0)から出ていってるんじゃないかと推測して ルーティングテーブル周りの設定変更を試してみたら、うまく動作した!っていうメモです。
たぶん、正しい手順の解決策ではないと思いますが、自分用のメモ(と、他に困ってる人の参考)に参考になったページのリンクを残しておきます。
似たような現象を探してて見つけたページがこちら。
このページを見て、ルーティングテーブルか何かがおかしいのでは、と気づき ip ruleで検索してでてきた
このページに倣って設定したら、動作しました。
こんな感じ!
Before(といいながら有線LAN抜いた時の速度です。スクショ撮り忘れた…)
After
特定のクラスを継承したクラス一覧を出力する方法
掲題の通り、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