【リファクタリングカタログ】スーパークラスの抽出


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

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

これは何

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

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

カタログ:Extract Superclass

当初コード、JavaScript版
class Department {
  get totalAnnualCost() {...}
  get name() {...}
  get headCount() {...}
}

class Employee {
  get annualCost() {...}
  get name() {...}
  get id() {...}
}
リファクタリング後
class Party {
  get name() {...}
  get annualCost() {...}
}

class Department extends Party {
  get annualCost() {...}
  get headCount() {...}
}

class Employee extends Party {
  get annualCost() {...}
  get id() {...}
}

Python版

当初コード
class Department:
    def totalAnnualCost(self):
        ...

と行きたいところですが、書籍を見ると、Employee, Department で具体説明をしていて、ぐぬぬ、という訳で改めまして

当初コード、JavaScript版、改
class Employee {
  constructor(name, id, monthlyCost) {
    this._id = id;
    this._name = name;
    this._monthlyCost = monthlyCost;
  }
  get monthlyCost() {return this._monthlyCost;}
  get name() {return this._name;}
  get id() {return this._id;}

  get annualCost() {
    return this.monthlyCost * 12;
  }
}
class Department {
  constructor(name, staff){
    this._name = name;
    this._staff = staff;
  }
  get staff() {return this._staff.slice();}
  get name() {return this._name;}

  get totalMonthlyCost() {
    return this.staff
      .map(e => e.monthlyCost)
      .reduce((sum, cost) => sum + cost);
  }
  get headCount() {
    return this.staff.length;
  }
  get totalAnnualCost() {
    return this.totalMonthlyCost * 12;
  }
}

という訳で初期コードはつぎのようになります

Python版、改

当初コード
from dataclasses import dataclass
from functools import reduce
from operator import add


@dataclass
class Employee:
    __name: str
    __id: str
    __monthlyCost: int

    @property
    def name(self):
        return self.__name

    @property
    def id(self):
        return self.__id

    @property
    def monthlyCost(self):
        return self.__monthlyCost

    @property
    def annualCost(self):
        return self.monthlyCost * 12


@dataclass
class Department:
    __name: str
    __staff: list

    @property
    def name(self):
        return self.__name

    @property
    def staff(self):
        return self.__staff[:]

    @property
    def totalMonthlyCost(self):
        return reduce(add, (e.monthlyCost for e in self.staff))

    @property
    def headCount(self):
        return len(self.staff)

    @property
    def totalAnnualCost(self):
        return self.totalMonthlyCost * 12

テストコード

テストコード
from unittest import TestCase


class TestEmployee(TestCase):
    def setUp(self):
        self.target = Employee("foo bar", "baz", 3000)

    def test_monthlyCost(self):
        self.assertEqual(self.target.monthlyCost, 3000)

    def test_name(self):
        self.assertEqual(self.target.name, "foo bar")

    def test_id(self):
        self.assertEqual(self.target.id, "baz")

    def test_annualCost(self):
        self.assertEqual(self.target.annualCost, 36000)


class TestDepartment(TestCase):
    def setUp(self):
        self.target = Department("hanya", [Employee("foo bar", "baz", 3000)])

    def test_staff(self):
        self.assertEqual(self.target.staff, [Employee("foo bar", "baz", 3000)])

    def test_name(self):
        self.assertEqual(self.target.name, "hanya")

    def test_totalMonthlyCost(self):
        self.assertEqual(self.target.totalMonthlyCost, 3000)

    def test_headCount(self):
        self.assertEqual(self.target.headCount, 1)

    def test_totalAnnualCost(self):
        self.assertEqual(self.target.totalAnnualCost, 36000)

ここをこうこうこう

空のスーパークラスを作成、元クラス群をサブクラスにする
from dataclasses import dataclass
from functools import reduce
from operator import add


@dataclass  # add
class Party:  # add
    pass  # add


@dataclass
class Employee(Party):  # edit
    __name: str
    __id: str
    __monthlyCost: int

    @property
    def name(self):
        return self.__name

    @property
    def id(self):
        return self.__id

    @property
    def monthlyCost(self):
        return self.__monthlyCost

    @property
    def annualCost(self):
        return self.monthlyCost * 12


@dataclass
class Department(Party):  # edit
    __name: str
    __staff: list

    @property
    def name(self):
        return self.__name

    @property
    def staff(self):
        return self.__staff[:]

    @property
    def totalMonthlyCost(self):
        return reduce(add, (e.monthlyCost for e in self.staff))

    @property
    def headCount(self):
        return len(self.staff)

    @property
    def totalAnnualCost(self):
        return self.totalMonthlyCost * 12
変数名の変更
from dataclasses import dataclass
from functools import reduce
from operator import add


@dataclass
class Party:
    pass


