Neste exercício, iremos refatorar um sistema simples para aluguel de livros de uma biblioteca. Este exercício é adaptado do livro Refactoring de Martin Fowler e Kent Beck. Você deve realizar os 5 commits descritos abaixo e submeter os 5 links dos commits via Moodle.
Primeiramente, explore o código do sistema em model.py.
Note que temos três classes: Book
(livros que podem ser alugados), Rental
(dados de um aluguel) e Client
(clientes da biblioteca).
A classe Client
possui um método statement
, responsável por gerar o recibo do aluguel para o cliente:
def statement(self) -> str:
total_amount = 0
frequent_renter_points = 0
result = f"Rental summary for {self.name}\n"
for rental in self._rentals:
amount = 0
# determine amounts for each line
if rental.book.price_code == Book.REGULAR:
amount += 2
if rental.days_rented > 2:
amount += (rental.days_rented - 2) * 1.5
elif rental.book.price_code == Book.NEW_RELEASE:
amount += rental.days_rented * 3
elif rental.book.price_code == Book.CHILDREN:
amount += 1.5
if rental.days_rented > 3:
amount += (rental.days_rented - 3) * 1.5
# add frequent renter points
frequent_renter_points += 1
if rental.book.price_code == Book.NEW_RELEASE and rental.days_rented > 1:
frequent_renter_points += 1
# show each rental result
result += f"- {rental.book.title}: {amount}\n"
total_amount += amount
# show total result
result += f"Total: {total_amount}\n"
result += f"Points: {frequent_renter_points}"
return result
Reflita sobre os possíveis problemas do método statement
:
- Esse método possui muitas responsabilidades?
- Como adicionar um novo tipo filme?
- Como adicionar um novo tipo de recibo, por exemplo, HTML, CSV, JSON, etc?
Explore também os testes em tests.py para entender melhor como o sistema funciona.
Por exemplo, o teste test_rent_regular_book_short_duration
:
def test_rent_regular_book_short_duration():
book = Book("Refactoring", Book.REGULAR)
r = Rental(book, 2)
c = Client("Fulano")
c.add_rental(r)
expected_report = (
"Rental summary for Fulano\n"
"- Refactoring: 2\n"
"Total: 2\n"
"Points: 1"
)
assert c.statement() == expected_report
Você deve realizar os 5 commits descritos abaixo e submeter os 5 links dos commits via Moodle.
Antes de iniciar as atividades de refatoração, precisamos configurar o repositório de trabalho.
Primeiramente, crie um fork deste repositório.
Para isso, basta clicar no botão Fork
no canto superior direito.
Caso tenha dúvidas, verifique a documentação do GitHub sobre como criar fork de um repositório.
Neste projeto, utilizamos o GitHub Actions (ferramenta de CI/CD do GitHub) para executar os testes automaticamente a cada commit.
Abra o arquivo.github/workflows/tests.yml
e observe que os testes são executados em três sistemas operacionais (Ubuntu, macOS e Windows) e várias versões da linguagem Python. Veja um exemplo em https://github.com/andrehora/library/actions/runs/14231197771.
Ative o GitHub Actions no seu repositório.
Para isso, basta ir na aba Actions
e clicar no botão verde.
Em seguida, clone o seu repositório para uma pasta local e entre na pasta:
$ git clone https://github.com/<SEU-USUARIO>/library
$ cd library
Nossos testes utilizam o framework de testes pytest. Instale o pytest:
$ pip install pytest
Para executar os testes localmente, basta rodar o comando pytest -v tests.py
:
$ pytest -v tests.py
========================================== test session starts ==========================================
...
tests.py::test_rent_regular_book_short_duration PASSED [ 9%]
tests.py::test_rent_regular_book_long_duration PASSED [ 18%]
tests.py::test_rent_multiple_regular_books PASSED [ 27%]
tests.py::test_rent_children_book_short_duration PASSED [ 36%]
tests.py::test_rent_children_book_long_duration PASSED [ 45%]
tests.py::test_rent_multiple_children_books PASSED [ 54%]
tests.py::test_rent_new_release_book_short_duration PASSED [ 63%]
tests.py::test_rent_new_release_book_long_duration PASSED [ 72%]
tests.py::test_rent_multiple_new_release_books PASSED [ 81%]
tests.py::test_rent_distinct_books_short_duration PASSED [ 90%]
tests.py::test_rent_distinct_books_long_duration PASSED [100%]
========================================== 11 passed in 0.01s ===========================================
Os testes são executados automaticamente no GitHub Actions sempre que um commit é realizado.
Portanto, para rodar os testes no GitHub Actions, realize uma alteração qualquer neste arquivo README.md
e faça o commit da alteração com a seguinte mensagem: Commit 1: Running the tests.
Em seguida, clique na aba Actions
e veja que os testes foram executados com sucesso no GitHub Actions.
Observe as execuções em múltiplos sistemas operacionais e versões da linguagem Python.
Observe que as classes Book
, Rental
e Client
possuem 5 propriedades @property
que representam métodos getters.
Não iremos precisar dessas propriedades, portanto, remova todas as 5.
Em seguida, renomeie os 5 atributos das classes, removendo o underline (_). Por exemplo, mude de self._book
para self.book
.
Rode os testes localmente para garantir que o comportamento do sistema não foi alterado. Só faça o commit com os testes passando. Refatoração não deve quebrar os teste.
Para rodar os testes localmente, basta executar o pytest na linha comando:
$ pytest -v tests.py
========================================== test session starts ==========================================
...
tests.py::test_rent_regular_book_short_duration PASSED [ 9%]
tests.py::test_rent_regular_book_long_duration PASSED [ 18%]
tests.py::test_rent_multiple_regular_books PASSED [ 27%]
tests.py::test_rent_children_book_short_duration PASSED [ 36%]
tests.py::test_rent_children_book_long_duration PASSED [ 45%]
tests.py::test_rent_multiple_children_books PASSED [ 54%]
tests.py::test_rent_new_release_book_short_duration PASSED [ 63%]
tests.py::test_rent_new_release_book_long_duration PASSED [ 72%]
tests.py::test_rent_multiple_new_release_books PASSED [ 81%]
tests.py::test_rent_distinct_books_short_duration PASSED [ 90%]
tests.py::test_rent_distinct_books_long_duration PASSED [100%]
========================================== 11 passed in 0.01s ===========================================
Com os testes passando, faça o commit com a seguinte mensagem: Commit 2: Removing getters @property and renaming attributes.
Extraia um método chamado get_charge
de Client.statement
para a própria classe Client
.
O novo método deverá ter a seguinte assinatura:
def get_charge(self, rental: Rental) -> float:
O método extraído deve conter o código relativo ao comentário determine amounts for each line.
Com os testes passando, faça o commit com a seguinte mensagem: Commit 3: Extracting method get_charge from Client.statement.
Mova o método get_charge
da classe Client
para a classe Rental
, já que esse método não usa informações da primeira, mas sim da segunda classe.
Não esqueça de atualizar o método get_charge
em Rental
para chamar self
ao invés de rental
, por exemplo:
# Em Client
if rental.book.price_code == Book.REGULAR:
# Em Rental
if self.book.price_code == Book.REGULAR:
Também não esqueça de atualizar a chamada do método get_charge
em statement
:
# De...
amount = self.get_charge(rental)
# Para...
amount = rental.get_charge()
Com os testes passando, faça o commit com a seguinte mensagem: Commit 4: Moving method get_charge from Client to Rental.
Vamos decompor mais uma vez statement
para diminuir seu tamanho e complexidade.
O método extraído deve conter o código relativo ao comentário add frequent renter points.
Extraia o seguinte método chamado get_frequent_renter_points
e para classe Rental
:
def get_frequent_renter_points(self):
points = 1
if self.book.price_code == Book.NEW_RELEASE and self.days_rented > 1:
points += 1
return points
Atualize a chamada do método get_frequent_renter_points
em statement
:
frequent_renter_points = rental.get_frequent_renter_points()
Rode os testes, e observe que vários testes falham. Ou seja, inserimos uma regressão (BUG!) no sistema. Testes funcionam como uma rede de proteção contra a inserção de bugs.
Encontre e corrija o bug! Dica: o bug está no código acima.
Com os testes passando, faça o commit com a seguinte mensagem: Commit 5: Extracting get_frequent_renter_points from Client.statement to Rental.