Yabu.log

ITなどの雑記

Java読書会「現場で役立つシステム設計の原則」を読む会 第1回に参加

本の概要

投票の結果こちらの本になりました。 ペース的に4回くらいになりそうです。 前回の投票でもKotlinの本に続き2位だったそうです。かなり人気な本だと思います。

正誤表:

サポートページ:現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法:|技術評論社

賛否

賛否のある本だと思いますが、思っていたより批判が激しい感じでした。

というのも「現場で役立つシステム設計の原則」というタイトルですが、現場として次のものが暗黙的に想定されていると思います

  • WEBシステムである
  • モダンなJavaでSpringのようなフレームワークが使える
  • バックエンドはRDBを使う
  • 国内向けのシステムで内製ができる(アジャイルが可能な現場)
    • 超大規模ではなく、国際化対応などはしなくて良い

著者が想定しきれていないほど多様な「現場」の方が参加されているので、業界が違う技術者に「原則」として著者の経験や考えが読み上げられてしまうと異論が湧き出るだろうなという印象です。

全体的にこのアマゾンレビューのスタンスな方が多い印象です。

この業界に溢れた、「オレのやり方がナンバーワンだ」の本の域からは出ていないように思います。「変更が容易」の判断基準や根拠が極めて定性的なまま、著者の考えにそぐわない既存の設計手法を「誤り」と断じる論調にはだんだん首をかしげてしまいました。 著者は『ドメインモデルのコードの中に、すべてのビジネスルールを凝集させてしまいたい』派なのだと思います。オブジェクトを第一に考えている、という点で確かにオブジェクト指向には違いないです。が、DBやUIとのミスマッチをどう埋めるか?という話に関しては論理に雑な印象を受けました。ストアドやリッチクライアントなど、ビジネスロジックを散在させる要素はそこかしこにあるわけで、「あくまでドメインモデルのコード内に閉じ込めるのが正しく、そうできないのはやりかたが悪いから」なスタンスは、現実から目をそらした排他主義に見えます。著者が「正しい」とする方法が、果たして本当にどこまで経済的にメリットのあるやり方なのかは疑問に感じました。 本書は「こういう考え方もあるのか」の視点で読める人には良書と思います。しかしながら、初心者が最初に読むには全くお勧めしません。著者の考えに強く賛同する人であったとしても、この本に「正しい」と書かれていることを妄信すべきではない、と思いました。

https://www.amazon.co.jp/gp/customer-reviews/R1SZREL1NO1RXE/ref=cm_cr_dp_d_rvw_ttl?ie=UTF8&ASIN=B073GSDBGT

一方的に批判がでる箇所もあり、主催の高橋さんがバランスを取って何度かフォローされていますが、DDD界隈の方が参加されていい感じの反論が聞けたら面白いのになと思っています。

感想

オブジェクト指向Javaを極めるとDDDに行き着くと思っていましたが、そうでも無いようです。実装が好きな人、モデルが好きな人、ドメインが好きな人、という感じに(もっと多いかも)枝分かれするようです。

ベテランの方が多いので、全員「ドメイン駆動設計」のファンだと思い込んで参加したのですが、そういう方がいなかったので衝撃的でした。*1

私自身はDDDの「プログラミングの対象領域を大事にする」という考え方は結構好きです。設計はあまりわかっていないので、勉強になるところも多かったです。

以下読書会ノートです。

オブジェクト指向とは何か

設計とプログラムどっちのことを言っているのかわからない

モデルとプログラムは一致するか?

  • 最近のシステム開発は紙ベースの業務を置き換えるだけではない
  • まだ世の中にないビジネス。どうやってITで作る?

文字数制限の話

文字数たくさん使ってわかりやすい単語で作れ的な本だが、 長い命名を使うことにも問題がある

ShipingCostの設計について

class ShippingCost {
    static final int minimumForFree = 3000;
    static final int cost = 500;
    int basePrice ;

    //メソッドの実装略
    ShippingCost(int basePrice);
    int amount();
}

