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
A Program that generates and checks Stripe keys 24x7.

A Program that generates and checks Stripe keys 24x7. This was made only for Educational Purposes, I'm not responsible for the damages cause by you

iNaveen 18 Dec 17, 2022
A simple python implementation of Decision Tree.

DecisionTree A simple python implementation of Decision Tree, using Gini index. Usage: import DecisionTree node = DecisionTree.trainDecisionTree(lab

1 Nov 12, 2021
Delete all of your forked repositories on Github

Fork Purger Delete all of your forked repositories on Github Installation Install using pip: pip install fork-purger Exploration Under construc

Redowan Delowar 29 Dec 17, 2022
Simple code to generate a password for your account!

Password-Generator Simple code to generate a password for your account! Password Generator for passwords for your accounts or anything else! This code

DEEM 1 Jun 05, 2022
isort is a Python utility / library to sort imports alphabetically, and automatically separated into sections and by type.

isort is a Python utility / library to sort imports alphabetically, and automatically separated into sections and by type. It provides a command line utility, Python library and plugins for various e

Python Code Quality Authority 5.5k Jan 08, 2023
New time-based UUID formats which are suited for use as a database key

uuid6 New time-based UUID formats which are suited for use as a database key. This module extends immutable UUID objects (the UUID class) with the fun

26 Dec 30, 2022
A tool written in python to generate basic repo files from github

A tool written in python to generate basic repo files from github

Riley 7 Dec 02, 2021
✨ Un code pour voir les disponibilités des vaccins contre le covid totalement fait en Python par moi, et en français.

Vaccine Notifier ❗ Un chois aléatoire d'un article sur Wikipedia totalement fait en Python par moi, et en français. 🔮 Grâce a une requète API, on peu

MrGabin 3 Jun 06, 2021
Know your customer pipeline in apache air flow

KYC_pipline Know your customer pipeline in apache air flow For a successful pipeline run take these steps: Run you Airflow server Admin - connection

saeed 4 Aug 01, 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
Build capture utility for Linux

CX-BUILD Compilation Database alternative Build Prerequisite the CXBUILD uses linux system call trace utility called strace which was customized. So I

GLaDOS (G? L? Automatic Debug Operation System) 3 Nov 03, 2022
A simple gpsd client and python library.

gpsdclient A small and simple gpsd client and library Installation Needs Python 3 (no other dependencies). If you want to use the library, use pip: pi

Thomas Feldmann 33 Nov 24, 2022
Bounding Boxes Python Utils

Bounding Boxes Python Utils

Vadim 4 May 01, 2022
Implementing C++ Semantics in Python

Implementing C++ Semantics in Python

Tamir Bahar 7 May 18, 2022
Definitely legit social credit generator with python

definitely-legit-social-credit-generator I made this simple GUI program for a meme, no cap. Video: https://youtu.be/RmjxKtoli04 How to run: Clone this

Joshua Malabanan 8 Nov 01, 2021
JavaScript-style async programming for Python.

promisio JavaScript-style async programming for Python. Examples Create a promise-based async function using the promisify decorator. It works on both

Miguel Grinberg 191 Dec 30, 2022
A primitive Python wrapper around the Gromacs tools.

README: GromacsWrapper A primitive Python wrapper around the Gromacs tools. The library is tested with GROMACS 4.6.5, 2018.x, 2019.x, 2020.x, and 2021

Becksteinlab 140 Dec 28, 2022
A sys-botbase client for remote control automation of Nintendo Switch consoles. Based on SysBot.NET, written in python.

SysBot.py A sys-botbase client for remote control automation of Nintendo Switch consoles. Based on SysBot.NET, written in python. Setup: Download the

7 Dec 16, 2022
A script to check for common mistakes in LaTeX source files of scientific papers.

LaTeX Paper Linter This script checks for common mistakes in LaTeX source files of scientific papers. Usage python3 paperlint.py file.tex [-i/x inc

Michael Schwarz 12 Nov 16, 2022
Finds price floor for every single attribute in a given collection

Solana Solanart Scanner Enjoy the Free Code Steps to run Download VS Code

Dalton Nisbett 19 Oct 20, 2022