【リファクタリングカタログ】関数の抽出

## 書籍 [リファクタリング―既存のコードを安全に改善する 第2版](https://www.yodobashi.com/product/100000009003204206/)
または[Web版](https://martinfowler.com/articles/access-refactoring-web-edition.html)（の方が完全版なんですが）には、
これを補完する [リファクタリングカタログ](https://refactoring.com/catalog/) が公開されています
## これは何
書籍およびカタログはサンプルコードが JavaScript です、
カタログをもとに Python でリファクタリングのサンプルコードを示します

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

## カタログ：Extract Function
https://refactoring.com/catalog/extractFunction.html

```javascript:当初コード、JavaScript版
function printOwing(invoice) {
  printBanner();
  let outstanding  = calculateOutstanding();

  //print details
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${outstanding}`);  
}
```
```javascript:リファクタリング後
function printOwing(invoice) {
  printBanner();
  let outstanding  = calculateOutstanding();
  printDetails(outstanding);

  function printDetails(outstanding) {
    console.log(`name: ${invoice.customer}`);
    console.log(`amount: ${outstanding}`);
  }
}
```
### Python版
```python:当初コード
from logging import getLogger


logger = getLogger(__name__)


def printOwing(invoice):
    printBanner()
    outstanding = calculateOutstanding()

    #   print details
    logger.debug(f"name: {invoice.customer}")
    logger.debug(f"amount: {outstanding}")
```
と行きたいところですが、書籍を見ると、printBanner, calculateOutstanding の導出前から始まっていて、ぐぬぬ、という訳で改めまして
さらに書籍には無い具体コードを参考動画から拝借しまして

```javascript:当初コード、JavaScript版、改
function printOwing(invoice) {
    let outstanding = 0;

    console.log("***********************");
    console.log("**** Customer Owes ****");
    console.log("***********************");

    // calculate outstanding
    for (const o of invoice.orders) {
        outstanding += o.amount;
    }

    // record due date
    const today = Clock.today;
    invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);

    //print details
    console.log(`name: ${invoice.customer}`);
    console.log(`amount: ${outstanding}`);
    console.log(`due: ${invoice.dueDate.toLocaleDateString()}`);
}

class Clock {
    static get today() {return new Date(2021, 0, 30)}
}
```
とはいえ [Clock の狙いはテスト時にオーバライドすること (bliki)](https://martinfowler.com/bliki/ClockWrapper.html) なので、Pythonコードとしてはもうちょい素直に datetime.now を返しておきます
という訳で初期コードはつぎのようになります
### Python版、改
```python:当初コード
from dataclasses import dataclass
from dataclasses import field
from datetime import date
from datetime import datetime
from datetime import timedelta
from logging import getLogger

logger = getLogger(__name__)


def printOwing(invoice):
    outstanding = 0
    logger.debug("***********************")
    logger.debug("**** Customer Owes ****")
    logger.debug("***********************")

    # calculate outstanding
    for o in invoice.orders:
        outstanding += o.amount

    # record due date
    today: datetime = Clock.today
    invoice.dueDate = date(today.year, today.month, today.day) + timedelta(days=30)

    #   print details
    logger.debug(f"name: {invoice.customer}")
    logger.debug(f"amount: {outstanding}")
    logger.debug(f"due: {invoice.dueDate}")


@dataclass
class Clock:
    today: datetime = datetime.now()


@dataclass
class Order:
    amount: int = 0


@dataclass
class Invoice:
    customer: str = ""
    orders: list[Order] = field(default_factory=list)
    dueDate: datetime = Clock()
```

### テストコード
```python:テストコード
from unittest import TestCase
from unittest.mock import patch
from logging import DEBUG


