【LT登壇7名決定!】リーダブルコード LT会 - vol.4 #readablelt https://rakus.connpass.com/event/253650/
一緒に使うことが多い値は別クラスにしよう(Data Clumps)リーダブルコード LT会 - vol.4 #readablelt 20220824きり丸(水上 皓登)@nainaistar
View Slide
※ Javaのソースコードで例示します。
名前:きり丸(水上 皓登)twitter:nainaistarGitHub:hirotoKirimaruブログ:きり丸の技術日記https://nainaistar.hatenablog.com/3一度でいいからバズってみたい
良いコードとは?
良いコードとは悪くないコード
悪くないコードにするには、Code Smellsに気を付けたらいい
今日のキーワードData Clumps(データの塊)
Data Clumpsとは(データの塊)関連性が高く、一緒に使われることが多いデータを別のクラスにまとめること。モデリングをしっかりしている人にとっては、当たり前といえば当たり前のお話です。
期間(日付)
例えば、期間(日付)開始日 または 終了日を纏めたクラスがあるとします。最低限の仕様としては次の挙動が考えられます● 存在する日付であること(2022/02/31は許容しない)○ 文字列型ではなく、日付型なら発生しない● 開始日は必須であること(終了日がないパターンは期間無限)● 開始日のほうが終了日より古いこと(期間の逆転禁止)
例えば、期間(日付)public class サブスクリプション {String id;LocalDate contractStartDate;LocalDate contractEndDate;int qty;BigDecimal price;}サブスクリプションのモデル。契約開始日、契約終了日を持っています。
例えば、期間(日付)public class サブスクリプション {String id;期間 contractTerm;int qty;BigDecimal price;}public record 期間(LocalDate start, LocalDate end){}ちょっとすっきり。契約の開始日ではなく、開始日、終了日と単語を短縮できるのも好き。
例えば、期間(日付)複雑な仕様も閉じ込めることができます● 経過期間の計算○ 2022/01/15-2022/12/31 -> 11ヵ月?12ヵ月?● 期間が暦上の一か月で割り切れるかどうか。○ 開始日に対して、日割の発生しない終了日であるか● 期間の重複チェック○ A.start <= B.end && B.start <= A.end
例えば、期間(日付)期間と期間の比較は初見は面倒。
期間(時刻)
例えば、期間(時刻)株式市場の取引時間をアプリで管理するとします。日本市場:0900-1500(9時から15時)米国市場:2330-0600(23時30分から翌6時)仮想通貨:0000-2400(24時間いつでも可能)
例えば、期間(時刻)public class 取引時間 {String id;String startTime;String endTime;public isBusinessTime(LocalDateTime now) {// いい感じの処理}}Stringの”0900”と”1500”を元に、いい感じに処理する
例えば、期間(時刻)日付を跨ぐ処理はどうしよう…?2400が上限値ではない?休み時間は…?Stringをたくさんいじりたくないな…
例えば、期間(時刻)開始時刻は同じように管理して、終了時刻ではなくN時間連続して営業していることを示せばいいのでは?フレックスのように、9:00勤務開始、18:00勤務終了が大事ではなく、勤務時間が8H確保していることが大事では?
例えば、期間(時刻)public class 取引時間 {String id;String startTime;int straightHours;public isBusinessTime(LocalDateTime now) {// いい感じの処理}}終了時刻はすぐにわからないが、日付を跨ぐ場合には優しいかもしれない
例えば、期間(時刻)このモデルが絶対的な正義だという話ではありません。必要なデータだけに絞って注目し、制約事項を丁寧に処理していた結果、当初、想像できなかったモデリングに導かれることがありうるという例です。
DBの複合キー
DBの複合キーpublic class 企業 {String 企業ID;}public class 契約 {String 企業ID;String 契約ID;}public class 販売 {String 企業ID;String 契約ID;String 販売ID;}public class 仕入 {String 企業ID;String 契約ID;String 販売ID;String 仕入ID;}適切な例が思い浮かばず…
例えば、DBの複合キー複合キー自体が、既にデータの塊。あくまで一意に定めるための塊であり、振る舞いを持つものではないただ、名前を持ったデータの塊は意図が非常に伝わりやすい。例:・企業IDと契約IDと販売IDから仕入全体の金額を知りたい・この販売の仕入全体の金額を知りたい
※ 私がコードリーディングする時の脳内に浮かんでいる図です
DBの複合キーpublic class 販売主キー {String 企業ID;String 契約ID;String 販売ID;}public class 仕入 {String 企業ID;String 契約ID;String 販売ID;String 仕入ID; # パラメータが一つにまとまるpublic boolean salesEquals(販売主キー id) {return (this.企業ID.equals(id.企業ID) &&this.契約ID.equals(id.契約ID) &&this.販売ID.equals(id.販売ID))}}販売の複合主キーとして、親のIDをすべて持つ前提
DBの複合キー企業IDと契約IDと販売IDから仕入全体の金額を知りたいあまりER図は意識しない
仕入企業IDで絞込契約IDで絞込販売IDで絞込
DBの複合キーこの販売の仕入全体の金額を知りたいコードリーディングで認識すべき内容が減る
仕入仕入販売 販売
例えば、DBの複合キーばらばらのデータだと、それぞれの値に関係性がない可能性がある。一つのデータの塊にすると、意図を伝えてくれる。特にER図で考えたときに、複合主キーという存在は、データの絞込ではなく、一意に決定づけられるため、思考コストが減る。
ちょっと脱線
ちょっと脱線● オブジェクト化したからと言って、DBの構造も合わせる必要はありません。
例えば、期間(日付)public class 販売 {String id;期間 salesTerm;int qty;BigDecimal price;}public record 期間(LocalDate start, LocalDate end){}Javaの構造。
例えば、期間(日付)CREATE TABLE 販売(id VARCHAR(13) PRIMARY KEY,start_date TIMESTAMP,end_date TIMESTAMP,qty INTEGER,price INTEGER,);DBの構造。
例えば、期間(日付)CREATE TABLE 販売(id VARCHAR(13) PRIMARY KEY,term_id VARCHAR(13),qty INTEGER,price INTEGER,);CREATE TABLE 期間(id VARCHAR(13) PRIMARY KEY,start_date TIMESTAMP,end_date TIMESTAMP,)わざわざここまでする必要はない。
例えば、期間(時刻)CREATE TABLE 取引時間(id VARCHAR(13) PRIMARY KEY,start_time VARCHAR(4),end_time VARCHAR(4),);CREATE TABLE 取引時間(id VARCHAR(13) PRIMARY KEY,start_time VARCHAR(4),straight_hours INTEGER);モデリングの結果、DBの構造を変えること自体は良いこと
ちょっと脱線● 使いづらくなったら、データ構造を解体してフラットにするのは全然あり
例えば、期間(日付)public class サブスクリプション {期間 contractTerm;LocalDate cancelDate;public boolean isCancelable{// 期間クラスの開始日を使用する。// 別クラスの属性を欲しがるという意味のCode Smells// Feature Envy// が起きかねないreturnthis.contractTerm.start.isAfter(cancelDate)}}解約日という概念を追加。
例えば、期間(日付)public class サブスクリプション {期間 contractTerm;LocalDate cancelDate;}↓public class サブスクリプション {LocalDate contractStartDate;LocalDate contractEndDate;LocalDate cancelDate;public boolean cancelable?(){return // 期間クラスに// 解約可能かどうか判断メソッドを作ってもいい// 作らなくてもいいnew 期間(contractStartDate, cancelDate).noProration()}}使いづらかったら、データ構造をフラットにするのはあり。
ちょっと待ってメリットだけなの?
メリットだけなの?● プリミティブな値をオブジェクト化しているので、意図せずに別インスタンスから同一インスタンスを参照してしまう可能性があります。また、コピーも面倒です。JavaScriptでは {...object} のシャローコピーで済むのに、ディープコピーをするためにLodashを使う必要があります。
メリットだけなの?● どこまで処理を閉じ込めるかは議論の余地があります。サブスクリプションを解約する際に、解約時点で利用が不可能になるか、月末まで利用可能か、日割りが絡む中途半端な時期に解約できないか、違約金を払えば解約できるのか…。営業時間にサマータイムの時間は入りますか?
メリットだけなの?● 無難な処理については間違いなく含めていいです。ただ、業務色が強い機能については、検討の余地があります。もちろん、「期間」という一般名詞ではなく、「立合時間」(株式が取引可能な時間)といった業務に特化した名前があるのであれば、含めても問題ないでしょう。🤼< 適切かどうかチームで話し合おう
その他の例
他にも● 期間○ 開始日 と 終了日○ 開始時間 と 継続時間● 行列○ X と Y と Z■ 加算・減算・内積・外積 等々
例えば● ページング○ 現在ページ数 と 最大ページ数 と ページサイズ○ ソート■ キー と 優先度 と 順番(ASC, DESC) と Nullableの扱い● LIMIT, OFFSETをまとめたRowBounds
まとめ
まとめ従えば必ず有力になる強力なルールではありません。導入せずとも、特に困ることは無いでしょう。ただ、データをまとめていくことで、より洗礼されたモデルになる可能性があります。期間という概念はどのシステムでも実装する概念だと思いますので、まずは期間からまとめてみてはいかがでしょうか。
Appendix
例えば、期間(日付)個人的には、日付の比較が非常に苦手なので、閉じ込めてしまいたいです。これが嫌い。AがBより古い(同値含まず): A.isBefore(B)AがBより古い(同値含む) :!A.isAfter(B)
話すこと / 話さないこと● Code Smells● Primitive Obsession● Java● Primitive Obsessionが有効ではない個所(私もわからない)詰めなおしは推奨されないので、べき論が分からない話すこと 話さないこと
対象者 / 非対象者● リファクタリングをしたいけど、ヒントが分からない人● リファクタリングに興味が無い人● 動的型付言語対象者 非対象者
登壇を見た人への期待するアクション● Code Smellsに興味を持つ● 型を作ることをやってみるアクション