はじめに
この記事は、麻雀大好きな新米エンジニアが見よう見まねで点棒(麻雀における通貨みたいなもの)を値オブジェクトにしてみたという記事です。暇つぶし程度に読んでもらえたら嬉しいです。
まず簡単に「そもそも値オブジェクトって?」という話をしてから、実際に自分が書いてみた麻雀の点棒オブジェクトを紹介したいと思います。
(※ 麻雀への知識がある前提で書かれた記事です。何もわからないという方は、「値オブジェクトとは」の項目だけ読むことをお勧めします。)
値オブジェクトとは
値オブジェクトとは、値をクラスとして表現する設計パターンのことです。
より詳しく言えば、アプリケーションで頻繁に使われる金額や日付、電話番号といった値をクラスとして扱い、各値の正常性を初期化段階で担保しながら値そのものとその振る舞いをまとめて管理することです。
例えば金額を単なるint型の変数として扱った場合、金額用の変数に負の値が入力されてしまったり、「販売数」など別のint型を金額用の変数に加算してしまうといったミスが起こり得ますが、金額を値オブジェクト化することでこのようなミスを防ぐことができます。
また、税込計算メソッドなど値に持たせたい振る舞いを凝集度(*1)高く持つことができます。
上記のように何かと便利な値オブジェクトですが、実装する際には以下の特徴を実装しなくてはならない点に注意が必要です。
- 値が不変である
- 交換可能
- 値の等価性によって比較される
※ 参考資料
- 『アプリケーションサービスの凝集度を高めたい』(※1 凝集度について)
- 『良いコード/悪いコードで学ぶ設計入門』(値オブジェクトについて)
- 『ドメイン駆動設計入門』(値オブジェクトについて)
点棒オブジェクト
サンプルコード
# frozen_string_literal: true
#
# 点棒
#
class Counter
include Comparable
attr_reader :point
# ゲーム開始時の点棒
DEFAULT_POINT = 25000
# 順位点処理として、4位3位2位がそれぞれ1位に対して支払う点棒(オカ)
OKA_POINT = 5000
# 順位点処理として、4位が1位に支払う点棒(ウマ)
UMA_POINT1 = 30000
# 順位点処理として、3位が2位に支払う点棒(ウマ)
UMA_POINT2 = 10000
def initialize(point = DEFAULT_POINT)
# 最小の点棒が100点なので、100で割り切れない値は不正とする
raise ArgumentError, '不正な点棒です' if point % 100 != 0
@point = point
freeze # 不変にする
end
# 値オブジェクトは不変なので、オブジェクトを交換する形で足し算を実装する
def add(other)
Counter.new(point + other.point)
end
# 引き算も同様
def subtract(other)
Counter.new(point - other.point)
end
# 値の等価性で比較する
def <=>(other)
point <=> other.point
end
# 着順に応じて順位点処理を行う
def add_rank_point(rank)
case rank
when 1
rank_point = UMA_POINT1 + OKA_POINT * 3
when 2
rank_point = UMA_POINT2 - OKA_POINT
when 3
rank_point = -UMA_POINT2 - OKA_POINT
when 4
rank_point = -UMA_POINT1 - OKA_POINT
else
raise ArgumentError, '不正な着順です'
end
Counter.new(point + rank_point)
end
end
サンプルコード詳細
attr_reader
値を不変としたかったので、setterのみを定義するattr_reader
で実装しています。
initialize
麻雀の点棒は100の位までしか存在しないので100で割り切れない引数に対してエラーを吐くようにしました。金額オブジェクトなどを実装する際は、同様にinitialize内で負の値に対してバリデーションをかけるといいと思います。
また、不変にするためにfreeze
をかけています。
<=>
値の等価性で比較するために記述しています。
この記述がないと同値のオブジェクトであってもfalseと判定されてしまいます。
Counter.new(100) == Counter.new(100)
#=> false
# <=>メソッド定義後
Counter.new(100) == Counter.new(100)
#=> true
add_rank_point
着順に応じた順位点処理を記述しています。このように値そのものと値の振る舞いをまとめて記述出来ることが値オブジェクトのいい点です。
具体的な内容については特に見なくても大丈夫です。
感想
実務で値オブジェクトを書く機会が今までなく、初めて書いたのですがなかなか面白かったです。もっとこうした方がいいという意見があれば是非コメントお願いします。
足し算と引き算をオブジェクトを受け取るシンプルな形で実装しましたが、麻雀は動く点数が翻数と符によって確定するので引数に翻数、符、供託を受け取って計算した方が点数の正当性は確保できたかもしれません。もし機会があればいつか書いてみます。
Mリーグも開幕です!麻雀の熱上げていきましょう🔥