int shippingCost(int basePrice) {
    ShippingCost cost = new ShippingCost(basePrice);
    return cost.amount() ;
}
  • 重複をなくすだけならstatic methodにくくり出すだけで良い
  • このクラス設計はいけてない。最近だとロジックとしてラムダ式とかを渡す?

計算の対象になる値が異なれば、別のオブジェクトに分けるのがオブジェクト指向らしい設計です

  • この意見は局面による
  • 直接newしたものを使うべきではない。(interfaceを使う例が後に出てくる)
    • 結果を知りたい側(amountが欲しい呼び出し側)がbasePriceを知らない場合は、こんな設計もあり
  • ライフサイクルが長いから送料クラスが意味を持つ
  • 好意的に解釈するとプライムな変数ではなく、独自定義型を使うというのはOK
  • intは使うな
    • int型の最大値は21億だが、その規模の額を扱わないシステムでもカードなどで端数の扱いの違いがあるのでintは危険
  • 国境を超えるシステムでは送料計算はめちゃくちゃ大変。
  • エリックエヴァンスの本に貨物を輸送する例が出てくる
    • DDD界隈の人達は貨物を輸送する例を好む傾向がある
  • 著者はDDD界隈の人
    • このへんからドメイン駆動設計への誘いを感じる。

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

業務構造とプログラムを一致させる

業務の構造とプログラムの構造が一致していれば

int itemPrice = (basePrice + shippingCost) * taxRate();
  • 例えば上記は軽減税率などが入ると、税金計算の際に、料金だけでなく、明細が必要になってくる
    • そうなると破綻する
  • 一致させるのは大変難しい

DBとタイムゾーン

ロケールを明記していないと、タイムゾーン情報が消える(必ずUTCになるわけでない) この辺はサマータイム対応が難しい、ということになる。

可視性について

class Customers {
    List<Customer> customers;

    ...

    List<Customer> getList() {
        return customers;
    }
}
  • List<Customer> customers;はprivateスコープにすべきでは?とはという話が出た
  • DDDをする上で可視性などは些細な問題なのでは?

