Week 03 / 14 Object-Oriented Programming
Lab 3 - From requirements to classes and encapsulation
We turn a short list of requirements into a Product class. First we design it on paper, then we hide its state and write methods that never let it become invalid.
- Identify objects, state and behaviour from requirements
- Define a class with private attributes and public methods
- Protect class invariants inside the methods
- Mark methods that only read the object as const
- Avoid writing a getter and a setter for every attribute
- Answer the question "What is this class responsible for?"
Plan (90 min)
| Time | Activity |
|---|---|
| 0-10 | Design without code: state, behaviour, impossible states |
| 10-20 | Discussion: what is private, what is public, which attributes get no setter |
| 20-30 | The starting program: how a public struct gets broken |
| 30-60 | Individual work: the Product class |
| 60-75 | Requirement change: warehouse capacity |
| 75-85 | Individual checks and short oral questions |
| 85-90 | What is this class responsible for? |
Requirements
A shop keeps products. Each product has an id, a name, a price and a stock (how many pieces are left). The shop can change the price, sell some pieces, restock and ask for the value of the stock (price times stock).
Design first, no code
Ten minutes on paper
Close the editor. On paper, answer:
- What is the object’s state?
- What behaviour belongs to the object?
- What states should be impossible?
- What should be private?
- What should be public?
- Which properties should not have setters?
Hint
For question 3, think of numbers that make no sense in a real shop: a stock of -5, a price of 0 or less, an empty name.Hint
For question 6, ask how each attribute is supposed to change in real life. Does stock change because someone “sets” it, or because something was sold or delivered?Solution
- State:
id,name,price,stock. - Behaviour: change the price, sell, restock, compute the value.
- Impossible: negative stock, price
<= 0, empty name, an id that changes after creation, selling more than is in stock. - Private: all four attributes.
- Public:
changePrice,sell,restock,getValue, and read-only getters where someone needs the value. idnever changes.stockchanges only throughsellandrestock. So neither gets a setter.pricechanges throughchangePrice, which validates it.
Starting program
This is how the data looks without a class. Read it and predict the last line before you run it.
#include <iostream>
#include <string>
using namespace std;
struct Product {
int id;
string name;
double price;
int stock;
};
double value(Product p) {
return p.price * p.stock;
}
int main() {
Product p;
p.id = 1;
p.name = "Pen";
p.price = 2.5;
p.stock = 10;
p.stock = p.stock - 15; // sold 15 pens we did not have
p.price = -1; // typo while changing the price
p.id = 7; // someone "fixed" the id
cout << p.id << " " << p.name << " " << p.stock << " " << value(p) << endl;
}
It prints 7 Pen -5 5. The stock value is positive and looks reasonable, because two wrong numbers multiplied give a plausible one. Every line in main compiled. Nothing in the program knows the rules of a shop.
Mandatory questions
- What is the object’s state?
- What behaviour belongs to the object?
- What states should be impossible?
- What should be private? What should be public?
- Which properties should not have setters?
- What is this class responsible for?
Predict, then check:
#include <iostream>
using namespace std;
class Product {
double price = 0;
int stock = 0;
public:
void init(double p, int s) { price = p; stock = s; }
bool sell(int qty) {
if (qty <= 0 || qty > stock) return false;
stock -= qty;
return true;
}
int getStock() const { return stock; }
double getValue() const { return price * stock; }
};
int main() {
Product p;
p.init(2.5, 10);
p.sell(3);
p.sell(20);
cout << p.getStock() << " " << p.getValue();
} Minimum task
The Product class
Complete the class. Every method that changes the object checks its input and returns false if it refuses. Methods that only read are const.
initaccepts onlyid > 0, a non-empty name,price > 0,stock >= 0, and works only once (the id must not change later).changePricerefuses a price<= 0.sellrefuses a quantity<= 0or larger than the stock.restockrefuses a quantity<= 0.getValuereturnsprice * stock.
Test every refusal from main, not only the cases that work.
#include <iostream>
#include <string>
using namespace std;
class Product {
int id = 0;
string name;
double price = 0;
int stock = 0;
public:
bool init(int newId, const string& newName, double newPrice, int newStock) {
// TODO: reject invalid data, accept only the first call
id = newId;
name = newName;
price = newPrice;
stock = newStock;
return true;
}
bool changePrice(double newPrice) {
// TODO
return false;
}
bool sell(int qty) {
// TODO
return false;
}
bool restock(int qty) {
// TODO
return false;
}
double getValue() const {
// TODO
return 0;
}
int getStock() const { return stock; }
};
int main() {
Product p;
p.init(1, "Pen", 2.5, 10);
cout << p.sell(3) << " " << p.getStock() << endl; // expected: 1 7
cout << p.sell(20) << " " << p.getStock() << endl; // expected: 0 7
cout << p.getValue() << endl; // expected: 17.5
}Input (stdin)
Hint
A product that was never initialised hasid == 0. Use that to know whether init was already called.Hint
Write the check first and the change after it:if (qty <= 0 || qty > stock) return false; and only then stock -= qty;.Solution
bool init(int newId, const string& newName, double newPrice, int newStock) {
if (id != 0) return false; // already initialised
if (newId <= 0 || newName.empty() || newPrice <= 0 || newStock < 0) return false;
id = newId;
name = newName;
price = newPrice;
stock = newStock;
return true;
}
bool changePrice(double newPrice) {
if (newPrice <= 0) return false;
price = newPrice;
return true;
}
bool sell(int qty) {
if (qty <= 0 || qty > stock) return false;
stock -= qty;
return true;
}
bool restock(int qty) {
if (qty <= 0) return false;
stock += qty;
return true;
}
double getValue() const { return price * stock; }The invariant “stock is never negative” is checked in exactly two places, sell and restock, because they are the only code that can change stock.
Requirement change
New requirement: the warehouse holds at most 100 pieces of a product. A restock or init that would exceed 100 is refused.
- How many places in your code did you have to change? Why so few?
- What would you have had to search for in the starting program, with the public struct?
- Write
void print(const Product& p)outside the class. Which methods can it call? What does the compiler say ifgetValueis notconst?