← Computer Programming II

Problem 1 (Easy): Shared Visit Counter

A small website wants to count how many times users interact with it. Two different parts of the app need to update and read the same counter:

  • auth.py — adds 1 to the counter every time a user logs in.
  • shop.py — adds 1 to the counter every time a user buys something.

Both files must work with the same shared counter — not their own copies. Build the counter as a module singleton (a plain .py file with shared state).

  1. Create a file counter.py with:
    • a variable count starting at 0
    • a function add_visit() that increases count by 1
    • a function get_count() that returns the current count
  2. Create a file auth.py with a function login(user) that prints "<user> logged in" and calls counter.add_visit().
  3. Create a file shop.py with a function buy(user, item) that prints "<user> bought <item>" and calls counter.add_visit().
  4. Create a file main.py that imports all three modules and runs the input scenario below.
  5. At the end, main.py must print the total visit count by calling counter.get_count() — and the number must reflect every call from both auth.py and shop.py.

Input

Alisher logs in
Sevara logs in
Alisher buys "book"
Sevara buys "phone"
Alisher buys "pen"

Expected Output

Alisher logged in
Sevara logged in
Alisher bought book
Sevara bought phone
Alisher bought pen
Total visits: 5

Problem 2 (Easy+): Unique ID Generator

Many parts of an app need to create unique IDs — for users, for orders, for messages. The rule: no two IDs should ever be the same, even if different parts of the app create their own IDGenerator().

Build an IDGenerator class as a Singleton (using __new__). Every time someone calls next_id(), it returns the next number: 1, 2, 3, … — even if the call comes from a different IDGenerator() object.

  1. Create a class IDGenerator with:
    • a class variable _instance = None
    • a __new__ method that creates the object only once and saves it in _instance
    • inside __new__, set current = 0 on the instance the first time it is created (do not put this in __init__)
    • a method next_id() that increases current by 1 and returns the new value
  2. In the main code, create three different generator objects: users_gen, orders_gen, messages_gen.
  3. Run the input code below.
  4. At the end, print whether all three variables point to the same object using is.

Input

print(f"User ID: {users_gen.next_id()}")
print(f"User ID: {users_gen.next_id()}")
print(f"Order ID: {orders_gen.next_id()}")
print(f"User ID: {users_gen.next_id()}")
print(f"Message ID: {messages_gen.next_id()}")
print(f"Order ID: {orders_gen.next_id()}")

Expected Output

User ID: 1
User ID: 2
Order ID: 3
User ID: 4
Message ID: 5
Order ID: 6
Same object? True

Problem 3 (Medium): Drink Factory

A coffee shop app sells three kinds of drinks: coffee, tea, and juice. Each drink has a size and knows how to prepare itself. The cashier should not write if/elif chains — they should just say “give me a coffee, large” and get the right object back.

Build a Factory that creates drinks based on a type chosen from an Enum. Use an abstract base class so every drink follows the same interface.

  1. Create an Enum called DrinkType with three members: COFFEE, TEA, JUICE.
  2. Create an abstract base class Drink:
    • __init__ takes size (a string like "small", "medium", "large") and saves it.
    • has an abstract method prepare().
  3. Create three subclasses of Drink:
    • Coffee.prepare() → prints "Brewing <size> coffee ☕"
    • Tea.prepare() → prints "Steeping <size> tea 🍵"
    • Juice.prepare() → prints "Squeezing <size> juice 🧃"
  4. Create a class DrinkFactory with:
    • a class variable _types — a dict mapping each DrinkType to its class.
    • a @staticmethod create(kind, size) that looks up the class in _types, creates the drink, and returns it.
    • if kind is not in the dict, raise ValueError(f"Unknown drink: {kind}").
  5. Run the input code below.

Input

orders = [
    (DrinkType.COFFEE, "large"),
    (DrinkType.TEA, "small"),
    (DrinkType.JUICE, "medium"),
    (DrinkType.COFFEE, "small"),
]

for kind, size in orders:
    drink = DrinkFactory.create(kind, size)
    drink.prepare()

Expected Output

Brewing large coffee ☕
Steeping small tea 🍵
Squeezing medium juice 🧃
Brewing small coffee ☕

Problem 4 (Medium+): Self-Registering Exporter Factory

In the previous problem, the factory had a hard-coded _types dict — every time someone added a new drink, they had to open the factory and edit it (OCP violation).

Now we will fix that. Build a Factory where each exporter class registers itself using a decorator. Adding a new exporter should require zero changes to the factory class.

The app exports a list of names into different file formats: CSV, JSON, and XML.