class TestPrintOwing(TestCase):
    @patch.object(Clock, "today", datetime(2001, 3, 1))
    def test_printOwing(self):
        _invoice = Invoice(customer="a", orders=[Order(amount=10), Order(amount=2)])
        with self.assertLogs(level=DEBUG) as cm:
            printOwing(_invoice)

        def __logger_debug(message):
            prefix = f"{cm.records[0].levelname}:{cm.records[0].name}:"
            return f"{prefix}{message}"

        expected = [
            "***********************",
            "**** Customer Owes ****",
            "***********************",
            "name: a",
            "amount: 12",
            "due: 2001-03-31",
        ]

        self.assertEqual(cm.output, [__logger_debug(e) for e in expected])
```

### ここをこうこうこう
```python:コピペで関数printBannerをつくる
from dataclasses import dataclass
from dataclasses import field
from datetime import date
from datetime import datetime
from datetime import timedelta
from logging import getLogger

logger = getLogger(__name__)


def printOwing(invoice):
    outstanding = 0
    logger.debug("***********************")
    logger.debug("**** Customer Owes ****")
    logger.debug("***********************")

    # calculate outstanding
    for o in invoice.orders:
        outstanding += o.amount

    # record due date
    today: datetime = Clock.today
    invoice.dueDate = date(today.year, today.month, today.day) + timedelta(days=30)

    #   print details
    logger.debug(f"name: {invoice.customer}")
    logger.debug(f"amount: {outstanding}")
    logger.debug(f"due: {invoice.dueDate}")


def printBanner(): # add
    logger.debug("***********************") # add
    logger.debug("**** Customer Owes ****") # add
    logger.debug("***********************") # add


@dataclass
class Clock:
    today: datetime = datetime.now()


@dataclass
class Order:
    amount: int = 0


@dataclass
class Invoice:
    customer: str = ""
    orders: list[Order] = field(default_factory=list)
    dueDate: datetime = Clock()
```

```python:関数printBannerを利用する
from dataclasses import dataclass
from dataclasses import field
from datetime import date
from datetime import datetime
from datetime import timedelta
from logging import getLogger

logger = getLogger(__name__)


def printOwing(invoice):
    outstanding = 0
    # logger.debug("***********************") # del
    # logger.debug("**** Customer Owes ****") # del
    # logger.debug("***********************") # del
    printBanner()  # add

    # calculate outstanding
    for o in invoice.orders:
        outstanding += o.amount

    # record due date
    today: datetime = Clock.today
    invoice.dueDate = date(today.year, today.month, today.day) + timedelta(days=30)

    #   print details
    logger.debug(f"name: {invoice.customer}")
    logger.debug(f"amount: {outstanding}")
    logger.debug(f"due: {invoice.dueDate}")


def printBanner():
    logger.debug("***********************")
    logger.debug("**** Customer Owes ****")
    logger.debug("***********************")


@dataclass
class Clock:
    today: datetime = datetime.now()


@dataclass
class Order:
    amount: int = 0


@dataclass
class Invoice:
    customer: str = ""
    orders: list[Order] = field(default_factory=list)
    dueDate: datetime = Clock()
```
”関数の抽出”できあがり

```python:同様にコピペで関数printDetailsをつくる、利用する
from dataclasses import dataclass
from dataclasses import field
from datetime import date
from datetime import datetime
from datetime import timedelta
from logging import getLogger

logger = getLogger(__name__)


def printOwing(invoice):
    outstanding = 0
    printBanner()

    # calculate outstanding
    for o in invoice.orders:
        outstanding += o.amount

    # record due date
    today: datetime = Clock.today
    invoice.dueDate = date(today.year, today.month, today.day) + timedelta(days=30)

    #   print details
    # logger.debug(f"name: {invoice.customer}")  # del
    # logger.debug(f"amount: {outstanding}")  # del
    # logger.debug(f"due: {invoice.dueDate}")  # del
    printDetails(invoice, outstanding)  # add


def printBanner():
    logger.debug("***********************")
    logger.debug("**** Customer Owes ****")
    logger.debug("***********************")