Listへの参照をそのまま外部に渡すと、要素の追加や削除がCustomersクラスの外部からできてしまいます。`

  • だとしてもこのような前提があるならば、可視性はこだわってサンプルコードを作成するべき
  • 可能な限りpackageスコープにする。という記述がある。その方針に沿っているのでは?

“関連性の強いクラスは同じパッケージに集めます。そして、クラスやメソッドのスコープ(参照範囲)は、可能な限りパッケージスコープにします。”

  • デフォルトで可視性をpublicにする風潮は如何なものかと思う*2
  • Java9で入ったモジュールは難しい
    • それだけを扱っている本が3冊くらい出ている

この著者は防衛プログラミング反対派

まだこの章に到達していませんが、懇親会などで結構話題に上がりました。

Javaの互換性

コレクションオブジェクト

class Customers {
    List<Customer> customers;

    ...

    Customers add(Customer customer) {
        List<Customer> result = new ArrayList<>(customers);
        result.add(customer);
        return new Customers(result);
        //return new Customers(result.add(customer));👈誤植間違い

    }
}
  • コピー先のCustomersでも参照先のcustomerオブジェクトは変更できるがいいのか?(シャローコピー、ディープコピーの話)
  • ここまでの文脈的にCustomerはimmutableなオブジェクトなのでは?
  • 少し後ろに説明が書いてある

“unmodifiableなListでも、個々の要素のオブジェクトの内容は変更できます。コレクションの要素を値オブジェクトにすればこの変更も防げます。”

  • コンストラクタの説明がない
  • list行間の下の...に書かれているのでは

Listオブジェクトのファクトリメソッドについて

  • List.of() 変更不可能なクラスを返す。可変長引数のみ。コレクションを引数を取ることはできない
  • Arrays.aslist() 固定サイズのlistを返す
  • Collections.unmodifiableList 変更不可能なクラスを返す
  • Collectors.toList() ストリームとかで使う?

技術者の人種

  • モデル屋さん

    • モデルを作るのが目標
    • 自動生成とかやりたがる
    • 別の設計関係の本の読書会で意見が合わないことがあった
  • ドメイン屋さん

  • 実装屋さん
    • この読書会の大半の人?

https://ja.wikipedia.org/wiki/モデル駆動型アーキテクチャ

  • プログラマ不要説は度々流行るが、結局流行らない
    • if文やfor文を違う表現で出してるだけ。
    • caseツール

※モデル屋さんとドメイン屋さんが来ておらず、偏った議論になっているのでは?と感じた。

インスタンスを返すメソッドの意味は?

以下のコード。なんの意味があるのか?という質問が出た

class Customers {
    List<Customer> customers;

    ...
    //👇これ
    Customers add(Customer customer) {
        List<Customer> result = new ArrayList<>(customers);
        return new Customers(result.add(customer));
    }
}
  • メソッドチェーンができる
  • 標準ライブラリのimmutableなオブジェクトも大体この仕様

サブストリングにおけるStringPoolの扱い

最近はやってない

Strng.Classのソースを確認したところ、確かに

  • substring(int beginIndex, int endIndex)
  • StringLatin1.newString(value, beginIndex, subLen)
    • new String(Arrays.copyOfRange(val, index, index + len), LATIN1)

こんな感じで新たなインスタンスが作られるようになっていました。

public String substring(int beginIndex, int endIndex) {
    int length = length();
    checkBoundsBeginEnd(beginIndex, endIndex, length);
    int subLen = endIndex - beginIndex;
    if (beginIndex == 0 && endIndex == length) {
        return this;
    }
    return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen)
                      : StringUTF16.newString(value, beginIndex, subLen);
}
  • サブストリング側の生で親がGCされずに残る負荷が多い。
    • 親が消えても子供が残っているから親が消えない。(GC対象にならない)

なぜラッパークラスを使っているのか

  • Booleanではなくbooleanを使うべき
private Boolean isChild() {
    return customerType.equals("child");
}
  • Object.equalsはboolean型を返すが?
    • 無駄なBooleanクラスを使ってる
  • Booleanを使っている意図がわからない
  • 執筆にワードを使って書いてる説
  • customerTypeの値はメソッド内に持たなくて良いのか?
    • ちょっと変えてストラテジーパターンに落ち着くいつものパターンでは?

Reservationクラスは不変クラスにしなくていいのか

  • addFeeでオブジェクトの状態を変えられるけどそれはいいのか?
class Reservation {
    List<Fee> fees; //大人と子供の内訳は不明

    Reservation() {
        fees = new ArrayList<Fee>()
    }

    void addFee(Fee fee) { //大人でも子供でも追加できる
        fees.add(fee);
    }

    Yen feeTotal() {
        Yen total = new Yen() // 合計ゼロ円
        for( Fee each : fees ) {
            total.add( each.yen() );
        }
        return total;
    }
}
  • 現実的には徹底的にやる必要はない。どこかで線引きが必要
  • 徹底的にやると目指せhaskellになってしまう。

enum

  • enumは積極的に進めるべき
  • enumにメソッドを定義できるのはJavaの良い点
  • enumのメンバは大文字にすべきだが?
    • 小文字でMapにアクセスしたいので発想はわかる
enum FeeType {
    adult( new AdultFee() ),
    child( new ChildFee() ),
    senior( new SeniorFee() );

    private  Fee fee;
    // Feeインターフェースを実装したどれかのクラスのオブジェクト

    private FeeType(Fee fee) {
        this.fee = fee; // 料金区分ごとのオブジェクトを設定する
    }

    Yen yen() {
        return fee.yen();
    }

    String label() {
        return fee.label();
    }
}
  • enumをDBで管理したいという要望があると、このような設計はできない

型引数

Set nextStates = EnumSet.of(承認済,差し戻し中);
                                 // 状態のグルーピング
  • Setの宣言時に型引数を使うべき

日本語の定義名

  • Enumの定義名が日本語でわかりやすくて良い
enum State {
    審査中,
    承認済,
    実施中,
    終了,
    差し戻し中,
    中断中
}
  • Javaだとファイルにした時に問題が怒る
  • 「が」とかはmacOSでへんなバグりかたする
  • クラス名には使わない方が良い。
    • 日本語を使うのはメソッド名にまでに止めること

EnumMapを使うべきか?

class StateTransitions {
    Map<State, Set<State>> allowed;

    {
        allowed = new HashMap<>();

        allowed.put(審査中, EnumSet.of(承認済, 差し戻し中));
        allowed.put(差し戻し中, EnumSet.of(審査中, 終了));
        allowed.put(承認済, EnumSet.of(実施中, 終了));
        allowed.put(実施中, EnumSet.of(中断中, 終了));
        allowed.put(中断中, EnumSet.of(実施中, 終了));
    }

    boolean canTransit(State from, State to) {
        Set<State> allowedStates = allowed.get(from);
        return allowedStates.contains(to);
    }
}
  • HashMapではなく、EnumMapを使うべき
  • 「終了」をセットしていないので、終了で呼ぶとぬるぽで落ちる
    • バグでは?
    • EnumMapを使った場合もぬるぽが発生する
  • EnumMapを使うメリットは?
    • コンパイル時にKey側の要素数がわかるので、メモリ効率が良い
    • (パフォーマンス以外の)機能的な利点は特になさそう?

データクラスと機能クラス

UtilとかCommonな便利クラスを作るなという恒例のアレ

  • 悪い共通クラス
    • RXJavaが共通ライブラリの最たる例。
    • メソッドが多すぎる。それでも使うけど。

Javaのルーツ(ちょっとざわついた)

Javaは、C言語からの移行しやすさを重視して設計されました。

オブジェクト指向設計の基本

ロジックの置き場所やクラス名/メソッド名の改善を続け、より良い設計を見つけていくのが、オブジェクト指向設計の基本です。

凝集度

“このように、関連性の強いデータとロジックだけを集めたクラスを凝集度が高いと言います。”

  • モジュール設計のことを書いて凝集度を説明するのは、少し説明不足な気がする
    • そもそも凝集度なんかは本を読んでもよくわからない。
      • 実際に手を動かして作ってみるべき。

データと処理を同じクラスに置く(分離させない)

“データとロジックを一体にして業務ロジックを整理する”

  • この話はWEBアプリが流行り始めた頃からかなり議論されている
  • lombokhibernateのが許容できない感じの思想
    • hibernateのデータクラスもロジックをぶら下げることは可能

時間の流れ?

時間軸に沿った業務の基本の流れを軸に、業務ロジックの関係を整理します。

  • この手法は初めて聞いた
    • 注文と出荷は非同期。同期する必要はない(溜め込めばいい)

パッケージ間の参照

図3-3

出荷する時に顧客は見ないのか?

  • has a関係のようなものでは?
  • クラス単位の参照関係のようなものでは?
  • パッケージの関連では?

  • モジュラー屋さんはよくこのようなUMLを書く

    • 矢印がないからと言って、本当に参照しないという意味ではないのではないか。

技術選定などのフェーズが抜けている

  • Spring,Javaありきで技術選定などのフェーズが抜けている

    • グランドデザインができてしまっている
    • どうしてもWEBアプリケーションで成り立つ理論になる
      • 大企業の調達システムなどでは通用しない考え方
  • 動き(設計)を想定しながら分析する

    • 最初に習ったオブジェクト思考設計は確かにこんな感じ
  • 分析と設計と実装を一人の人間がやるのは小規模でないと難しい

  • この本は実装者に結構寄り添っている

  • モジュラー屋さんほどコードから離れていない
    • モジュラー屋さんはプログラミング実現性みたいなものを全く考えない。
  • ものごとは綺麗なツリーにはならないので、多重継承*3の形で分析することもある

次回Chapter 4の“なぜドメインモデルだと複雑な業務ロジックを整理しやすいのか”から

*1:いたらすみません

*2:この本の話では無い

*3:Javaではプログラミングで表現できない