@dataclass
class Employee(Party):
    _name: str  # edit
    __id: str
    __monthlyCost: int

    @property
    def name(self):
        return self._name  # edit

    @property
    def id(self):
        return self.__id

    @property
    def monthlyCost(self):
        return self.__monthlyCost

    @property
    def annualCost(self):
        return self.monthlyCost * 12


@dataclass
class Department(Party):
    _name: str  # edit
    __staff: list

    @property
    def name(self):
        return self._name  # edit

    @property
    def staff(self):
        return self.__staff[:]

    @property
    def totalMonthlyCost(self):
        return reduce(add, (e.monthlyCost for e in self.staff))

    @property
    def headCount(self):
        return len(self.staff)

    @property
    def totalAnnualCost(self):
        return self.totalMonthlyCost * 12
フィールドの引き上げ
from dataclasses import dataclass
from functools import reduce
from operator import add


@dataclass
class Party:
    _name: str  # add


@dataclass
class Employee(Party):
    # _name: str  # del
    __id: str
    __monthlyCost: int

    @property
    def name(self):
        return self._name

    @property
    def id(self):
        return self.__id

    @property
    def monthlyCost(self):
        return self.__monthlyCost

    @property
    def annualCost(self):
        return self.monthlyCost * 12


@dataclass
class Department(Party):
    # _name: str  # del
    __staff: list

    @property
    def name(self):
        return self._name

    @property
    def staff(self):
        return self.__staff[:]

    @property
    def totalMonthlyCost(self):
        return reduce(add, (e.monthlyCost for e in self.staff))

    @property
    def headCount(self):
        return len(self.staff)

    @property
    def totalAnnualCost(self):
        return self.totalMonthlyCost * 12
メソッドの引き上げ
from dataclasses import dataclass
from functools import reduce
from operator import add


@dataclass
class Party:
    _name: str

    @property  # add
    def name(self):  # add
        return self._name  # add


@dataclass
class Employee(Party):
    __id: str
    __monthlyCost: int

    # @property  # del
    # def name(self):  # del
    #     return self._name  # del

    @property
    def id(self):
        return self.__id

    @property
    def monthlyCost(self):
        return self.__monthlyCost

    @property
    def annualCost(self):
        return self.monthlyCost * 12


@dataclass
class Department(Party):
    __staff: list

    # @property  # del
    # def name(self):  # del
    #     return self._name  # del

    @property
    def staff(self):
        return self.__staff[:]

    @property
    def totalMonthlyCost(self):
        return reduce(add, (e.monthlyCost for e in self.staff))

    @property
    def headCount(self):
        return len(self.staff)

    @property
    def totalAnnualCost(self):
        return self.totalMonthlyCost * 12
変数名の変更
from dataclasses import dataclass
from functools import reduce
from operator import add


@dataclass
class Party:
    __name: str  # edit

    @property
    def name(self):
        return self.__name  # edit


@dataclass
class Employee(Party):
    __id: str
    __monthlyCost: int

    @property
    def id(self):
        return self.__id

    @property
    def monthlyCost(self):
        return self.__monthlyCost

    @property
    def annualCost(self):
        return self.monthlyCost * 12


@dataclass
class Department(Party):
    __staff: list

    @property
    def staff(self):
        return self.__staff[:]

    @property
    def totalMonthlyCost(self):
        return reduce(add, (e.monthlyCost for e in self.staff))

    @property
    def headCount(self):
        return len(self.staff)

    @property
    def totalAnnualCost(self):
        return self.totalMonthlyCost * 12
関数宣言の変更の準備、テスト
from unittest import TestCase


class TestEmployee(TestCase):
    def setUp(self):
        self.target = Employee("foo bar", "baz", 3000)

    def test_monthlyCost(self):
        self.assertEqual(self.target.monthlyCost, 3000)

    def test_name(self):
        self.assertEqual(self.target.name, "foo bar")

    def test_id(self):
        self.assertEqual(self.target.id, "baz")

    def test_annualCost(self):
        self.assertEqual(self.target.annualCost, 36000)


class TestDepartment(TestCase):
    def setUp(self):
        self.target = Department("hanya", [Employee("foo bar", "baz", 3000)])

    def test_staff(self):
        self.assertEqual(self.target.staff, [Employee("foo bar", "baz", 3000)])

    def test_name(self):
        self.assertEqual(self.target.name, "hanya")

    def test_monthlyCost(self):  # edit
        self.assertEqual(self.target.monthlyCost, 3000)  # edit

    def test_headCount(self):
        self.assertEqual(self.target.headCount, 1)

    def test_totalAnnualCost(self):
        self.assertEqual(self.target.totalAnnualCost, 36000)
関数宣言の変更
from dataclasses import dataclass
from functools import reduce
from operator import add


@dataclass
class Party:
    __name: str

    @property
    def name(self):
        return self.__name


