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 string to hashtags module

A string to hashtags module

Fayas Noushad 4 Dec 01, 2021
Simple tool for creating changelogs

Description Simple utility for quickly generating changelogs, assuming your commits are ordered as they should be. This tool will simply log all lates

2 Jan 05, 2022
A work in progress box containing various Python utilities

python-wipbox A set of modern Python libraries under development to simplify the execution of reusable routines by different projects. Table of Conten

Deepnox 2 Jan 20, 2022
Python lightweight dependency injection library

pythondi pythondi is a lightweight dependency injection library for python Support both sync and async functions Installation pip3 install pythondi Us

Hide 41 Dec 16, 2022
DUQ is a python package for working with physical Dimensions, Units, and Quantities.

DUQ is a python package for working with physical Dimensions, Units, and Quantities.

2 Nov 02, 2022
A module for account creation with python

A module for account creation with python

Fayas Noushad 3 Dec 01, 2021
Toolkit for collecting and applying templates of prompting instances

PromptSource Toolkit for collecting and applying templates of prompting instances. WIP Setup Download the repo Navigate to root directory of the repo

BigScience Workshop 1k Jan 05, 2023
An OData v4 query parser and transpiler for Python

odata-query is a library that parses OData v4 filter strings, and can convert them to other forms such as Django Queries, SQLAlchemy Queries, or just plain SQL.

Gorilla 39 Jan 05, 2023
Python humanize functions

humanize This modest package contains various common humanization utilities, like turning a number into a fuzzy human-readable duration ("3 minutes ag

Jason Moiron 1.6k Jan 01, 2023
Skywater 130nm Klayout Device Generators PDK

Skywaters 130nm Technology for KLayout Device Generators Mabrains is excited to share with you our Device Generator Library for Skywater 130nm PDK. It

Mabrains 18 Dec 14, 2022
Networkx with neo4j back-end

Dump networkx graph into nodes/relations TSV from neo4jnx.tsv import graph_to_tsv g = pklload('indranet_dir_graph.pkl') graph_to_tsv(g, 'docker/nodes.

Benjamin M. Gyori 1 Oct 27, 2021
Bounding Boxes Python Utils

Bounding Boxes Python Utils

Vadim 4 May 01, 2022
one_click_kag_server is a program which tries to fully automate the creation of a King Arthur's Gold server.

one_click_kag_server is a program which tries to fully automate the creation of a King Arthur's Gold server.

Benjamin Gorman 4 Jan 05, 2022
Import the module and create an object of the class LocalVariable.

LocalVariable Import the module and create an object of the class LocalVariable. Call the save method with the name and the value of a variable as arg

Sajedur Rahman Fiad 2 Dec 14, 2022
A Python script that transcript Arcaea chart file (.aff file) into AutoJS touchscreen script which automatically plays the Arcaea chart

ArcaeaAutoplay (AutoJS Version) A Python script that transcript Arcaea chart file (.aff file) into AutoJS touchscreen script which automatically plays

7 Dec 03, 2021
Give you a better view of your Docker registry disk usage.

registry-du Give you a better view of your Docker registry disk usage. This small tool will analysis your Docker registry(vanilla or Harbor both work)

Nova Kwok 16 Jan 07, 2023
A python package for your Kali Linux distro that find the fastest mirror and configure your apt to use that mirror

Kali Mirror Finder Using Single Python File A python package for your Kali Linux distro that find the fastest mirror and configure your apt to use tha

MrSingh 6 Dec 12, 2022
Generates a random prnt.sc link and display image.

Generates a random prnt.sc link and display image.

Emirhan 3 Oct 08, 2021
A (very dirty) experiment to remove layers from a Docker image.

Surgically remove layers from a Docker image (with a chainsaw)

Jérôme Petazzoni 9 Jun 08, 2022
🔩 Like builtins, but boltons. 250+ constructs, recipes, and snippets which extend (and rely on nothing but) the Python standard library. Nothing like Michael Bolton.

Boltons boltons should be builtins. Boltons is a set of over 230 BSD-licensed, pure-Python utilities in the same spirit as — and yet conspicuously mis

Mahmoud Hashemi 6k Jan 04, 2023