0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【リファクタリングカタログ】参照から値への変更

Last updated at Posted at 2021-01-10

書籍 リファクタリング―既存のコードを安全に改善する 第2版

またはWeb版(の方が完全版なんですが)には、
これを補完する リファクタリングカタログ が公開されています

これは何

書籍およびカタログはサンプルコードが JavaScript です、
カタログをもとに Python でリファクタリングのサンプルコードを示します

詳細解説は本にお任せして、「ここをこうこうこう」くらいのノリで示します

カタログ:Change Reference to Value

当初コード、JavaScript版
class Product {
  applyDiscount(arg) {this._price.amount -= arg;}
リファクタリング後
class Product {
  applyDiscount(arg) {
    this._price = new Money(this._price.amount - arg, this._price.currency);
  }

Python版

当初コード
class Product():
  def applyDiscount(self, arg):
    self._price.amount -= arg

と行きたいところですが、_price をどう定義していこうかなと書籍を見ると、class Person とclass TelephoneNumber で具体説明をしていて、ぐぬぬ、という訳で改めまして

当初コード、JavaScript版、改
class Person{
  constructor() {
    this._telephoneNumber = new TelephoneNumber();
  }
  get officeAreaCode()    {return this._telephoneNumber.areaCode;}
  set officeAreaCode(arg) {this._telephoneNumber.areaCode = arg;}
  get officeNumber()    {return this._telephoneNumber.number;}
  set officeNumber(arg) {this._telephoneNumber.number = arg;}  
}

class TelephoneNumber{
  get areaCode()    {return this._areaCode;}
  set areaCode(arg) {this._areaCode = arg;}
  get number()    {return this._number;}
  set number(arg) {this._number = arg;} 
}
リファクタリング後
class Person{
  constructor() {
    this._telephoneNumber = new TelephoneNumber();
  }
  get officeAreaCode()    {return this._telephoneNumber.areaCode;}
  set officeAreaCode(arg) {
    this._telephoneNumber = new TelephoneNumber(arg, this.officeNumber);
  }
  get officeNumber()    {return this._telephoneNumber.number;}
  set officeNumber(arg) {
    this._telephoneNumber = new TelephoneNumber(this.officeAreaCode, arg);
  }
}

class TelephoneNumber{
  constructor(areaCode, number) {
    this._areaCode = areaCode;
    this._number = number;
  }
  get areaCode()    {return this._areaCode;}
  set areaCode(arg) {this._areaCode = arg;}
  get number()    {return this._number;}
  set number(arg) {this._number = arg;} 
}

Python版、改

当初コード
class Person():
  def __init__(self):
    self.__telephoneNumber = TelephoneNumber()

  @property
  def officeAreaCode(self):
    return self.__telephoneNumber.areaCode
  @officeAreaCode.setter
  def officeAreaCode(self, arg):
    self.__telephoneNumber.areaCode = arg

  @property
  def officeNumber(self):
    return self.__telephoneNumber.number
  @officeNumber.setter
  def officeNumber(self, arg):
    self.__telephoneNumber.number = arg

class TelephoneNumber():
  def __init__(self):
    self.__areaCode = ''
    self.__number = ''

  @property
  def areaCode(self):
    return self.__areaCode
  @areaCode.setter
  def areaCode(self, arg):
    self.__areaCode = arg

  @property
  def number(self):
    return self.__number
  @number.setter
  def number(self, arg):
    self.__number = arg

テストコード

テストコード
from unittest import TestCase
class TestPerson(TestCase):
  target = Person()

  def test_officeAreaCode(self):
    self.target.officeAreaCode = '312'
    self.assertEqual(self.target.officeAreaCode, '312')

  def test_officeNumber(self):
    self.target.officeNumber = '555-0142'
    self.assertEqual(self.target.officeNumber, '555-0142')

class TestTelephoneNumber(TestCase):
  target = TelephoneNumber()

  def test_areaCode(self):
    self.target.areaCode = '312'
    self.assertEqual(self.target.areaCode, '312')

  def test_number(self):
    self.target.number = '555-0142'
    self.assertEqual(self.target.number, '555-0142')

ここをこうこうこう

セッター除去の準備、コンストラクタにパラメータを追加、テスト追加
class TestTelephoneNumber(TestCase):
  target = TelephoneNumber()

  def test_areaCode(self):
    self.target.areaCode = '312'
    self.assertEqual(self.target.areaCode, '312')

  def test_number(self):
    self.target.number = '555-0142'
    self.assertEqual(self.target.number, '555-0142')
  
  def test_telephoneNumber(self): # add
    target = TelephoneNumber('312', '555-0142') # add
    self.assertEqual(target.areaCode, '312') # add
    self.assertEqual(target.number, '555-0142') # add
セッター除去の準備、変数を抽出
class TelephoneNumber():
  def __init__(self, areaCode='', number=''): # edit
    self.__areaCode = areaCode # edit
    self.__number = number # edit

  @property
  def areaCode(self):
    return self.__areaCode
  @areaCode.setter
  def areaCode(self, arg):
    self.__areaCode = arg

  @property
  def number(self):
    return self.__number
  @number.setter
  def number(self, arg):
    self.__number = arg
参照から値への変更
class Person():
  def __init__(self):
      self.__telephoneNumber = TelephoneNumber()

