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
Collection of code auto-generation utility scripts for the Horizon `Boot` system module

boot-scripts This is a collection of code auto-generation utility scripts for the Horizon Boot system module, intended for use in Atmosphère. Usage Us

4 Oct 11, 2022
Prime Path Generator is a prime path generator used to generate prime paths.

Prime Path Generator is a prime path generator used to generate prime paths.

1 Nov 06, 2021
Python code to divide big numbers

divide-big-num Python code to divide big numbers

VuMinhNgoc 1 Oct 15, 2021
Generates a random prnt.sc link and display image.

Generates a random prnt.sc link and display image.

Emirhan 3 Oct 08, 2021
A simple tool to extract python code from a Jupyter notebook, and then run pylint on it for static analysis.

Jupyter Pylinter A simple tool to extract python code from a Jupyter notebook, and then run pylint on it for static analysis. If you find this tool us

Edmund Goodman 10 Oct 13, 2022
Group imports from Windows binaries

importsort This is a tool that I use to group imports from Windows binaries. Sometimes, you have a gigantic folder full of executables, and you want t

【☆ ゆう ☆ 】 15 Aug 27, 2022
The Black shade analyser and comparison tool.

diff-shades The Black shade analyser and comparison tool. AKA Richard's personal take at a better black-primer (by stealing ideas from mypy-primer) :p

Richard Si 10 Apr 29, 2022
Customized python validations.

A customized python validations.

Wilfred V. Pine 2 Apr 20, 2022
python script to generate color coded resistor images

Resistor image generator I got nerdsniped into making this. It's not finished at all, and the code is messy. The end goal it generate a whole E-series

MichD 1 Nov 12, 2021
Compute the fair market value (FMV) of staking rewards at time of receipt.

tendermint-tax A tool to help calculate the tax liability of staking rewards on Tendermint chains. Specifically, this tool calculates the fair market

5 Jan 07, 2022
a demo show how to dump lldb info to ida.

用一个demo来聊聊动态trace 这个仓库能做什么? 帮助理解动态trace的思想。仓库内的demo,可操作,可实践。 动态trace核心思想: 动态记录一个函数内每一条指令的执行中产生的信息,并导入IDA,用来弥补IDA等静态分析工具的不足。 反编译看一下 先clone仓库,把hellolldb

25 Nov 28, 2022
A Randomizer Oracle

Tezos Randomizer Tezod Randomizer "Oracle". It's a smart contract that you can call to get a random number between X and Y (for now). It uses entropy

Asbjorn Enge 19 Sep 13, 2022
Program to extract signatures from documents.

Extracting Signatures from Bank Checks Introduction Ahmed et al. [1] suggest a connected components-based method for segmenting signatures in document

Muhammad Saif Ullah Khan 9 Jan 26, 2022
About Library for extract infomation from thai personal identity card.

ThaiPersonalCardExtract Library for extract infomation from thai personal identity card. imprement from easyocr and tesseract New Feature v1.3.2 🎁 In

ggafiled 26 Nov 15, 2022
Simplex using Jordan exchanges taught in 236A

Simplex for 236A Python script to solve LP using simplex by Jordan exchanges taught in 236A. You will need python installed along with the 'numpy' and

Kunal Kishore 1 Nov 30, 2021
Produce a simulate-able SDF of an arbitrary mesh with convex decomposition.

Mesh-to-SDF converter Given a (potentially nasty, nonconvex) mesh, automatically creates an SDF file that describes that object. The visual geometry i

Greg Izatt 22 Nov 23, 2022
API for obtaining results from the Beery-Bukenica test of the visomotor integration development (VMI) 4th edition.

VMI API API for obtaining results from the Beery-Bukenica test of the visomotor integration development (VMI) 4th edition. Install docker-compose up -

Victor Vargas Sandoval 1 Oct 26, 2021
Modest utility collection for development with AIOHTTP framework.

aiohttp-things Modest utility collection for development with AIOHTTP framework. Documentation https://aiohttp-things.readthedocs.io Installation Inst

Ruslan Ilyasovich Gilfanov 0 Dec 11, 2022
Simple RGB to HEX game made in python

Simple RGB to HEX game made in python

5 Aug 26, 2022
Pass arguments by reference—in Python!

byref Pass arguments by reference—in Python! byrefis a decorator that allows Python functions to declare reference parameters, with similar semantics

9 Feb 10, 2022