どうもです、タドスケです。
こちらを作る際、以前に作った斜方蹴射のゲームモデル部分を使い回したのですが、コードの書き方がだいぶ良くないなぁと思いました。
当時はPythonの実務経験がなく、コード規約なども知らずに「とにかく動くもの作る」ことに集中していました。
Pythonの実務経験(まだ3ヶ月ではありますが)を経て、今の自分の感覚で「綺麗なコード」を書いてみようと思います。
リファクタリングのやり方に関して、参考になる点があれば幸いです。
以前のコード
以前のコードを載せておきます。
"""
斜方蹴射 ゲームロジック
"""
import random
from enum import Enum
# ===========================
# ゲームモード
# ===========================
class Mode(Enum):
INIT = 0 # 初期
POW_X = 1 # Xパワー決定待ち
POW_Y = 2 # Yパワー決定待ち
FLYING = 3 # 飛行中
WAIT_HIT = 4 # ヒット演出中
WAIT_OUT = 5 # 外れ演出中
TIME_UP = 6 # 時間切れ演出中
RESULT = 7 # リザルト
# ===========================
# ゲーム本体
# ===========================
class Game:
# クラス変数
max_pow_x = 100 # パワー横最大値
max_pow_y = 100 # パワー縦最大値
max_time = 60 # 制限時間
init_ball_x = 20 # ボール初期位置x
init_ball_y = 280 # ボール初期位置y
waittime_hit = 1.0 # ヒット演出時間
waittime_end = 2.0 # タイムアップ演出時間
ground_h = 20 # 地面の高さ
min_target_x = 150
max_target_x = 350
min_target_y = 30
max_target_y = 180
scr_w = 400
scr_h = 300
# コンストラクタ
def __init__(self):
self.stage = Stage(Game.ground_h)
self.reset()
self.time = 0 # 残り時間
self.mode = Mode.INIT # モード番号
self.decide = False # 決定操作がされたか
self.wait_timer = 0 # 演出タイマー
self.score = 0 # スコア
# 状態リセット
def reset(self):
self.ball = Ball()
self.ball.reset(Game.init_ball_x, Game.init_ball_y)
self.pow_x = 0
self.pow_y = 0
self.stage.reset_wind()
self.target = Target()
self.target.set_pos(random.randint(Game.min_target_x, Game.max_target_x),
random.randint(Game.min_target_y, Game.max_target_y))
# 更新
def update(self, delta):
# 初期設定
if self.mode == Mode.INIT:
# インスタンス変数のリセット
self.reset()
self.score = 0
self.time = Game.max_time
self.mode = Mode.POW_X
# 横パワー決定待ち
elif self.mode == Mode.POW_X:
# 残り時間更新
self.time -= delta
if self.time <= 0:
self.time = 0
self.wait_timer = Game.waittime_end
self.mode = Mode.TIME_UP
return
# 横パワー更新
else:
self.pow_x += 1
if Game.max_pow_x < self.pow_x:
self.pow_x = 0
if self.decide:
self.mode = Mode.POW_Y
# 縦パワー決定待ち
elif self.mode == Mode.POW_Y:
# 残り時間更新
self.time -= delta
if self.time <= 0:
self.time = 0
self.wait_timer = Game.waittime_end
self.mode = Mode.TIME_UP
# 縦パワー更新
else:
self.pow_y += 1
if Game.max_pow_y < self.pow_y:
self.pow_y = 0
if self.decide:
self.ball.kick(self.pow_x, self.pow_y)
self.mode = Mode.FLYING
# ボール飛行中
elif self.mode == Mode.FLYING:
self.ball.update(delta, self.stage.wind)
# 的に接触
if self.target.is_hit(self.ball.x, self.ball.y):
self.wait_timer = Game.waittime_hit
self.mode = Mode.WAIT_HIT
# 地面に接触
elif Game.scr_h - Game.ground_h <= self.ball.y:
self.ball.y = Game.scr_h - Game.ground_h
self.wait_timer = Game.waittime_hit
self.mode = Mode.WAIT_OUT
# ヒット演出待ち
elif self.mode == Mode.WAIT_HIT:
self.wait_timer -= delta
if self.wait_timer <= 0:
self.score += int(self.ball.time)
self.reset()
self.mode = Mode.POW_X
# 外れ演出待ち
elif self.mode == Mode.WAIT_OUT:
self.wait_timer -= delta
if self.wait_timer <= 0:
self.reset()
self.mode = Mode.POW_X
# タイムアップ演出待ち
elif self.mode == Mode.TIME_UP:
self.wait_timer -= delta
if self.wait_timer <= 0:
self.wait_timer = 0
self.mode = Mode.RESULT
# リザルト
elif self.mode == Mode.RESULT:
if self.decide:
self.mode = Mode.INIT
self.decide = False
# 決定操作
def send_decide(self):
self.decide = True
# ボール飛行中か
def is_flying(self):
return self.mode == Mode.FLYING
# ヒット演出中か
def is_hit(self):
return self.mode == Mode.WAIT_HIT
# 時間切れか
def is_timeup(self):
return self.mode == Mode.TIME_UP
# リザルト表示中か
def is_result(self):
return self.mode == Mode.RESULT
# ===========================
# ステージ
# ===========================
class Stage:
# クラス変数
wind_range = (-10, 10) # 風速の範囲
# コンストラクタ
def __init__(self, ground_h):
self.ground_h = ground_h # 地面の高さ
self.wind = 0
# 風速の再設定
def reset_wind(self):
(min, max) = Stage.wind_range
self.wind = random.randint(min, max)
# ===========================
# ボール
# ===========================
class Ball:
# クラス変数
size = 10 # 大きさ
gravity = 0.5 # ボールにかかる重力
wind_rate = 0.012 # 風の適用率
pow_rate_x = 0.15 # パワー横の適用率
pow_rate_y = 0.20 # パワー縦の適用率
# コンストラクタ
def __init__(self):
self.reset()
# リセット
def reset(self, x=0, y=0):
self.x = x # X座標
self.y = y # Y座標
self.sx = 0 # X速さ
self.sy = 0 # Y速さ
self.time = 0 # 飛行時間
# キック
def kick(self, pow_x, pow_y):
self.sx = pow_x * Ball.pow_rate_x
self.sy = pow_y * -Ball.pow_rate_y
self.time = 0
# 更新
def update(self, delta, wind):
self.sx += wind * Ball.wind_rate
self.sy += Ball.gravity
self.x += self.sx
self.y += self.sy
self.time += delta * 10
# ===========================
# 的
# ===========================
class Target:
# クラス変数
size_w = 20
size_h = 100
# コンストラクタ
def __init__(self):
self.set_pos(0, 0)
# 座標設定
def set_pos(self, x, y):
self.x = x
self.y = y
# 当たり判定
def is_hit(self, x, y):
if self.x < x and x < self.x + Target.size_w and self.y < y and y < self.y + Target.size_h:
return True
return False
このコードに対して、リファクタリングを行っていきます。
コメントをdocstring形式にする
まず気になったのはコメント部分。
# ===========================
# 的
# ===========================
class Target:
# クラス変数
size_w = 20
# 座標設定
def set_pos(self, x, y):
self.x = x
こんな感じでクラスの前にコードの「土手」を作っていますが、これは完全に我流です。
また、クラスや関数に対してのコメントも普通のコメント(#)を使っていますが、推奨されるコメント規約があるので、そちらのスタイルを参考にします。
(引数の説明など、厳密には合わせていない部分もあります)
対応したのが以下のコードです。
class Target:
"""的."""
#: 幅
size_w = 20
def set_pos(self, x, y):
"""座標設定."""
self.x = x
モジュールを分割する
ゲームモデル全体が一つのファイルなので、コードがちょっと長くなっています。
このあたりは明確な正解がないのですが、今回はクラスごとに.pyファイルを分けることにしました。
ユニットテストを書く
リファクタリングを行うにあたって重要とされるものにユニットテストが挙げられます。
ユニットテストとは、モジュール・クラス・関数などが意図通りかを確認するために書くコードです。
製品には含まれませんが、コードの品質を保証するために重要なものです。
(GitHubなどで公開されているケースでは、testsというフォルダ以下にユニットテスト用のコードが含まれていることが多いです)
この規模のゲームであれば組み込み箇所で確認するのでも十分なのですが、今回は練習も兼ねてやってみます。
Pythonのユニットテスト用ライブラリはいくつかありますが、今回はunittestを使います。
"""targetモジュールのテスト."""
import unittest
class TestTarget(unittest.TestCase):
def setUp(self) -> None:
super().setUp()
self.target = Target(size=Size(20, 60))
def test_init(self):
self.assertEqual(self.target.size, Size(20, 60))
この例では、「Targetクラスの生成時に渡したサイズ情報が正しく適用されているか」を確認しています。
self.assertEqual(a, b) は、aとbが等しければテストOK という意味です。
このテストを実行すると、以下のような結果が出ます。
============================= test session starts ============================= collecting ... collected 1 items test_target.py::TestTarget::test_init PASSED [ 100%] ============================== 1 passed in 0.02s ==============================
逆に失敗するとこんな感じ。
test_target.py::TestTarget::test_init FAILED [100%] test_target.py:17 (TestTarget.test_init) <shahou.app.model.values.Size object at 0x0000022B44BB4FA0> != <shahou.app.model.values.Size object at 0x0000022B44BB4F70> 期待:<shahou.app.model.values.Size object at 0x0000022B44BB4F70> 実際:<shahou.app.model.values.Size object at 0x0000022B44BB4FA0>
Targetクラスに手を入れる度にこのテストを実行することで、動作に変更が無いかを確認できます。
特にゲームを起動してシーケンスを進めないと確認できないようなクラスがあった場合、毎回起動してそこまで進むのは大変です。
もちろん起動しての確認も必須ですが、テストOKになった場合のみ起動して確認することで、確認の手間を減らすことができます。
メンバー変数を読み取り専用にする
現状、メンバー変数は全てpublicになっており、クラス外から好きなように参照・設定できます。
参照だけならともかく、設定できてしまうとバグが起きた際に「どこで値が変更されているかわからない!」ということになりますので、参照だけできるようにします。
具体的には以下のようにします。
- メンバー変数をprivate化する
- 参照が必要な変数に@propertyをつける
これをやったのが以下のコードです。
class Target:
"""的."""
def __init__(self):
self._pos: Position = Position(0, 0)
@property
def position(self) -> Position:
"""位置."""
return self._pos
self._posのように変数の先頭に「_」を付けることで、「クラス外から直接アクセスしないでね」と宣言します。
※Pythonの場合、実際には無理矢理アクセスできてしまうのですが、エディタ上で警告を出してくれたりするので、それでもOKとします。
外部から_posの値を参照したい場合は、target.position とすれば参照できます。
逆に target.position = pos のように設定しようとするとエラーが出ます。
これで読み手に「positionの変化を追うなら、Targetクラスの中だけを見ればいいのね」と伝えることができます。
このように、チームでコードを書く際には「他の人が見なきゃいけない範囲を以下に狭くするか」ということが大切です。
「他の人」というのは、「3ヶ月後の自分」も含まれるかもしれません💦
型ヒントをつける
Pythonには型ヒント(タイプアノテーションともいう)があり、これを付けることで変数や引数の型を明示できます。
型を明示することでコードが読みやすくなり、mypy などの解析ツールを使用したチェックも行えるようになります。
def kick(self, pow_x: float, pow_y: float) -> None:
"""キック."""
self.sx, self.sy = self._power_ratio * (pow_x, pow_y)
self._time = 0
↑の例では、kickの引数pow_xとpow_yがどちらもfloat型であり、戻り値が無いことを明示しています。
例えばこの関数の最後に return 0 と書き加えた場合、mypy実行時に以下のエラーが出ます。
-> None を書いていないとこのエラーは出ません。
これにより、意図しない値をreturnしたり、し忘れたりするのを防ぐことができます。
値オブジェクトの導入
現状、的のサイズを表す変数は int 型になっています。
size_w: int = 20
size_h: int = 100
int型はマイナスの値も含むので、サイズの変数に「-5」などの値を渡すこともできてしまいます。
「渡さなければいいじゃない」と思うかもしれませんが、例えば他の関数で計算されたマイナス値が意図せず渡されてしまうケースもあります。
使用する側でサイズ値を渡す前にチェックを行うこともできますが、使用箇所が増えた場合に同じようなチェックコードが各所に書かれることになります。
この問題を解決するため、ちょっと前に読んだドメイン駆動設計の本に出てきた値オブジェクトを導入することにします。
値オブジェクトとは、値をラッピングしたクラスのことです。
例えばサイズの場合は以下のようになります。
class Size:
"""大きさ."""
def __init__(self, width: int, height: int) -> None:
if width <= 0:
raise ValueError
self._width = width
if height <= 0:
raise ValueError
self._height = height
使う側のコードはこう。
size: Size = Size(10, 20)
initの内部で、引数に対してエラーチェック(マイナスならエラー)を行っています。
またSizeクラスにはサイズを変更するためのメソッドを用意していない(読み込み専用のプロパティのみ)ので、生成後に外部からサイズが変更される恐れもありません。
エラーなしに生成できたsizeの値は、必ず正しいことが保証されます。
この値オブジェクトを各所で利用していくことで、安定かつ読みやすいコードになります。
ただし以下のデメリットもあります。
- コードを書く量が増える
- パフォーマンスが落ちる
特にゲーム開発においてはパフォーマンスは重要なので、実際の現場ではあまりできないやり方かもしれません。
※実践している例もあります
テスト同様、今回は検証がてらやってみます。
ロジックをクラス内に閉じ込める
以下のコード。
self.wait_timer -= delta
if self.wait_timer <= 0:
# 処理
ヒット演出の終了待ち(wait_timerが0になるのを待つ)をしている部分ですが、生の値をそのまま使用しています。
wait_timerを使う場所で毎回似たような処理を書かなければいけなくなります。
そこで、前述の値オブジェクトでTimerクラスを作り、クラス内に以下のようなメソッドを追加します。
class Timer:
"""タイマー."""
def __init__(self, seconds: float) -> None:
if seconds < 0:
raise ValueError
self._time = seconds
def update(self, delta: float) -> None:
"""タイマーの更新."""
if delta < 0:
raise ValueError
self._time -= delta
self._time = max(self._time, 0)
def is_end(self) -> bool:
"""終了しているか."""
return math.isclose(self._time, 0)
使う側のコードは以下のようになります。
self._wait_timer.update(delta)
if self._wait_timer.is_end():
# 処理
これにより、Timerクラスを使う側は内部の細かい計算処理のことを知らなくても、
updateにdeltaを渡して、is_end()がTrueになるまで待てばいいんだな
とだけ把握しておけば作業ができるようになります。
仮にupdateやis_endメソッドの実装が変わったとしても、引数や戻り値を変えない限り使用する側への影響はありません。
リファクタリング後のコード
以上のことを踏まえて書き直したコードが以下です。
※コード量が増えたので、一部のファイルのみ抜粋しています。
ゲーム本体(game.py)
"""ゲームモデル."""
from __future__ import annotations
from enum import Enum
from shahou.app.model.ball import Ball, PowerRatio
from shahou.app.model.power import Power
from shahou.app.model.stage import Stage
from shahou.app.model.target import Target
from shahou.app.model.values import (
Timer,
Score,
Size,
Position,
Ratio,
)
class Mode(Enum):
"""ゲームモード."""
INIT = 0 # 初期
POW_X = 1 # Xパワー決定待ち
POW_Y = 2 # Yパワー決定待ち
FLYING = 3 # 飛行中
WAIT_HIT = 4 # ヒット演出中
WAIT_OUT = 5 # 外れ演出中
TIME_UP = 6 # 時間切れ演出中
RESULT = 7 # リザルト
class Game:
"""ゲーム本体."""
#: 制限時間
_MAX_TIME: float = 60
#: ヒット演出時間
_WAIT_TIME_HIT: float = 1.0
#: タイムアップ演出時間
_WAIT_TIME_END: float = 2.0
#: 的の最小位置
_TARGET_POS_MIN: Position = Position(150, 100)
#: 的の最大位置
_TARGET_POS_MAX: Position = Position(350, 200)
#: 的のサイズ
_TARGET_SIZE: Size = Size(20, 80)
#: ボール初期位置
_BALL_POS_INIT: Position = Position(20, 25)
#: ボールサイズ
_BALL_SIZE: int = 6
#: ボールにかかる重力
_BALL_GRAVITY: float = 0.4
#: 風の適用率
_BALL_WIND_RATE: Ratio = Ratio(0.01)
#: パワーの適用率
_BALL_POWER_RATIO: PowerRatio = PowerRatio(0.08, 0.2)
#: 地面の高さ
_GROUND_H: int = 20
def __init__(self) -> None:
self._stage = Stage(Game._GROUND_H)
self._target = Target(
size=Game._TARGET_SIZE,
min_pos=Game._TARGET_POS_MIN,
max_pos=Game._TARGET_POS_MAX)
self._power: Power = Power()
self._timer: Timer = Timer(0)
self._wait_timer: Timer = Timer(0) # 演出タイマー
self._reset()
self._mode = Mode.INIT # モード番号
self._decide = False # 決定操作がされたか
self._score = Score(0)
# update関数テーブル
self.update_dictionary = {
Mode.INIT: self._update_init,
Mode.POW_X: self._update_pow_x,
Mode.POW_Y: self._update_pow_y,
Mode.FLYING: self._update_flying,
Mode.WAIT_HIT: self._update_wait_hit,
Mode.WAIT_OUT: self._update_wait_out,
Mode.TIME_UP: self._update_timeup,
Mode.RESULT: self._update_result,
}
@property
def score(self) -> Score:
"""スコア."""
return self._score
@property
def time(self) -> float:
"""残り時間."""
return self._timer.time
@property
def target(self) -> Target:
"""的."""
return self._target
@property
def stage(self) -> Stage:
"""ステージ."""
return self._stage
@property
def power(self) -> Power:
"""パワー."""
return self._power
@property
def ball(self) -> Ball:
"""ボール."""
return self._ball
def _reset(self) -> None:
"""状態をリセットする."""
self._ball = Ball(
size=Game._BALL_SIZE,
power_ratio=Game._BALL_POWER_RATIO,
gravity=Game._BALL_GRAVITY,
wind_rate=Game._BALL_WIND_RATE)
self._ball.reset(Game._BALL_POS_INIT)
self._power.reset()
self._stage.reset_wind()
self._target.move_random()
def _update_init(self, delta: float) -> None:
"""更新:初期設定."""
self._reset()
self._score = Score(0)
self._timer = Timer(Game._MAX_TIME)
self._mode = Mode.POW_X
def _update_pow_x(self, delta: float) -> None:
"""更新:横パワー決定待ち."""
# 残り時間更新
self._timer.update(delta)
if self._timer.is_end():
self._wait_timer = Timer(Game._WAIT_TIME_END)
self._mode = Mode.TIME_UP
# 横パワー更新
else:
self._power.increase_x()
if self._decide:
self._mode = Mode.POW_Y
def _update_pow_y(self, delta: float) -> None:
"""更新:縦パワー決定待ち."""
# 残り時間更新
self._timer.update(delta)
if self._timer.is_end():
self._wait_timer = Timer(Game._WAIT_TIME_END)
self._mode = Mode.TIME_UP
# 縦パワー更新
else:
self._power.increase_y()
if self._decide:
self._ball.kick(self._power.x, self._power.y)
self._mode = Mode.FLYING
def _update_flying(self, delta: float) -> None:
"""更新:ボール飛行中."""
self._ball.update(delta, self._stage.wind)
# 的に接触
if self._target.is_hit(self._ball.pos):
self._wait_timer = Timer(Game._WAIT_TIME_HIT)
self._mode = Mode.WAIT_HIT
# 地面に接触
elif self._is_ball_on_ground(self._ball):
self._ball.attach_bottom(Game._GROUND_H)
self._wait_timer = Timer(Game._WAIT_TIME_HIT)
self._mode = Mode.WAIT_OUT
@staticmethod
def _is_ball_on_ground(ball: Ball):
"""ボールが地面に触れたか."""
return ball.bottom <= Game._GROUND_H
def _update_wait_hit(self, delta: float) -> None:
"""更新:ヒット演出待ち."""
self._wait_timer.update(delta)
if self._wait_timer.is_end():
self._score.add(self._balltime_to_score())
self._reset()
self._mode = Mode.POW_X
def _balltime_to_score(self) -> Score:
"""ボールの滞空時間からスコアを得る."""
return Score(int(self._ball.time))
def _update_wait_out(self, delta: float) -> None:
"""更新:外れ演出待ち."""
self._wait_timer.update(delta)
if self._wait_timer.is_end():
self._reset()
self._mode = Mode.POW_X
def _update_timeup(self, delta: float) -> None:
"""更新:タイムアップ演出待ち."""
self._wait_timer.update(delta)
if self._wait_timer.is_end():
self._wait_timer = Timer(0)
self._mode = Mode.RESULT
def _update_result(self, delta: float) -> None:
"""更新:リザルト."""
if self._decide:
self._mode = Mode.INIT
def update(self, delta: float) -> None:
"""更新."""
func = self.update_dictionary[self._mode]
func(delta)
self._decide = False
def send_decide(self) -> None:
"""決定操作."""
self._decide = True
def is_flying(self) -> bool:
"""ボール飛行中か."""
return self._mode == Mode.FLYING
def is_hit(self) -> bool:
"""ヒット演出中か."""
return self._mode == Mode.WAIT_HIT
def is_timeup(self) -> bool:
"""時間切れか."""
return self._mode == Mode.TIME_UP
def is_result(self) -> bool:
"""リザルト表示中か."""
return self._mode == Mode.RESULT
的(target.py)
"""的."""
import random
from shahou.app.model.values import Position, Size
class Target:
"""的."""
def __init__(
self,
size: Size,
min_pos: Position,
max_pos: Position) -> None:
self._size = size
self._min_pos = min_pos
self._max_pos = max_pos
self._pos: Position = Position(0, 0)
@property
def position(self) -> Position:
"""位置."""
return self._pos
@property
def size(self) -> Size:
"""サイズ."""
return self._size
def move_random(self) -> None:
"""位置をランダムで移動させる."""
self._pos = Position(
x=random.randint(self._min_pos.x, self._max_pos.x),
y=random.randint(self._min_pos.y, self._max_pos.y))
def is_hit(self, position: Position) -> bool:
"""地点(x, y)と衝突しているか."""
left = self._pos.x
right = self._pos.x + self._size.width
top = self._pos.y
bottom = self._pos.y + self._size.height
contain_x = (left <= position.x <= right)
contain_y = (top <= position.y <= bottom)
if contain_x and contain_y:
return True
return False
値オブジェクトたち(values.py)
"""値オブジェクトたち."""
from __future__ import annotations
import math
class Ratio:
"""割合."""
def __init__(self, ratio: float) -> None:
if ratio < 0:
raise ValueError
self._ratio: float = ratio
@property
def ratio(self) -> float:
return self._ratio
def __eq__(self, other) -> bool:
"""等価比較."""
if isinstance(other, Ratio):
return math.isclose(self._ratio, other.ratio)
elif isinstance(other, float):
return math.isclose(self._ratio, other)
return False
def __mul__(self, other):
"""乗算."""
if isinstance(other, Ratio):
return self._ratio * other.ratio
return self._ratio * other
class Position:
"""座標."""
def __init__(self, x: float, y: float) -> None:
self._x = x
self._y = y
@property
def x(self) -> float:
"""X座標."""
return self._x
@property
def y(self) -> float:
"""Y座標."""
return self._y
def get(self) -> tuple[float, float]:
"""タプルでまとめて取得"""
return self._x, self._y
def move(self, x: float, y: float) -> Position:
"""x, yだけ移動した座標を得る."""
return Position(self._x + x, self._y + y)
def __eq__(self, other) -> bool:
"""等価比較."""
if isinstance(other, Position):
return self._x == other.x and self._y == other.y
elif isinstance(other, tuple):
(x, y) = other
return self._x == x and self._y == y
return False
class Size:
"""大きさ.
:param width:
"""
def __init__(self, width: int, height: int) -> None:
if width <= 0:
raise ValueError
self._width = width
if height <= 0:
raise ValueError
self._height = height
@property
def width(self) -> int:
"""幅."""
return self._width
@property
def height(self) -> int:
"""高さ."""
return self._height
def get(self) -> tuple[int, int]:
"""タプルでまとめて取得する."""
return self._width, self._height
def __eq__(self, other) -> bool:
"""等価比較."""
if isinstance(other, Size):
return self._width == other.width and self._height == other.height
elif isinstance(other, tuple):
(w, h) = other
return self._width == w and self._height == h
return False
class Timer:
"""タイマー.
:param seconds: 秒数
"""
def __init__(self, seconds: float) -> None:
if seconds < 0:
raise ValueError
self._time = seconds
def update(self, delta: float) -> None:
"""タイマーの更新."""
if delta < 0:
raise ValueError
self._time -= delta
self._time = max(self._time, 0)
@property
def time(self) -> float:
"""残り時間."""
return self._time
def is_end(self) -> bool:
"""終了しているか."""
return math.isclose(self._time, 0)
class Score:
"""スコア."""
def __init__(self, score: int) -> None:
if score < 0:
raise ValueError
self._score = score
def add(self, score: Score) -> None:
"""加算."""
self._score += score.value
@property
def value(self) -> int:
"""値の取得."""
return self._score
def __str__(self):
return str(self._score)
def __eq__(self, other):
if isinstance(other, Score):
return self.value == other.value
return self.value == other
まとめ
既存のコードをリファクタリングしていく過程をお伝えしました。
値オブジェクトとかは正直やり過ぎな気もしますが、考え方の一つとして参考にしていただければと思います。
コメント