Skip to content

Commit eb70df8

Browse files
committed
Grading of Labexam 2
1 parent 870fac9 commit eb70df8

File tree

4 files changed

+249
-1
lines changed

4 files changed

+249
-1
lines changed

code-review/Reviews.pdf

22 Bytes
Binary file not shown.

code-review/Reviews.ppt

-512 Bytes
Binary file not shown.

review/labexam2-feedback.md

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
# Lab Exam 2 Grading and Comments
2+
3+
Full score is 100, but its possible to get more than 100 points:
4+
5+
|Problem | Points |
6+
|:-----------------------|-------:|
7+
|1. Preserve Object | 20 |
8+
|2. Define a LineItem | 20 |
9+
|3. Divide Long Method | 40 |
10+
|4. Polymorphic Customer | 20 |
11+
|5. Unit Testing of Order| 20 |
12+
|Total | 120 |
13+
14+
7 students earned score 100 or above. Excellent work!
15+
16+
17+
## 1. Preserve Whole Object (20 pt)
18+
19+
Pass the whole `product` object to `order.add_item`.
20+
First, modify `make_sale()` in `main.py` to invoke:
21+
```python
22+
order.add_item(product, quantity)
23+
```
24+
25+
and in `Order`:
26+
```python
27+
def __init__(self,customer: Customer):
28+
self.item_quantity: List[int] = []
29+
self.products: List[Product] = []
30+
31+
def add_item(self, product, quantity: int) -> None:
32+
"""Add an item to the order, or update an existing item."""
33+
if product in self.products:
34+
k = self.products.index(product)
35+
self.item_quantity[k] += quantity
36+
else:
37+
self.products.append(product)
38+
self.item_quantity.append(quantity)
39+
```
40+
41+
#### Common Error 1. Adding a `quantity` attribute to Product!
42+
43+
**Wrong** because:
44+
45+
1. The exam stated **not** to modify Product.
46+
2. Conceptually its a bad idea: "product" represents a kind of item for sale, not a specific purchase of the item.
47+
3. It can cause errors. The same Product instance is returned by `ProductCatalog` each time `find_product(id)` is called with same `id`. Suppose you have 2 Order objects that include the same product. They would modify each other's quantity!
48+
49+
#### Common Error 2. Logic Errors in Checking for Product in Order
50+
51+
Bad "if - else" logic, which is described in Problem 2.
52+
53+
## 2. Replace Parallel Arrays with Array of Objects (20 pt)
54+
55+
Define a `LineItem` like this:
56+
```python
57+
class LineItem:
58+
def __init__(self, product, quantity):
59+
self.product = product
60+
self.quantity = quantity
61+
```
62+
If you want to make it *easy* to access product attributes then add read-only properties for them:
63+
```python
64+
# this is not required
65+
@property
66+
def id(self):
67+
return self.product.id
68+
69+
@property
70+
def name(self):
71+
return self.product.name
72+
```
73+
and use a list of `LineItem` objects in `Order` instead of parallel arrays:
74+
```pyton
75+
class Order:
76+
def __init__(self, customer: Customer):
77+
self.customer = customer
78+
# items in the order
79+
self.line_items: List[LineItem] = []
80+
```
81+
82+
83+
#### Common Error 1. Not Preserving Whole Object
84+
85+
After Problem 1 recommended to *Preserve Whole Object* some people did not do that in LineItem. Wow!
86+
```python
87+
class LineItem:
88+
# WRONG: should pass whole product object as parameter to __init__
89+
def __init__(self, product_id, name, price, quantity):
90+
self.id = product_id
91+
self.name = name
92+
self.price = price
93+
self.quantity = quantity
94+
```
95+
96+
A few students wrote `class LineItem():` when defining a class. Parens are **not required**.
97+
Please stop writing them.
98+
99+
100+
#### Common Error 2. Logic Errors in `order.add_item()`
101+
102+
After introducing LineItem, how do you check if a product is already part of a sale?
103+
These codes are incorrect:
104+
105+
```python
106+
def addItem(self, product, quantity):
107+
108+
for line_item in self.line_items:
109+
if product.id == line_item.product.id:
110+
k = self.line_items.index(line_item)
111+
self.line_items[k].quantity += quantity
112+
else:
113+
# Not found, so add a new line item
114+
self.line_items.append( LineItem(product, quantity) )
115+
```
116+
this "else" clause makes *no sense* but Python permits it:
117+
118+
```python
119+
for line_item in self.line_items:
120+
if product.id == line_items.product.id:
121+
k = self.line_items.index(line_item)
122+
self.line_items[k].quantity += quantity
123+
else:
124+
# Not found, so add a new line item
125+
self.line_items.append( LineItem(product, quantity) )
126+
```
127+
128+
But the logic is still incorrect. One possible correct solution is:
129+
```python
130+
matched_item = None
131+
for item in self.line_items:
132+
if product == item.product:
133+
matched_item = item
134+
break # found a match, so stop looking!
135+
# is this a new product or existing product?
136+
if matched_item:
137+
matched_item.quantity += quantity
138+
else:
139+
self.line_items.append( LineItem(product, quantity) )
140+
```
141+
142+
#### Wrong Use of Plurality in Names
143+
144+
A collection name should be **plural**. A variable from a single item should be **singular**.
145+
146+
```python
147+
self.line_item: List[LineItem] = []
148+
```
149+
Should be:
150+
```python
151+
self.line_items: List[LineItem] = []
152+
```
153+
154+
#### Redundant Prefixes on Names
155+
156+
Inside the `LineItem` class, there is no reason to prefix names with `item_` like this:
157+
```python
158+
class LineItem:
159+
def __init__(self, product, quantity):
160+
self.item_product = product
161+
self.item_quantity = quantity
162+
etc.
163+
```
164+
165+
It makes the code harder to read, as shown here:
166+
```python
167+
item = self.lineitems[k]
168+
print(item.item_product.name, item.item_quantity, item.item_product.price)
169+
```
170+
The `item_` prefix is redundant. See how much easier to read:
171+
```python
172+
item = self.lineitems[k]
173+
print(item.product.name, item.quantity, item.product.price)
174+
```
175+
176+
## 3. Divide Long Method (40 pt)
177+
178+
Divide `print_sale` into 4 methods. This problem has 4 parts. Each part is worth 10 points.
179+
180+
| Method | Description |
181+
|:----------------|:----------------------------|
182+
| `get_total()` | Compute and return sale total. **Does not print anything**. |
183+
| `get_discount()` | Compute and return discount. **Does not print anything**. |
184+
| `get_loyalty_pts()` | Compute and return member points. **Does not print anything**. |
185+
| `print_sale()` | Invokes the other 3 methods, and is the only method to print the sale. |
186+
187+
Common Errors:
188+
189+
* Using an attribute to compute and save order total, discount, or member points instead of a local variable.
190+
* `get_total`, `get_discount`, or `get_loyalty_pts` save the value as attribute but don't return it.
191+
* `get_total` prints the line items.
192+
* replacing `print_sale` with another method, or having the main class call several methods to do what `print_sale` did. *Refactoring is not rewriting*.
193+
194+
## 4. Replace Conditional Logic with Polymorphism (20 pt)
195+
196+
You should define 3 "customer" classes, with one as base class.
197+
Each class has its own `get_discount(order)` and `get_loyalty_pts(order)` methods.
198+
199+
For example:
200+
```python
201+
class Customer:
202+
"""Base class is for non-members"""
203+
204+
def get_discount(self, order):
205+
return 0.0
206+
207+
class Member(Customer):
208+
"""Pricing rules for ordinary members"""
209+
210+
def get_discount(self, order):
211+
total = order.get_total()
212+
return 0.03*total
213+
214+
class GoldMember(Customer):
215+
"""Pricing rules for gold members"""
216+
217+
def get_discount(self, order):
218+
total = order.get_total()
219+
return 0.05*total + 0.05*max(0.0, total-1000)
220+
```
221+
222+
Then, in the `Order` class, the `get_discount` method simply invokes the
223+
customer's own method:
224+
```python
225+
class Order:
226+
227+
def get_discount(self):
228+
"""Customer needs a reference to the order to compute the discount"""
229+
return self.customer.get_discount(self)
230+
```
231+
232+
This is an example of the *Strategy Design Pattern*.
233+
Its sometimes called the *State Pattern*, which has the same structure,
234+
but I thick *Strategy* is more correct in this application.
235+
236+
How to create customers in `make_customer`?
237+
Some students wrote a clever solution like this:
238+
```python
239+
def make_customer(cust_type: str):
240+
classes = {'m': Member, 'g': GoldMember}
241+
return (classes[cust_type]() if cust_type in classes
242+
else Customer())
243+
```
244+
There is no parameter to the class constructors since each customer class already knows its customer type.
245+
246+
## 5. Unit Tests (20 pt)
247+
248+
Most students who got this far wrote good tests.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Comments on Programming Exam
1+
## Comments on Programming Exam 2018
22

33
It is great that you can write a useful, working web application in only 4 hours! This class *raises the bar* for future SKE students.
44

0 commit comments

Comments
 (0)