@dataclass
class Employee(Party):
    __id: str
    __monthlyCost: int

    @property
    def id(self):
        return self.__id

    @property
    def monthlyCost(self):
        return self.__monthlyCost

    @property
    def annualCost(self):
        return self.monthlyCost * 12


@dataclass
class Department(Party):
    __staff: list

    @property
    def staff(self):
        return self.__staff[:]

    @property
    def monthlyCost(self):  # edit
        return reduce(add, (e.monthlyCost for e in self.staff))

    @property
    def headCount(self):
        return len(self.staff)

    @property
    def totalAnnualCost(self):
        return self.monthlyCost * 12  # edit
関数宣言の変更の準備、テスト
from unittest import TestCase


class TestEmployee(TestCase):
    def setUp(self):
        self.target = Employee("foo bar", "baz", 3000)

    def test_monthlyCost(self):
        self.assertEqual(self.target.monthlyCost, 3000)

    def test_name(self):
        self.assertEqual(self.target.name, "foo bar")

    def test_id(self):
        self.assertEqual(self.target.id, "baz")

    def test_annualCost(self):
        self.assertEqual(self.target.annualCost, 36000)


class TestDepartment(TestCase):
    def setUp(self):
        self.target = Department("hanya", [Employee("foo bar", "baz", 3000)])

    def test_staff(self):
        self.assertEqual(self.target.staff, [Employee("foo bar", "baz", 3000)])

    def test_name(self):
        self.assertEqual(self.target.name, "hanya")

    def test_monthlyCost(self):
        self.assertEqual(self.target.monthlyCost, 3000)

    def test_headCount(self):
        self.assertEqual(self.target.headCount, 1)

    def test_annualCost(self):  # edit
        self.assertEqual(self.target.annualCost, 36000)  # edit
関数宣言の変更
from dataclasses import dataclass
from functools import reduce
from operator import add


@dataclass
class Party:
    __name: str

    @property
    def name(self):
        return self.__name


@dataclass
class Employee(Party):
    __id: str
    __monthlyCost: int

    @property
    def id(self):
        return self.__id

    @property
    def monthlyCost(self):
        return self.__monthlyCost

    @property
    def annualCost(self):
        return self.monthlyCost * 12


@dataclass
class Department(Party):
    __staff: list

    @property
    def staff(self):
        return self.__staff[:]

    @property
    def monthlyCost(self):
        return reduce(add, (e.monthlyCost for e in self.staff))

    @property
    def headCount(self):
        return len(self.staff)

    @property
    def annualCost(self):  # edit
        return self.monthlyCost * 12
メソッドの引き上げ
from dataclasses import dataclass
from functools import reduce
from operator import add


@dataclass
class Party:
    __name: str

    @property
    def name(self):
        return self.__name

    @property  # add
    def annualCost(self):  # add
        return self.monthlyCost * 12  # add


@dataclass
class Employee(Party):
    __id: str
    __monthlyCost: int

    @property
    def id(self):
        return self.__id

    @property
    def monthlyCost(self):
        return self.__monthlyCost

    # @property  # del
    # def annualCost(self):  # del
    #     return self.monthlyCost * 12  # del


@dataclass
class Department(Party):
    __staff: list

    @property
    def staff(self):
        return self.__staff[:]

    @property
    def monthlyCost(self):
        return reduce(add, (e.monthlyCost for e in self.staff))

    @property
    def headCount(self):
        return len(self.staff)

    # @property  # del
    # def annualCost(self):  # del
    #     return self.monthlyCost * 12  # del

クラス Party は関数 annualCost が自身に存在しない変数 monthlyCost を参照してます、ので、リファクタリング手順外ですが、ちょっと追加しておきます

NotImplementedError
from dataclasses import dataclass
from functools import reduce
from operator import add


@dataclass
class Party:
    __name: str

    @property
    def name(self):
        return self.__name

    @property  # add
    def monthlyCost(self):  # add
        raise NotImplementedError  # add

    @property
    def annualCost(self):
        return self.monthlyCost * 12


@dataclass
class Employee(Party):
    __id: str
    __monthlyCost: int

    @property
    def id(self):
        return self.__id

    @property
    def monthlyCost(self):
        return self.__monthlyCost


@dataclass
class Department(Party):
    __staff: list

    @property
    def staff(self):
        return self.__staff[:]

    @property
    def monthlyCost(self):
        return reduce(add, (e.monthlyCost for e in self.staff))

    @property
    def headCount(self):
        return len(self.staff)

出来上がり

変数名の変更や関数宣言の変更はエディタのリファクタリング機能が助けてくれることもあると思います

以上

参考

(Youtube)マーチンファウラーによろしく - リファクタリングカタログ - スーパークラスの抽出 @ Tommy109