OOP Search

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.

Objectives
  • 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)

TimeActivity
0-10Design without code: state, behaviour, impossible states
10-20Discussion: what is private, what is public, which attributes get no setter
20-30The starting program: how a public struct gets broken
30-60Individual work: the Product class
60-75Requirement change: warehouse capacity
75-85Individual checks and short oral questions
85-90What 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:

  1. What is the object’s state?
  2. What behaviour belongs to the object?
  3. What states should be impossible?
  4. What should be private?
  5. What should be public?
  6. Which properties should not have setters?
HintFor 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.
HintFor 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
  1. State: id, name, price, stock.
  2. Behaviour: change the price, sell, restock, compute the value.
  3. Impossible: negative stock, price <= 0, empty name, an id that changes after creation, selling more than is in stock.
  4. Private: all four attributes.
  5. Public: changePrice, sell, restock, getValue, and read-only getters where someone needs the value.
  6. id never changes. stock changes only through sell and restock. So neither gets a setter. price changes through changePrice, 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

  1. What is the object’s state?
  2. What behaviour belongs to the object?
  3. What states should be impossible?
  4. What should be private? What should be public?
  5. Which properties should not have setters?
  6. 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.

  • init accepts only id > 0, a non-empty name, price > 0, stock >= 0, and works only once (the id must not change later).
  • changePrice refuses a price <= 0.
  • sell refuses a quantity <= 0 or larger than the stock.
  • restock refuses a quantity <= 0.
  • getValue returns price * 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)

Open in Godbolt

HintA product that was never initialised has id == 0. Use that to know whether init was already called.
HintWrite 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.

Code in repository: L03

Romanian version