  @property
  def officeAreaCode(self):
    return self.__telephoneNumber.areaCode
  @officeAreaCode.setter
  def officeAreaCode(self, arg):
    self.__telephoneNumber = TelephoneNumber(arg, self.officeNumber) # edit

  @property
  def officeNumber(self):
    return self.__telephoneNumber.number
  @officeNumber.setter
  def officeNumber(self, arg):
    self.__telephoneNumber = TelephoneNumber(self.officeAreaCode, arg) # edit
セッターの除去、テストの削除
class TestTelephoneNumber(TestCase):
  target = TelephoneNumber('312', '555-0142') # edit

  def test_areaCode(self):
    # self.target.areaCode = '312' # del
    self.assertEqual(self.target.areaCode, '312')

  def test_number(self):
    # self.target.number = '555-0142' # del
    self.assertEqual(self.target.number, '555-0142')
  
  # def test_telephoneNumber(self): # del
  #   target = TelephoneNumber('312', '555-0142') # del
  #   self.assertEqual(target.areaCode, '312') # del
  #   self.assertEqual(target.number, '555-0142') # del
セッターの除去
class TelephoneNumber():
  def __init__(self, areaCode='', number=''): # edit
    self.__areaCode = areaCode # edit
    self.__number = number # edit

  @property
  def areaCode(self):
    return self.__areaCode
  # @areaCode.setter # del
  # def areaCode(self, arg): # del
  #   self.__areaCode = arg # del

  @property
  def number(self):
    return self.__number
  # @number.setter # del
  # def number(self, arg): # del
  #   self.__number = arg # del

これで TestTelephoneNumber はイミュータブルになりました、
バリューオブジェクトとしては値ベースの同等性を判定できるようにしたいところです、
ここで書籍は終わっています、本記事では実装もしていきます

同等性判定、テストを追加
class TestTelephoneNumber(TestCase):
  target = TelephoneNumber('312', '555-0142')

  def test_areaCode(self):
    self.assertEqual(self.target.areaCode, '312')

  def test_number(self):
    self.assertEqual(self.target.number, '555-0142')
  
  def test_equals(self): # add
    self.assertEqual(self.target == TelephoneNumber('312', '555-0142'), True) # add
同等性判定、まずテストグリーン
class TelephoneNumber():
  def __init__(self, areaCode='', number=''):
    self.__areaCode = areaCode
    self.__number = number

  def __eq__(self, o): # add
    return True # add

  @property
  def areaCode(self):
    return self.__areaCode
  @areaCode.setter
  def areaCode(self, arg):
    self.__areaCode = arg

  @property
  def number(self):
    return self.__number
  @number.setter
  def number(self, arg):
    self.__number = arg
同等性判定、テストを追加、三角測量
class TestTelephoneNumber(TestCase):
  target = TelephoneNumber('312', '555-0142')

  def test_areaCode(self):
    self.assertEqual(self.target.areaCode, '312')

  def test_number(self):
    self.assertEqual(self.target.number, '555-0142')
  
  def test_equals(self):
    self.assertEqual(self.target == TelephoneNumber('312', '555-0142'), True)

  def test_equals_areaCode_false(self): # add
    self.assertEqual(self.target == TelephoneNumber('312x', '555-0142'), False) # add
同等性判定、__eq__をきちんと実装、areaCode
class TelephoneNumber():
  def __init__(self, areaCode='', number=''):
    self.__areaCode = areaCode
    self.__number = number

  def __eq__(self, o):
    return self.areaCode == o.areaCode # edit

  @property
  def areaCode(self):
    return self.__areaCode
  @areaCode.setter
  def areaCode(self, arg):
    self.__areaCode = arg

  @property
  def number(self):
    return self.__number
  @number.setter
  def number(self, arg):
    self.__number = arg
同等性判定、テストを追加、三角測量
class TestTelephoneNumber(TestCase):
  target = TelephoneNumber('312', '555-0142')

  def test_areaCode(self):
    self.assertEqual(self.target.areaCode, '312')

  def test_number(self):
    self.assertEqual(self.target.number, '555-0142')
  
  def test_equals(self):
    self.assertEqual(self.target == TelephoneNumber('312', '555-0142'), True)

  def test_equals_areaCode_false(self):
    self.assertEqual(self.target == TelephoneNumber('312x', '555-0142'), False)

  def test_equals_number_false(self): # add
    self.assertEqual(self.target == TelephoneNumber('312', '555-0142x'), False) # add
同等性判定、__eq__をきちんと実装、number
class TelephoneNumber():
  def __init__(self, areaCode='', number=''):
    self.__areaCode = areaCode
    self.__number = number

  def __eq__(self, o):
    return self.areaCode == o.areaCode and self.number == o.number # edit

  @property
  def areaCode(self):
    return self.__areaCode

  @property
  def number(self):
    return self.__number

Python 3.7 以降ではこのようなクラスはもっと簡単に実装できます

dataclass
from dataclasses import dataclass

@dataclass(frozen=True)
class TelephoneNumber():
  areaCode: str = ''
  number: str = ''

出来上がり

dataclass の採用もテストグリーンな準備をしてこそ安心して出来ますね

以上

参考

(Youtube)マーチンファウラーによろしく - リファクタリングカタログ - 参照から値への変更 @ Tommy109

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?