Step 1 — Write the exporters first (the natural way).

  1. Create an abstract base class Exporter with an abstract method export(data) (where data is a list of strings).
  2. Create three concrete exporter classes — no factory, no decorators yet:
    • CSVExporter — prints all names joined by ,
    • JSONExporter — prints the list as JSON (use json.dumps)
    • XMLExporter — prints <list>, then ` NAME for each name, then </list>`

Step 2 — Add the factory.

You could now write a _types = {"csv": CSVExporter, ...} dict by hand like in Problem 3, but every new exporter would force you to edit the factory. Instead, let each exporter class register itself.

  1. Create a class ExporterFactory with:
    • a class variable _types = {} (empty dict — it will fill up automatically).
    • a @classmethod register(cls, kind) that returns a decorator. The decorator takes a class (the exporter class being decorated, e.g. CSVExporter) as its only argument, saves it in _types[kind], and returns the same class unchanged.
    • a @classmethod create(cls, kind) that creates and returns an exporter instance for the given kind. If kind is unknown, raise ValueError(f"Unknown exporter: {kind}").

Step 3 — Move the exporters below the factory and decorate them.

  1. Move your three exporter classes so they appear after ExporterFactory, and add a decorator to each one:
    • @ExporterFactory.register("csv") above CSVExporter
    • @ExporterFactory.register("json") above JSONExporter
    • @ExporterFactory.register("xml") above XMLExporter

    Notice: ExporterFactory itself stayed completely untouched. To add a new format tomorrow, you just write a new class with @ExporterFactory.register("yaml") — the factory never changes. That is the Open/Closed Principle in action.

  2. Run the input code below.

Input

data = ["Alisher", "Sevara", "Aziz"]

for fmt in ["csv", "json", "xml"]:
    print(f"--- {fmt.upper()} ---")
    exporter = ExporterFactory.create(fmt)
    exporter.export(data)

Expected Output

--- CSV ---
Alisher,Sevara,Aziz
--- JSON ---
["Alisher", "Sevara", "Aziz"]
--- XML ---
<list>
  <item>Alisher</item>
  <item>Sevara</item>
  <item>Aziz</item>
</list>

Problem 5 (Advanced): Stock Market Traders

Build a small stock market simulator that combines two patterns:

  • Observer — a stock notifies all subscribed traders whenever its price changes.
  • Strategy — each trader uses a different trading strategy to decide what to do (BUY, SELL, or HOLD) based on the new price.

A Stock is like a Telegram channel — when its price changes, every subscriber instantly hears about it. Some subscribers are traders that must decide BUY / SELL / HOLD; others just watch and record. A trader never hardcodes its own rule — the rule is a swappable object the trader consults. Figure out yourself where Observer ends and Strategy begins.

  1. Create an abstract base class TradingStrategy with abstract method decide(price) returning a string: "BUY", "SELL", or "HOLD".
  2. Create three concrete strategies — BuyLowStrategy(threshold), SellHighStrategy(threshold), AlwaysHoldStrategy() — with the constructor signatures shown in the driver code. Their decision rules are not given to you; infer each one from the expected output.
  3. Create an abstract base class Observer with abstract method update(price).
  4. Create a class Trader(Observer):
    • __init__ takes name and a TradingStrategy object.
    • update(price) — figure out what it must do and what it must print from the expected output.
  5. Create a class PriceLogger(Observer) — a different kind of observer that does not trade, it just records prices.
    • __init__ takes no arguments.
    • update(price) — figure out what it must do and what it must print from the expected output. Hint: the (total: N) part means it has to remember something between calls.
  6. Create a class Stock (the subject):
    • __init__ takes a symbol (like "AAPL"), keeps an empty list of observers.
    • subscribe(observer) adds an observer.
    • unsubscribe(observer) removes an observer.
    • set_price(price) prints "📊 <symbol>: $<price>", then notifies every observer by calling its update(price).
  7. Run the input code below.

Input

apple = Stock("AAPL")

apple.subscribe(Trader("Alisher", BuyLowStrategy(100)))
apple.subscribe(Trader("Sevara",  SellHighStrategy(150)))
apple.subscribe(Trader("Aziz",    AlwaysHoldStrategy()))
apple.subscribe(PriceLogger())

for price in [120, 90, 160, 140]:
    apple.set_price(price)

Expected Output

📊 AAPL: $120
Alisher: HOLD at $120
Sevara: HOLD at $120
Aziz: HOLD at $120
📒 Logger: recorded $120 (total: 1)
📊 AAPL: $90
Alisher: BUY at $90
Sevara: HOLD at $90
Aziz: HOLD at $90
📒 Logger: recorded $90 (total: 2)
📊 AAPL: $160
Alisher: HOLD at $160
Sevara: SELL at $160
Aziz: HOLD at $160
📒 Logger: recorded $160 (total: 3)
📊 AAPL: $140
Alisher: HOLD at $140
Sevara: HOLD at $140
Aziz: HOLD at $140
📒 Logger: recorded $140 (total: 4)