def printDetails(invoice, outstanding):  # add
    logger.debug(f"name: {invoice.customer}")  # add
    logger.debug(f"amount: {outstanding}")  # add
    logger.debug(f"due: {invoice.dueDate}")  # add


@dataclass
class Clock:
    today: datetime = datetime.now()


@dataclass
class Order:
    amount: int = 0


@dataclass
class Invoice:
    customer: str = ""
    orders: list[Order] = field(default_factory=list)
    dueDate: datetime = Clock()
```
”関数の抽出”の引数がある版もできあがり

```python:変数outstandingを”ステートメントのスライド”する
from dataclasses import dataclass
from dataclasses import field
from datetime import date
from datetime import datetime
from datetime import timedelta
from logging import getLogger

logger = getLogger(__name__)


def printOwing(invoice):
    printBanner()

    # calculate outstanding
    outstanding = 0  # edit
    for o in invoice.orders:
        outstanding += o.amount

    # record due date
    today: datetime = Clock.today
    invoice.dueDate = date(today.year, today.month, today.day) + timedelta(days=30)

    #   print details
    printDetails(invoice, outstanding)


def printBanner():
    logger.debug("***********************")
    logger.debug("**** Customer Owes ****")
    logger.debug("***********************")


def printDetails(invoice, outstanding):
    logger.debug(f"name: {invoice.customer}")
    logger.debug(f"amount: {outstanding}")
    logger.debug(f"due: {invoice.dueDate}")


@dataclass
class Clock:
    today: datetime = datetime.now()


@dataclass
class Order:
    amount: int = 0


@dataclass
class Invoice:
    customer: str = ""
    orders: list[Order] = field(default_factory=list)
    dueDate: datetime = Clock()
```
[カタログ：ステートメントのスライド](https://refactoring.com/catalog/slideStatements.html)

```python:コピペで関数calculateOutstandingをつくる
from dataclasses import dataclass
from dataclasses import field
from datetime import date
from datetime import datetime
from datetime import timedelta
from logging import getLogger

logger = getLogger(__name__)


def printOwing(invoice):
    printBanner()

    # calculate outstanding
    outstanding = 0
    for o in invoice.orders:
        outstanding += o.amount

    # record due date
    today: datetime = Clock.today
    invoice.dueDate = date(today.year, today.month, today.day) + timedelta(days=30)

    #   print details
    printDetails(invoice, outstanding)


def printBanner():
    logger.debug("***********************")
    logger.debug("**** Customer Owes ****")
    logger.debug("***********************")


def printDetails(invoice, outstanding):
    logger.debug(f"name: {invoice.customer}")
    logger.debug(f"amount: {outstanding}")
    logger.debug(f"due: {invoice.dueDate}")


def calculateOutstanding(invoice):  # add
    outstanding = 0  # add
    for o in invoice.orders:  # add
        outstanding += o.amount  # add
    return outstanding  # add


@dataclass
class Clock:
    today: datetime = datetime.now()


@dataclass
class Order:
    amount: int = 0


@dataclass
class Invoice:
    customer: str = ""
    orders: list[Order] = field(default_factory=list)
    dueDate: datetime = Clock()
```

```python:関数calculateOutstandingを利用する
from dataclasses import dataclass
from dataclasses import field
from datetime import date
from datetime import datetime
from datetime import timedelta
from logging import getLogger

logger = getLogger(__name__)


def printOwing(invoice):
    printBanner()

    # calculate outstanding
    # outstanding = 0  # del
    # for o in invoice.orders:  # del
    #     outstanding += o.amount  # del
    outstanding = calculateOutstanding(invoice)  # add

    # record due date
    today: datetime = Clock.today
    invoice.dueDate = date(today.year, today.month, today.day) + timedelta(days=30)

    #   print details
    printDetails(invoice, outstanding)


def printBanner():
    logger.debug("***********************")
    logger.debug("**** Customer Owes ****")
    logger.debug("***********************")


