私はなぜ今からKotlinを学ぶのか

それなりにJavaのことはわかってきた気がするので、よりよいコードを書けるようになるべくKotlinを学びはじめました

Kotlinに着目したきっかけは特に思い出せません。

better Javaと言われているからJavaより書きやすいんだろうな、くらいの印象からスタートだった気がします。

学び始めると魅力がどんどんわかってきて、学びがあるたびにリアルタイムにツイートしてました。

特に演算子オーバーロードは気に入りました。(他の言語にもありますが…

値オブジェクト同士の演算するときに四則演算子で表現できたらいいなぁってずっと思ってました。

たとえばコレクションオブジェクト同士のマージを.addAllとかではなくlist + listで表現したいです。

Kotlinならこれが実現できそうです。

whenもいい感じに使えそうです。

仕様を知ったその瞬間はこんな感想でしたが、用途を考えていたらすぐ閃きました。

OOPなコードを心がけていると、バリエーションが生まれるところでStrategy/Policy/Stateパターンを使ったりしますが、そのクラスをディスパッチするcreation methodを作る必要もあります。 Kotlinならこのcreation methodをいい感じにかけそうです。

という感じでまだKotlin In Actionを読んでいる途中なのですが、本の数章読んだだけでコードがきれいに描けそうなイメージが湧いてきました。 引き続き、学んでいきます( ・`д・´)

言語やFWなどの選定基準

みなさんは言語やFWなどの選定基準について考えたことがあるでしょうか?

私は使いたいと思ったFWなどを採用するかどうかを考える場合に、以下を基準にしています。

  • プロダクションで利用されている事例が多い
    • バグが改修される速度が早い
    • あるいは未対応でもissueに報告されているので、調査コストが低くなる
  • 枯れてきている(枯れている)
    • バグが改修されている確率が高い
    • 未改修でも回避策などのノウハウが溜まっている
  • コミュニティが活発
    • 上記の2点の情報が流通している
    • またそれだけ関心を持った人が多いので、多機能化しサポートも手厚くなる傾向が強い

しかし、上記の観点でダメだったとしても、目的を達成するために他の手段がなければ採用することもあります。

文字にすると当たり前な考え方のように見えますが、言語やFWがどうやって選定されているか考えたことはあまりないのではないでしょうか?

何でテックリードまたはアーキテクトはこれを選んだんだ!と不満に思うことなら多々あると思いますが。。。(私もありました笑

テックリードの気持ちになって、選ばれた技術がどんな課題(Why)があってどんな問題(What)をどうやって(How)解決してくれているのかを考えてみるとスキルアップにつながる気づきが得られるかもしれません

話が少しずれてしまいますが、上記のWhat/How/Whyはサイモン・シネックのゴールデンサークル理論というものです。

とても勉強になるのでぜひ観てほしいです。

単純に話として面白いです。あと聞き取りやすい英語なので勉強にも使えます。

Spring Security 5.xでログイン失敗時にmessages.propertiesで上書きしているにもかかわらず常に"Bad credentials"が表示されてしまう

messages.propertiesで、メッセージを上書きしているにも関わらず、デフォルトのBad credentialsというメッセージが表示されてしまう不具合に遭遇しました。

どうも、AbstractUserDetailsAuthenticationProviderにinjectionされているMessageSourceが不正なようです。

AbstractUserDetailsAuthenticationProviderの継承クラスを自作して、それをProviderManagerにセットして…なんて自分で色々組み立てているときに再現するようです。

対策として、こんな感じでAbstractUserDetailsAuthenticationProviderの継承クラスに自分でMessageSourceをセットしてあげると直りました。

    MyConcreteAuthenticationProvider athenticationProvider() {
        MyConcreteAuthenticationProvider provider = new MyConcreteAuthenticationProvider();
        provider.setUserDetailsService(this.userDetailsService());
        provider.setPasswordEncoder(passwordEncoder);
        provider.setMessageSource(messageSource); // <= ここがポイント
        return provider;
    }

ひしだまさんのサイトの対応策とこちらで、似たり寄ったりかもしれませんが、こういう解決策もあるよ、ということでせっかく見つけたのでメモを残しておきます。

昔作ったOOPなサンプルコードが出てきたからブログに残しておく

大昔に作ったOOPなコードの紹介記事を世に出していなかったので、恥ずかしいけど記録として公開しておきます。

今書けばもっと洗練できると思います(けど管理職になってから多忙で、その時間がないです…)。

オブジェクト指向プログラミングのサンプルコードご紹介

OOPらしく書くとコードはどう変わるか〜

題材

パスワードポリシー

Goal

以下の例を知って、OOPをやりたくなってもらう

  • 複雑さを抑え込んだ変更に強いコード
  • OOPらしく書くとコードが読みやすくなる
  • 手続き型で作ると複雑さを抑え込めず読み難い

Story

  • サービスクラスをターゲットに手続き型で書いたコードとOOPなコードの複雑さの違いを測る
  • ありそうな仕様変更を書いてみて違いを知る
    • その結果からOOPの効果の程を知る

具体例

要求仕様(ユーザストーリー)

  • パスワード変更
    1. パスワードポリシーを満たせていたらパスワードを変更する
    2. 満たせていない場合はエラーを返す

パスワードポリシー

  1. パスワードの長さは8文字以上20文字以内
  2. 英数字大文字小文字を最低1回使用する
  3. ユーザ名と一致しない
  4. 現在のパスワードと一致しない
  5. 氏名(アルファベット)を含まない
    • 大文字小文字は区別しない
  6. メールアドレスと一致しない
  7. 電話番号と一致しない

サンプルコード

https://github.com/kogayushi/ist-oop-refactoring-password-policy

Procedural

Production Code
public void changePassword(String id,String newPassword){
    User user = this.userRepository.userFromId(UUID.fromString(id));
    if (newPassword.length() < 8 || newPassword.length() > 20) {
        String msg = "inputted password violated password length policy";
        throw new ViolatedPasswordPolicyException(msg);
    } // omitted the middle, coz it's too long.
    if (INCLUDING_UPPER_CASE_ALPHABET_AT_LEAST_ONE.matcher(newPassword).find() == false) {
        String msg = "inputted password violated character policy";
        throw new ViolatedPasswordPolicyException(msg);
    }
    this.userRepository.updatePassword(user.getId(), new Password(newPassword));
}
特徴
  • ポリシーを満たすかチェックするif文が10個以上並んでいる(例では略した)
  • ポリシーにルールが増えるたびにif文が増える
  • つまり、パスワード変更というユーザストーリーの中にパスワードポリシーの詳細が漏れている

OOP

Production Code
public void changePassword(ChangePasswordCommand command) {

    UUID id = command.getId();
    User user = this.userRepository.userFromId(id);

    PasswordPolicy policy = this.policyFactory.generatePasswordPolicyFor(user);
    Password password = new Password(command.getNewPassword());

    this.policy.validate(password);

    this.userRepository.updatePassword(id, password);
}
特徴
  • 分岐(if文等)がない
  • パスワードポリシーの詳細をサービスクラスが知らない(知らなくて済む)
  • ポリシーのみを切り離して単体テストが可能

サンプルコードの複雑さを観測してみる

  • Cyclotic Complexityについては後述
Procedure/OOP Cyclotic Complexity
Procedure 13
OOP 1
  • 手続き型も13なら、悪くはない
  • OOPは1と最小値

What's cyclomatic complexity

  • コードの複雑さを表す指標
  • if文などで分岐が増えるとポイントが1増える
  • if文やswitchがなければcyclomatic complexityは1(最小値)
  • 分岐がなくてもメソッドが2つ生えてたら最低で2
  • 参考URL(不正確らしいがわかりやすいので紹介)
目安
循環的
複雑度
複雑さの状態 バグ
混入確率
10以下 非常に良い構造 25%
30以上 構造的なリスクあり 40%
50以上 テスト不可能 70%
75以上 いかなる変更も誤修正を生む 98%

サンプルコードの複雑さを観測してみる(再掲)

Procedure/OOP Cyclotic Complexity
Procedure 13
OOP 1
  • 現時点ではどちらも悪くはない
  • OOPで書いたほうが指標で判断すれば優れていると言える

仕様変更 is coming !

その1:ユーザ名も変更可能にしたい

  • パスワードと同じポリシーを適用したい

Procedural

修正ポイント
  1. changePasswordをコピペしてchangeUsernameを作成
  2. changePasswordchangeUsernameの重複を排除
Production Code
public void changeUsername(User user,String newUsername) {
    User user = this.userRepository.userFromId(UUID.fromString(id));
    this.validateCommonPolicy(user,  newPassword, "password");
    this.userRepository.updatePassword(user.getId(), new Password(newPassword));
}
private void validateCommonPolicy(User user, String newAuthenticationFactor, String name) {
    if (newAuthenticationFactor.length() < 8 || newAuthenticationFactor.length() > 20) {
        log.warn("inputted password violated password length policy");
        throw new ViolatedPasswordPolicyException(msg);
    } // omitted, a lot.
}

OOP

修正ポイント
  1. PasswordPolicyをコピペしてUsernamePolicyを作成
  2. generatePasswordPolicyForをコピペしてgenerateUsernamePolicyForを作成
  3. changePasswordをコピペしてchangeUsernameを作成
  4. PasswordPolicyUsernamePoilcyの重複を排除
Production Code
public void changeUsername(ChangeUsernameCommand command) {
    UUID id = command.getId();
    User user = this.userRepository.userFromId(id);

    UsernamePolicy policy = this.policyFactory .generateUsernamePolicyFor(user);
    Username username = new Username(command.getNewUsername());
    this.policy.validate(username);
    this.userRepository.updateUsername(id, username);
}

その2:ユーザ名の文字長の制限は
4文字以上10文字以内にしたい

Procedure

修正ポイント
  • 文字長チェックのif分をvalidateCommonPolicyからchangePasswordとchangeUsernameにコピー
  • usernameの文字長下限のチェックを8から4に変更
Production Code
public void changeUsername(String id, String newUsername) {
    // omitted
    if (newUsername.length() < 4 || newUsername.length() > 20) {
        String msg = "inputted uername violated username length policy";
        log.warn(msg); // it's warn just for testing.
        throw new ViolatedPolicyException(msg);
    }
    this.validateCommonPolicy(newUsername, user, "username");
    this.userRepository.updateUsername(user.getId(), new Username(newUsername));
}

OOP

修正ポイント
  • LengthPolicyをcommonPolicyから両方にコピペ
  • usernameの方だけ文字長を8から4に変更
Production Code
public PasswordPolicy generatePasswordPolicyFor(User user) {
    Set<Policy> policies = generateCommonPolicy(user);
    policies.add(new LengthPolicy(8, 20));

    return new PasswordPolicy(policies);
}

public UsernamePolicy generateUsernamePolicyFor(User user) {
    Set<Policy> policies = generateCommonPolicy(user);
    policies.add(new LengthPolicy(4, 20));

    return new UsernamePolicy(policies);
}

その3:ユーザ名はメールアドレスとの重複可

Procedure

修正ポイント
  • メールアドレスとの一致チェックのif文をvalidateCommonPolicyからchangePasswordに移動
Production Code
public void changePassword(String id, String newPassword) {
    // omitted
    if (newPassword.length() < 8 || newPassword.length() > 20) {
        String msg = "inputted password violated password length policy";
        log.warn(msg); // it's warn just for testing.
        throw new ViolatedPolicyException(msg);
    }
    if (newPassword.equals(user.getPerson().getContactInformation().getMailAddress().getValue())) {
        String msg = "inputted password violated not same with mail address policy";
        log.warn(msg);
        throw new ViolatedPolicyException(msg);
    } // omitted, a lot.
}

OOP

修正ポイント
  • NotSameWithMailAddressPolicyをgenerateCommonPolicyから移動
Production Code
public PasswordPolicy generatePasswordPolicyFor(User user) {
    Set<Policy> policies = generateCommonPolicy(user);

    policies.add(new LengthPolicy(8, 20));
    policies.add(new NotSameWithMailAddressPolicy(user.getPerson().getContactInformation().getMailAddress()));

    return new PasswordPolicy(policies);
}

まとめ

手続き型を振り返ると

  • 共通ロジックをメソッドにくくりだすことで重複が排除できた
    • しかし、確かに重複は排除出来たが複雑度が減ったわけではない
  • ユーザ名とパスワードのポリシーの違いが増えるたびに複雑度は増える

OOPらしく書くと何が違うか?

  • ユーザ名を変更するメソッドが増えても複雑度は1しかあがらない
  • パスワードポリシーのルールが変わったとしてもサービスクラス(ビジネスロジック)に影響がない
  • 1つ1つの処理(今回はポリシー)に名前がつくので全体的に読みやすい
  • サービスクラスがシンプル(分岐がない)になる

今回は利用側のクラスに着目したが…

  • 利用される側であるPolicyやFactoryについても仕様変更に影響してcyclomatic complexityのポイントがあがることはない
  • cyclomatic complexityはあくまで指標
    • 肝心なのは、業務ルールがカプセル化されることにより利用側のクラスがその詳細を知らずに済むこと
    • 業務の複雑さはなくせない。しかし、狭い範囲に閉じ込めることで変更に強くなる

例えば今回は

  • ポリシーをオブジェクトにしたことで
    • パスワードリセット機能など、パスワードを変更する他の機能でも利用できる(再利用性)
    • メールアドレスのアカウント部と一致してはいけない、などのポリシーの変更に強い(変更容易性)

Let's do the OOP !

マイクロサービスアーキテクチャで要求される各機能にSpringのどのフレームワークが対応するかの一覧

  • Health Check

    • Spring Boot Actuator
      • Helth check用エンドポイントあり(拡張を推奨します)
  • Fault Tolerance

    • Spring Cloud Hystrix x Spring Cloud RIbbonの組み合わせ(だと思う)
  • Open API

  • Metrics

    • Spring Boot Actuator
      • メトリクス収集用のエンドポイントあり(拡張可)
  • Rest Client

    • Spring Cloud Feigh
  • Config

    • propertiesの値を読み込んでクラスや変数にinjectionすることはSpringの標準機能でできます
    • また、propertiesをサーバに配信させつつ値が更新されたらそれを検知してホットリロードする機能もあります(Spring Cloud Config)
    • 私が趣味で書いたSpring Cloud Configのサンプルコードです ➝ https://github.com/kogayushi/spring-cloud-config-sample
  • Open Tracing(分散トレーシングのことだと思います)

前のブログでも触れたとおり、EOLなので移行しなきゃなんですけどね…

Spring Cloud Configを調べて検証コードを書いてみた

Spring Cloud Configを調べて検証コードを書いてみました。サンプルプロジェクトはこちら。

https://github.com/kogayushi/spring-cloud-config-sample

README.MDに関しては未来の自分のためのメモのため、ブログを見に来た方には意味がわからないかもしれません。

調べた結果として、以下のことができることがわかりました。

思い出しながらランダムな順番で記載します。

  1. Config Serverから設定ファイルを配信する
  2. 任意のタイミングで最新の設定をhot reloadできる
  3. logback.xml等のファイルも配信できる
  4. Config ServerをSpring Securityを使ってBASIC認証で保護できる
  5. Config ServerからConfig Clientに任意のタイミングで設定を配信できる
  6. DB接続情報など、秘匿が必要な情報は暗号化することができる(ただし、運用に乗せるにはかなり工夫が必要)
  7. profile毎に設定ファイルを分けることが出来る
  8. プロジェクトをまたいで共通設定を指定できる(application.ymlを書くだけ)

稼働中に設定をリロードした場合に問題なく動くかがまだ心配なので、その点について引き続き検証してみます。

日本語だと情報が少ないですが英語だとそれなりに出てきます。まぁ、海外では普通に使われてるのできっと本番で使っても大丈夫だろうとは思っています。

本番投入できそうなくらいには理解度が進んだので、実際の案件でホットリロードが出来ると嬉しい機能が出てきたら、提案してみます。

とかいろいろ調べたんだけど、Spring CloudってEOLになったんだよなぁ…○r2

ディスパッチテーブルというデザインパターン

ディスパッチテーブルというデザインパターンを紹介します。 https://qiita.com/hirokidaichi/items/c9a76191216f3cc6c4b2#%E3%83%87%E3%82%A3%E3%82%B9%E3%83%91%E3%83%83%E3%83%81%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB

これを覚えれば、if文やswitch文を書かずに条件分岐を表現できるのでコードの見通しを良くすることができます。

現場で役立つシステム設計の原則という本の2章で紹介されているMap(連想配列)やenum(列挙型)を使った例がまさにこれに該当します。

これはポリモーフィズムととても相性が良く、具体的なデザパタではPolicy/SpecificatioinやStrategy/Stateパターンとの相性が良いです。

ある条件を引数に与えて、その条件に沿ったインスタンスを取得するという性質は(広義の)Factoryパターンに近いです。

Factoryパターンを使えば『ある条件に該当するクラスのインスタンスを得る』という部分は同じように実現できます。

しかし、Springを使ったアプリケーションの場合そうもいきません。

AOPで提供される様々な便利機能を利用するためにどうしてもDIコンテナからインスタンスを取得する必要性があるためです。

Factoryメソッド内で新規にインスタンスをnewしてしまうとDIコンテナの管理対象外のためAOPが動作しないので@Transactionalなどの便利なアノテーションが使えなくなってしまいます。

Factoryパターンを使う場合、FactoryメソッドがDIコンテナから取得したインスタンスを返せればよいのですが、一般的にはfactoryメソッドはstaticであることが多く、staticな場合だと特別な工夫をしないとDIコンテナから取得したインスタンスを返させることはできませんし、出来たとしても直感に反します。

IDDD本で紹介されているDomainRegistryパターンを使えばstaticメソッド経由ではDIコンテナ内のBeanを取得できないという問題も克服できますが、 世の中に広く浸透しているデザパタではないところが少し利用を躊躇わせます。

単純にあるクラスのインスタンスのバリエーションを全てinjectionしてしまうとメソッド内でif文かswitch文でどのインスタンスを使うのか選択する必要がありますが、ディスパッチテーブルを使えば、それをせずに済みます。

ちなみにこれもデザインパターンの一種だと認識していますが、名前が有名ではないようでググっても全然サンプルコードが出てきません。 名前がついていても良さそうなもんですけどねぇ。