Make some improvements in the Pizza class and pizzashop file by refactoring.

Overview

Refactoring Practice

Make some improvements in the Pizza class and pizzashop file by refactoring.

Goals to achieve for the code are:

  1. Replace string literals with named constants.
  2. Rename amethods to use the Python naming convention.
  3. Move misplaced code to a better place (Extract Method and then Move Method). This improves encapsulation and makes the code more reusable.
  4. Replace "switch" (if ... elif ... elif) with object behavior.

Background

Pizza describes a pizza with a size and optional toppings. The price depends on size and number of toppings. For example, large pizza is 280 Baht plus 20 Baht per topping.

pizza = Pizza('large')
pizza.addTopping("mushroom")
pizza.addtopping("pineapple")
print("The price is", pizza.getPrice())
'The price is 320'

There are 2 files to start with:

pizza.py     - code for Pizza class
pizzashop.py - create some pizzas and print them. Use to verify code.

1. Replace String Literals with Named Constants

Use Named Constants instead of Literals in Code.

In the Pizza class replace 'small', 'medium', and 'large" with named constants. Use your IDE's refactoring feature, not manual find and replace.

  1. Select 'small' in Pizza.

    • VSCode: right click -> Extract variable.
    • Pycharm: right click -> Refactor -> Extract Constant
    • Pydev: Refactoring -> Extract local variable.
  2. Do the same thing for "medium" and "large".

  3. In my tests, none of the IDE did exactly what I want. The constants SMALL, MEDIUM, and LARGE are top-level variables in pizza.py, but not part of the Pizza class.

    SMALL = 'small'
    MEDIUM = 'medium'
    LARGE = 'large'
    
    class Pizza:
        ...

    We would prefer to encapsulate the sizes inside the Pizza class, e.g. Pizza.SMALL (I'm disappointed none of the IDE did this). However, we will eventually get rid of these constants, so leave the constants as top-level variables for now.

  4. When you are done, the strings 'small', 'medium', 'large' should only appear once in the code (in the Pizza class).

  5. Did the IDE also change the sizes in pizzashop.py? If not, edit pizzashop.py and change sizes to references (Pizza.SMALL)

    from pizza import *
    
    if __name__ == "__main__":
        pizza = Pizza(SMALL)
        ...
        pizza2 = Pizza(MEDIUM)
        ...
        pizza3 = Pizza(LARGE)
  6. Run the code. Verify the results are the same.

2. Rename Method

  1. getPrice is not a Python-style name. Use refactoring to rename it to get_price.

    • VSCode: right-click on method name, choose "Rename Symbol"
    • Pycharm: right-click, Refactor -> Rename
    • Pydev: "Refactoring" menu -> Rename
  2. Did the IDE also rename getPrice in order_pizza()?

    • VSCode: no
    • Pycharm: yes. Notification of dynamic code in preview.
    • Pydev: yes (lucky guess)
    • This is a limitation of tools for dynamic languages. The tool can't be sure that the "pizza" parameter in order_pizza is really a Pizza. To help it, use type annotations.
  3. Undo the refactoring, so you have original getPrice.

  4. Add a type annotation in pizzashop.py so the IDE knows that parameter is really a Pizza:

    def order_pizza(pizza: Pizza):
    • Then do Refactoring -> Rename (in pizza.py) again.
    • Does the IDE change getPrice to get_price in pizzashop.py also?
  5. Rename addTopping in Pizza to add_topping. Did the IDE also rename it in pizzashop?

    • If not, rename it manually.
    • In this case, a smart IDE can infer that addTopping in pizzashop refers to Pizza.addTopping. Why?
  6. Run the code. Verify the code works the same.

3. Extract Method and Move Method

Perform refactorings in small steps. In this case, we extract a method first, then move it to a better place.

order_pizza creates a string description to describe the pizza. That is a poor location for this because:

  1. the description could be needed elsewhere in the application
  2. it relies on info about a Pizza that only the Pizza knows.

Therefore, it should be the Pizza's job to describe itself. This is also known as the Information Expert principle.

Try an Extract Method refactoring, followed by Move Method.

  1. Select these statements in order_pizza that create the description:

     description = pizza.size
     if pizza.toppings:
         description += " pizza with "+ ", ".join(pizza.toppings)
     else:
         description += " plain pizza"
  2. Refactor (Extract Method):

    • VS Code: right click -> 'Extract Method'. Enter "describe" as method name. (This worked in 2020, but in current VS Code it does not.)
    • PyCharm: right click -> Refactor -> Extract -> Method
    • PyCharm correctly suggests that "pizza" should be parameter, and it returns the description. (correct!)
    • PyDev: Refactoring menu -> Extract method. PyDev asks you if pizza should a parameter (correct), but the new method does not return anything. Fix it.
    • All IDE: after refactoring, move the two comment lines from order_pizza to describe as shown here:
    def describe(pizza):
        # create printable description of the pizza such as
        # "small pizza with muschroom" or "small plain pizza"
        description = pizza.size
        if pizza.toppings:
            description += " pizza with "+ ", ".join(pizza.toppings)
        else:
            description += " plain pizza"
        return description

    Forgetting to move comments is a common problem in refactoring. Be careful.

  3. Move Method: The code for describe() should be a method in the Pizza class, so it can be used anywhere that we have a pizza.

    • None of the 3 IDE do this correctly, so do it manually.
    • Select the describe(pizza) method in pizzashop.py and CUT it.
    • Inside the Pizza class (pizza.py), PASTE the method.
    • Change the parameter name from "pizza" to "self" (Refactor -> Rename).
  4. Rename Method: In pizza.py rename describe to __str__(self) method. You should end up with this:

    # In Pizza class:
    def __str__(self):
        # create printable description of the pizza such as
        # "small pizza with muschroom" or "small plain pizza"
        description = self.size
        if self.toppings:
            description += " pizza with "+ ", ".join(self.toppings)
        else:
            description += " plain pizza"
        return description
  5. Back in pizzashop.py, modify the order_pizza to get the description from Pizza:

    def order_pizza(pizza):
        description = str(pizza)
        print(f"A {descripton}")
        print("Price:", pizza.get_price())
  6. Eliminate Temp Variable The code is now so simple that we don't need the description variable. Eliminate it:

    def order_pizza(pizza)
        print(f"A {str(pizza)}")
        print("Price:", pizza.get_price())
  7. Test. Run the pizzashop code. Verify the results are the same.

4. Replace 'switch' with Call to Object Method

This is the most complex refactoring, but it gives big gains in code quality:

  • code is simpler
  • enables us to validate the pizza size in constructor
  • prices and sizes can be changed or added without changing the Pizza class

The get_price method has a block like this:

if self.size == Pizza.SMALL:
    price = ...
elif self.size == Pizza.MEDIUM:
    price = ...
elif self.size == Pizza.LARGE:
    price = ...

The pizza has to know the pricing rule for each size, which makes the code complex. An O-O approach is to let the pizza sizes compute their own price. Therefore, we will define a new datatype (class) for pizza size.

Python has an Enum type for this. An "enum" is a type with a fixed set of values, which are static instances of the enum type. Each enum member has a name and a value.

  1. In pizza.py replace the named constants LARGE, MEDIUM, and SMALL with an Enum named PizzaSize:

    from enum import Enum
    
    class PizzaSize(Enum):
        # Enum members written as: name = value
        small = 120
        medium = 200
        large = 280
    
        def __str__(self):
            return self.name
  2. Write a short script (in pizza.py or another file) to test the enum:

    if __name__ == "__main__":
        # test the PizzaSize enum
        for size in PizzaSize:
            print(size.name, "pizza has price", size.value)

    This should print the pizza prices. But the code size.value doesn't convey it's meaning: it should be the price. but the meaning of size.value is not clear. Add a price property to PizzaSize:

    # PizzaSize
        @property
        def price(self):
            return self.value
  3. In Pizza.get_price(), eliminate the if size == SMALL: elif ... It is no longer needed. The Pizza sizes know their own price.

    def get_price(self):
        """Price of a pizza depends on size and number of toppings"""
        price = self.size.price + 20*len(self.toppings)
  4. In pizzashop.py replace the constants SMALL, MEDIUM, and LARGE with PizzaSize.small, PizzaSize.medium, etc.

  5. Run the code. It should work as before. If not, fix any

Extensibility

Can you add a new pizza size without changing the Pizza class?

class PizzaSize(Enum):
    ...
    jumbo = 400

# and in pizzashop.__main__:
pizza = Pizza(PizzaSize.jumbo)

Type Safety

Using an Enum instead of Strings for named values reduces the chance for error in creating a pizza, such as Pizza("LARGE").

For type safety, you can add an annotation and a type check in the Pizza constructor:

    def __init__(self, size: PizzaSize):
        if not isinstance(size, PizzaSize):
            raise TypeError('size must be a PizzaSize')
        self.size = size

Further Refactoring

What if the price of each topping is different? Maybe "durian" topping costs more than "mushroom" topping.

There are two refactorings for this:

  1. Pass whole object instead of values - instead of calling size.price(len(toppings)), use size.price(toppings).
  2. Delegate to a Strategy - pricing varies but sizes rarely change, so define a separate class to compute pizza price. (Design principle: "Separate the parts that vary from the parts that stay the same")

References

  • The Refactoring course topic has suggested references.
  • Refactoring: Improving the Design of Existing Code by Martin Fowler is the bible on refactoring. The first 4 chapters explain the fundamentals.
Owner
James Brucker
Instructor at the Computer Engineering Dept of Kasetsart University.
James Brucker
Fcpy: A Python package for high performance, fast convergence and high precision numerical fractional calculus computing.

Fcpy: A Python package for high performance, fast convergence and high precision numerical fractional calculus computing.

SciFracX 1 Mar 23, 2022
A small python library that helps you to generate localization strings for your mobile projects.

LocalizationUtiltiy A small python library that helps you to generate localization strings for your mobile projects. This small script aims to help yo

1 Nov 12, 2021
Script to generate a massive volume of data in sql, csv, json or xml format

DataGenerator Made with Python Open for pull requests 1. Dependencies To install required dependencies run pip install -r requirements.txt 2. Executi

icrescenti 3 Sep 20, 2022
PyResToolbox - A collection of Reservoir Engineering Utilities

pyrestoolbox A collection of Reservoir Engineering Utilities This set of functio

Mark W. Burgoyne 39 Oct 17, 2022
Abstraction of a Unit, includes convertions and basic operations.

Units Abstraction of a Unit, includes convertions and basic operations. ------ EXAMPLE : Free Fall (No air resistance) ------- from units_test import

1 Dec 23, 2021
This tool lets you perform some quick tasks for CTFs and Pentesting.

This tool lets you convert strings and numbers between number bases (2, 8, 10 and 16) as well as ASCII text. You can use the IP address analyzer to find out details on IPv4 and perform abbreviation a

Ayomide Ayodele-Soyebo 1 Jul 16, 2022
Extract XML from the OS X dictionaries.

Extract XML from the OS X dictionaries.

Joshua Olson 13 Dec 11, 2022
.bvh to .mcfunction file converter.

bvh-to-mcf .bvh file to .mcfunction converter

Hanmin Kim 28 Nov 21, 2022
JeNot - A tool to notify you when Jenkins builds are done.

JeNot - Jenkins Notifications NOTE: under construction, buggy, and not production-ready What A tool to notify you when Jenkins builds are done. Why Je

1 Jun 24, 2022
A collection of utility functions to prototype geometry processing research in python

gpytoolbox This repo is a work in progress and contains general utility functions I have needed to code while trying to work on geometry process resea

Silvia Sellán 73 Jan 06, 2023
Simple integer-valued time series bit packing

Smahat allows to encode a sequence of integer values using a fixed (for all values) number of bits but minimal with regards to the data range. For example: for a series of boolean values only one bit

Ghiles Meddour 7 Aug 27, 2021
python-codicefiscale: a tiny library for encode/decode Italian fiscal code - codifica/decodifica del Codice Fiscale.

python-codicefiscale python-codicefiscale is a tiny library for encode/decode Italian fiscal code - codifica/decodifica del Codice Fiscale. Features T

Fabio Caccamo 53 Dec 14, 2022
🦩 A Python tool to create comment-free Jupyter notebooks.

Pelikan Pelikan lets you convert notebooks to comment-free notebooks. In other words, It removes Python block and inline comments from source cells in

Hakan Özler 7 Nov 20, 2021
A utility that makes it easy to work with Python projects containing lots of packages, of which you only want to develop some.

Mixed development source packages on top of stable constraints using pip mxdev [mɪks dɛv] is a utility that makes it easy to work with Python projects

BlueDynamics Alliance 6 Jun 08, 2022
✨ Voici un code en Python par moi, et en français qui permet d'exécuter du Javascript en Python.

JavaScript In Python ❗ Voici un code en Python par moi, et en français qui permet d'exécuter du Javascript en Python. 🔮 Une vidéo pour vous expliquer

MrGabin 4 Mar 28, 2022
Patch the pclntable from Go binaries

Pretrain and Fine-tune a T5 model with Flax on GCP This tutorial details how pretrain and fine-tune a FlaxT5 model from HuggingFace using a TPU VM ava

6 Oct 05, 2022
一款不需要买代理来减少扫网站目录被封概率的扫描器,适用于中小规格字典。

PoorScanner使用说明书 -工具在不同环境下可能不怎么稳定,如果有什么问题恳请大家反馈。说明书有什么错误的地方也大家欢迎指正。 更新记录 2021.8.23 修复了云函数主程序 gitee上传文件接口写错了的BUG(之前把自己的上传地址写死进去了,没从配置文件里读) 更新了说明书 PoorS

14 Aug 02, 2022
Dependency Injector is a dependency injection framework for Python.

What is Dependency Injector? Dependency Injector is a dependency injection framework for Python. It helps implementing the dependency injection princi

ETS Labs 2.6k Jan 04, 2023
A Container for the Dependency Injection in Python.

Python Dependency Injection library aiodi is a Container for the Dependency Injection in Python. Installation Use the package manager pip to install a

Denis NA 3 Nov 25, 2022
ecowater-softner is a Python library for collecting information from Ecowater water softeners.

Ecowater Softner ecowater-softner is a Python library for collecting information from Ecowater water softeners. Installation Use the package manager p

6 Dec 08, 2022