def printDetails(invoice, outstanding):
    logger.debug(f"name: {invoice.customer}")
    logger.debug(f"amount: {outstanding}")
    logger.debug(f"due: {invoice.dueDate}")


def calculateOutstanding(invoice):
    outstanding = 0
    for o in invoice.orders:
        outstanding += o.amount
    return outstanding


@dataclass
class Clock:
    today: datetime = datetime.now()


@dataclass
class Order:
    amount: int = 0


@dataclass
class Invoice:
    customer: str = ""
    orders: list[Order] = field(default_factory=list)
    dueDate: datetime = Clock()
```
”関数の抽出”の戻り値がある版もできあがり

```python:コピペで関数recordDueDateをつくる
from dataclasses import dataclass
from dataclasses import field
from datetime import date
from datetime import datetime
from datetime import timedelta
from logging import getLogger

logger = getLogger(__name__)


def printOwing(invoice):
    printBanner()

    # calculate outstanding
    outstanding = calculateOutstanding(invoice)

    # record due date
    today: datetime = Clock.today
    invoice.dueDate = date(today.year, today.month, today.day) + timedelta(days=30)

    #   print details
    printDetails(invoice, outstanding)


def printBanner():
    logger.debug("***********************")
    logger.debug("**** Customer Owes ****")
    logger.debug("***********************")


def printDetails(invoice, outstanding):
    logger.debug(f"name: {invoice.customer}")
    logger.debug(f"amount: {outstanding}")
    logger.debug(f"due: {invoice.dueDate}")


def calculateOutstanding(invoice):
    outstanding = 0
    for o in invoice.orders:
        outstanding += o.amount
    return outstanding


def recordDueDate(invoice):  # add
    today: datetime = Clock.today  # add
    invoice.dueDate = date(today.year, today.month, today.day) + timedelta(
        days=30
    )  # add


@dataclass
class Clock:
    today: datetime = datetime.now()


@dataclass
class Order:
    amount: int = 0


@dataclass
class Invoice:
    customer: str = ""
    orders: list[Order] = field(default_factory=list)
    dueDate: datetime = Clock()
```

```python:関数recordDueDateを利用する
from dataclasses import dataclass
from dataclasses import field
from datetime import date
from datetime import datetime
from datetime import timedelta
from logging import getLogger

logger = getLogger(__name__)


def printOwing(invoice):
    printBanner()

    # calculate outstanding
    outstanding = calculateOutstanding(invoice)

    # record due date
    # today: datetime = Clock.today  # del
    # invoice.dueDate = date(today.year, today.month, today.day) + timedelta(days=30)  # del
    recordDueDate(invoice)  # add

    #   print details
    printDetails(invoice, outstanding)


def printBanner():
    logger.debug("***********************")
    logger.debug("**** Customer Owes ****")
    logger.debug("***********************")


def printDetails(invoice, outstanding):
    logger.debug(f"name: {invoice.customer}")
    logger.debug(f"amount: {outstanding}")
    logger.debug(f"due: {invoice.dueDate}")


def calculateOutstanding(invoice):
    outstanding = 0
    for o in invoice.orders:
        outstanding += o.amount
    return outstanding


def recordDueDate(invoice):
    today: datetime = Clock.today
    invoice.dueDate = date(today.year, today.month, today.day) + timedelta(days=30)


@dataclass
class Clock:
    today: datetime = datetime.now()


@dataclass
class Order:
    amount: int = 0


@dataclass
class Invoice:
    customer: str = ""
    orders: list[Order] = field(default_factory=list)
    dueDate: datetime = Clock()
```

```python:コメントが関数名と同じなので掃除、そして関数の順序入れ替え
from dataclasses import dataclass
from dataclasses import field
from datetime import date
from datetime import datetime
from datetime import timedelta
from logging import getLogger

logger = getLogger(__name__)


def printOwing(invoice):
    printBanner()
    outstanding = calculateOutstanding(invoice)
    recordDueDate(invoice)
    printDetails(invoice, outstanding)