Problem 6 (Advanced+): Smart Home Automation (Mini-Project)

Build a small smart home system that combines all four patterns, split across multiple files (one responsibility per file, like SRP from SOLID):

  • SingletonEventLog: one shared log of every event in the house.
  • FactoryDeviceFactory: creates devices by name; new device types register themselves with a decorator (no edits to the factory).
  • ObserverTempSensor is the subject; devices attach to it and react to temperature changes.
  • Strategy — each device has a strategy object that evaluates what action ("ON", "OFF", "ALERT", or nothing) to take given the current temperature.

A TempSensor reports the room temperature every step. Every device subscribed to the sensor receives the new temperature and asks its strategy what to do. To avoid repeated log entries, each device remembers the action it took last time. It only writes a new event to the EventLog when the new action is different from the previous one. For example, if a heater is already "ON" and the strategy says "ON" again, nothing is logged; but if the strategy now says "OFF", that change is logged.

File structure

smart_home/
├── event_log.py     # Singleton EventLog
├── strategies.py    # DeviceStrategy ABC + Heater/AC/Alarm strategies
├── device.py        # Listener ABC + Device class
├── sensor.py        # TempSensor (subject)
├── factory.py       # DeviceFactory + @register builders
└── main.py          # demo loop
  1. event_log.py — implement a singleton EventLog shared by the whole system:
    • Any part of the program should be able to create or access an EventLog, but all of them must refer to the same shared log.
    • The log should store all recorded events.
    • Provide a way to add a new event message and print it in the format "📝 <msg>".
    • Use this shared log from any module that needs to record sensor or device activity.
  2. strategies.py — abstract base class DeviceStrategy with abstract method evaluate(temp) returning "ON", "OFF", "ALERT", or None. Then three strategies:
    • HeaterStrategy(threshold)"ON" if temp < threshold (room is too cold), else "OFF".
    • ACStrategy(threshold)"ON" if temp > threshold (room is too hot), else "OFF".
    • AlarmStrategy()"ALERT" if temp > 40 or temp < 0, else "OFF".
  3. device.py — abstract base class Listener with abstract method on_reading(temp). Then class Device(Listener):
    • __init__(name, strategy) — saves both, plus last_action = "OFF".
    • on_reading(temp): ask the strategy to evaluate; if the result is None or equal to last_action, do nothing. Otherwise update last_action and call log.add(f"{name} → {action}") (using the log = EventLog() defined at the top of this file).
  4. sensor.py — class TempSensor:
    • holds a list of listeners; method attach(listener).
    • method report(temp) calls log.add(f"🌡️ Sensor reports {temp}°C") (using the log = EventLog() defined at the top of this file), then notifies every listener.
  5. factory.py — class DeviceFactory:
    • class variable _builders = {}.
    • @classmethod register(cls, kind) returns a decorator. The decorator takes a single argument — a builder function — saves it in _builders[kind], and returns it unchanged.
    • @classmethod create(cls, kind, name) looks up the function registered under kind and calls it with name, returning the resulting Device. Raise ValueError(f"Unknown device: {kind}") if kind was never registered.
    • Right after the class, register three builder functions:
      • "heater"Device(name, HeaterStrategy(threshold=18))
      • "ac"Device(name, ACStrategy(threshold=26))
      • "alarm"Device(name, AlarmStrategy())
  6. main.py — inside if __name__ == "__main__"::
    • Create the sensor and three devices via DeviceFactory.create(...): "Living Room Heater", "Bedroom AC", "Fire Alarm".
    • Attach all three to the sensor.
    • Loop over readings = [22, 16, 14, 28, 30, 45, 25] and call sensor.report(temp) for each value.
    • At the end print f"Total events logged: {len(log.events)}" (using the log = EventLog() defined at the top of main.py).

Expected Output

📝 🌡️ Sensor reports 22°C
📝 🌡️ Sensor reports 16°C
📝 Living Room Heater → ON
📝 🌡️ Sensor reports 14°C
📝 🌡️ Sensor reports 28°C
📝 Living Room Heater → OFF
📝 Bedroom AC → ON
📝 🌡️ Sensor reports 30°C
📝 🌡️ Sensor reports 45°C
📝 Fire Alarm → ALERT
📝 🌡️ Sensor reports 25°C
📝 Bedroom AC → OFF
📝 Fire Alarm → OFF
Total events logged: 13