def printBanner():
    logger.debug("***********************")
    logger.debug("**** Customer Owes ****")
    logger.debug("***********************")


def calculateOutstanding(invoice):
    outstanding = 0
    for o in invoice.orders:
        outstanding += o.amount
    return outstanding


def recordDueDate(invoice):
    today: datetime = Clock.today
    invoice.dueDate = date(today.year, today.month, today.day) + timedelta(days=30)


def printDetails(invoice, outstanding):
    logger.debug(f"name: {invoice.customer}")
    logger.debug(f"amount: {outstanding}")
    logger.debug(f"due: {invoice.dueDate}")


@dataclass
class Clock:
    today: datetime = datetime.now()


@dataclass
class Order:
    amount: int = 0


@dataclass
class Invoice:
    customer: str = ""
    orders: list[Order] = field(default_factory=list)
    dueDate: datetime = Clock()
```

```python:おまけ、calculateOutstandingは一行で書ける
from dataclasses import dataclass
from dataclasses import field
from datetime import date
from datetime import datetime
from datetime import timedelta
from logging import getLogger

logger = getLogger(__name__)


def printOwing(invoice):
    printBanner()
    outstanding = calculateOutstanding(invoice)
    recordDueDate(invoice)
    printDetails(invoice, outstanding)


def printBanner():
    logger.debug("***********************")
    logger.debug("**** Customer Owes ****")
    logger.debug("***********************")


def calculateOutstanding(invoice):
    return sum(o.amount for o in invoice.orders)  # add
    # outstanding = 0  # del
    # for o in invoice.orders:  # del
    #     outstanding += o.amount  # del
    # return outstanding  # del


def recordDueDate(invoice):
    today: datetime = Clock.today
    invoice.dueDate = date(today.year, today.month, today.day) + timedelta(days=30)


def printDetails(invoice, outstanding):
    logger.debug(f"name: {invoice.customer}")
    logger.debug(f"amount: {outstanding}")
    logger.debug(f"due: {invoice.dueDate}")


@dataclass
class Clock:
    today: datetime = datetime.now()


@dataclass
class Order:
    amount: int = 0


@dataclass
class Invoice:
    customer: str = ""
    orders: list[Order] = field(default_factory=list)
    dueDate: datetime = Clock()
```

```python:calculateOutstandingは”関数のインライン化”する
from dataclasses import dataclass
from dataclasses import field
from datetime import date
from datetime import datetime
from datetime import timedelta
from logging import getLogger

logger = getLogger(__name__)


def printOwing(invoice):
    printBanner()
    # calculate outstanding
    outstanding = sum(o.amount for o in invoice.orders)
    recordDueDate(invoice)
    printDetails(invoice, outstanding)


def printBanner():
    logger.debug("***********************")
    logger.debug("**** Customer Owes ****")
    logger.debug("***********************")


def recordDueDate(invoice):
    today: datetime = Clock.today
    invoice.dueDate = date(today.year, today.month, today.day) + timedelta(days=30)


def printDetails(invoice, outstanding):
    logger.debug(f"name: {invoice.customer}")
    logger.debug(f"amount: {outstanding}")
    logger.debug(f"due: {invoice.dueDate}")


@dataclass
class Clock:
    today: datetime = datetime.now()


@dataclass
class Order:
    amount: int = 0


@dataclass
class Invoice:
    customer: str = ""
    orders: list[Order] = field(default_factory=list)
    dueDate: datetime = Clock()
```
[カタログ：関数のインライン化](https://refactoring.com/catalog/inlineFunction.html)

### 出来上がり
この例のような変更はエディタのリファクタリング機能が助けてくれることもあると思います



以上

## 参考
(Youtube)マーチンファウラーによろしく - リファクタリングカタログ - 関数の抽出 @ Tommy109
<iframe width="560" height="315" src="https://www.youtube.com/embed/SyjZYgD5igE" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>