លំហសិក្សាកម្ពុជា លំហសិក្សាកម្ពុជា V1.0
ចូល ចុះឈ្មោះ

Homework for OOP II

មូលសង្ខេប / Abstract

This is the summary for OOP II Homework (Advanced OOP with Python)

ពាក្យគន្លឹះ

Field

Student Input

Project Title

Homework Summary for OOP II

Team Members

Kang Soksambathratanak

Course / Class

Advanced OOP with Python

Lecturer

SEK SOCHEAT

Semester / Year

Semester 1, Year 2 (2026–2027)

Submission Type

Homework


Lesson 1: Why Bother Thinking in Objects?

When you're new to coding, you basically ask "what do I run first, second, third?" OOP flips the question around: instead of steps, you start asking "what things live in my program, what do they each know, and whose job is what?"

Class vs Object. A class is just the blueprint or recipe. The object is the actual thing you built from it.

What's self doing? It just means "this one specific object, not any other." If I've got two users, Dara and Sophea, self is why Dara's email doesn't accidentally overwrite Sophea's.

Instance data vs class data. If the info belongs to one object only, like each user's own email, it's an instance variable (self.email). If it's info about the whole class as a group, like how many users have ever signed up total, that lives on the class itself (User.user_count).

The 3 Method Types

  • Instance methods (self): the everyday method that reads or changes one object's own info. Example: user.display().
  • Class methods (@classmethod, uses cls): works on the class as a whole. I mostly use these as shortcuts to build objects, like giving it just "dara" and having it auto-append @school.edu.kh for you.
  • Static methods (@staticmethod): doesn't touch self or cls at all. It's really just a helper function that lives inside the class because it's thematically related, like checking a username is at least 3 characters.

Mini Project: A Basic RBAC (Role-Based Access Control)

This is the example that shows why all this matters. Instead of dumping a giant list of permissions onto one user, it's built in layers:

  • Permission: smallest building block, e.g. "user.delete"
  • Role: a bundle of permissions (an Admin role holds several permissions together)
  • User: holds one or more roles

So when you call user.has_permission("user.delete"), the user checks its roles, the roles check their permissions, and you get a clean True/False back. The rest of your program never needs to know HOW that check happens internally. That's really the point of clean abstraction: hide the messy details behind a simple question.

Keeping the Project Organized

Stop shoving everything into one giant main.py. Split it like this:

  • a models/ folder with user.py, role.py, permission.py, plus an __init__.py so it's a proper package
  • a root main.py that just imports those models and runs or tests things

Takeaway: good code isn't just code that works today. It's code that survives getting bigger. Name things clearly, split by responsibility, clean your inputs, and let each object manage its own data instead of everyone reaching in and poking at it.


 

Lesson 2: Data Classes vs Regular Classes, and @property

Not every class should be written the same way. It depends on the job it's doing.

Data-centric classes (use @dataclass): their whole job is just holding a bundle of values together, think a Permission or a database row. Writing __init__, __repr__, __eq__ by hand for these is a waste of your time, so let the decorator generate them.

Behavior-centric classes (regular class): their job is managing actions, rules, workflows, like a User handling logins and permission checks. These need you to manually control how the data changes.

@dataclass: Handy but Has Traps

Slap @dataclass on top and it auto-writes __init__(), a readable __repr__(), and __eq__() for you. But watch out for:

  • The mutable default trap. Never do permissions: list = []. Python will literally share that one list across every single object you create. Always use field(default_factory=list) so each new object gets its own fresh list.
  • Ordering rule. Required fields have to come before optional ones with defaults.
  • __post_init__(). Runs right after the auto-generated __init__ finishes. It's the perfect place to clean up inputs (strip spaces, etc.) and validate ranges before garbage data gets in.
  • Extra knobs. frozen=True makes it read-only after creation, good for stable value objects. slots=True saves memory and stops people randomly bolting on attributes you didn't define. kw_only=True forces people to use keyword arguments so nobody mixes up the order.

@property: Keep the Nice Syntax, Add Protection Underneath

Without @property, anyone can just do user.email = "junk" and blow up your program later.

  • Getter/setter pattern. Lets outside code still write user.email like a normal attribute, while secretly running validation behind the scenes.
  • The infinite loop trap. Inside your @email.setter, you must assign to the private attribute self._email. If you accidentally write self.email = value in there, the setter calls itself forever and crashes.
  • Computed / read-only properties. Don't manually store a value you can just calculate on the fly, like role_count. Make it a getter with no setter. It stays accurate automatically with zero duplicate state.

Upgrading the RBAC From Lesson 1

  • Permission → @dataclass(frozen=True). Immutable. Auto-normalizes its code (resource.action) in __post_init__, and uses a read-only property for resource.
  • Role → @dataclass, but with field(default_factory=list) for its permissions list to avoid the shared-state trap, plus helper methods like add_permission().
  • User → stays a regular class, because it needs to manage more complex identity rules. It protects _username/_email with property setters, and returns its role list as an immutable tuple so nobody can mess with it directly.

Takeaway: good OOP design should make the right way to use a class easy, and the wrong way basically impossible. Use @dataclass for plain data holders, @property for validation and derived values, and pick your class type based on what the object is actually for.


 

Lesson 3: Identity, Comparisons, and Making Classes Act Like Built-ins

Every Object Has 3 Pillars

  • Identity (is / id()): which exact memory slot is this living in? Are two variables pointing at the literal same object?
  • Type (type() / isinstance()): what kind of thing is it, what can it do?
  • Value (== / __eq__()): what data does it hold? Two different objects can have different identities but the exact same value.

str vs repr

Both turn your object into text, but for different audiences.

  • __repr__(): the developer/debugger view. Detailed, unambiguous, no side effects (e.g. User(user_id=1, username='dara')). Always strip sensitive stuff like passwords here.
  • __str__(): the human-friendly view used by print(), e.g. just dara. If you skip defining __str__, Python just falls back to __repr__.

eq and hash

  • Safe equality. In __eq__(), if the other object isn't the right type, return NotImplemented, never False, never raise an error. That tells Python "let the other side try comparing instead."
  • The hashing rule. If a == b, then hash(a) == hash(b) must also be true, or sets and dicts silently break.
  • The mutable hash trap. Never hash using attributes that can change, like a username. If the data changes after the object's already in a set, its hash changes too, and the object basically vanishes inside the set forever. Only hash stable, immutable identity fields like an ID.

Making Custom Collections Behave Like Lists/Dicts

Instead of exposing your raw internal list directly (where someone could accidentally nuke it), wrap it in a custom class like UserDirectory and hook into the container protocols:

  • __len__(): enables len(directory)
  • __iter__(): enables for user in directory: (just delegate: return iter(self._users))
  • __contains__(): powers the in keyword
  • __getitem__(): enables directory[key], can handle integer index, slices, or string lookup, raising KeyError if missing

Operator Overloading: Keep It Obvious

You can define __or__() so two roles merge with a pipe: admin = manager | security. Golden rule: only overload an operator if the meaning is immediately obvious to any programmer reading it. Never mutate the originals, always return a new combined object. Unsupported type? Return NotImplemented.

Context Managers: Clean Setup/Teardown

with AuditSession("dara") as audit: guarantees cleanup happens no matter what blows up inside the block.

  • __enter__(): grabs the resource, returns the object bound to as
  • __exit__(exc_type, exc_val, tb): closes the resource, logs SUCCESS or ERROR
  • Always return False from __exit__ so real exceptions still bubble up. Returning True silently swallows the error, which you almost never want.

Takeaway: dunder methods give your classes superpowers that make the code feel natural and standard. Keep __repr__ free of secrets, never hash mutable data, delegate to real collections instead of reinventing them, and only overload operators when it's genuinely intuitive.


 

Lesson 4: How Should Classes Relate to Each Other?

The 4 Lenses

Before writing any code, figure out what the relationship actually is:

  • Abstract Base Class (is-a): an explicit family tree. You own the code and want to force every subclass to implement certain methods.
  • Composition (has-a / uses-a): one object holds smaller worker objects and hands off work to them.
  • Mixin (borrowed behavior): a small optional add-on skill plugged into a class, without defining what the class fundamentally is.
  • Protocol (behaves-like): duck typing, made official. The caller only cares whether the object has the method it needs. No shared base class required.

ABCs: The Explicit Contract

Inherit from ABC, mark methods with @abstractmethod. Python then completely blocks you from instantiating a subclass that forgot to implement one of those methods, so you catch the mistake immediately on startup instead of mid-workflow later. You can still bundle default shared logic in there too, like a general validation rule, so subclasses don't repeat themselves.

Composition Over Inheritance: Stop the Fake "is-a"

Bad idea: making NotificationService inherit from EmailChannel. A notification service is not an email channel, it just uses one. The fix is constructor injection: pass the channel in when you build the service, like NotificationService(sender, audit_sink). Big win: you can swap EmailChannel for SmsChannel at runtime with zero rewrites, and testing gets way easier since you can just pass in a FakeSender / MemoryAuditSink instead of hitting real infrastructure.

Mixins: Tiny, Optional, Focused

A mixin gives a class one specific extra skill without shoving it into a rigid hierarchy, like RetryMixin giving something the ability to retry 3 times. Rules:

  • Keep it tiny, one job only
  • Try to keep it stateless, avoid __init__. If you must have one, call super().__init__(*args, **kwargs) cooperatively so you don't break the Method Resolution Order
  • List the mixin first in the class definition: class EmailChannel(RetryMixin, NotificationChannel):

Protocols: Structural Typing

Defined with typing.Protocol. Instead of forcing inheritance from an ABC, a protocol just says "I expect this object to have a .name and a .send() method," that's it. Why it's useful: it works great with legacy or third-party code you can't or don't want to edit to make it inherit from your ABC. @runtime_checkable lets you isinstance(obj, Sender) check that the methods exist, though you still need real tests or a type checker like mypy to verify the signatures actually match.

Mini-Project Layout

  • models.py: small immutable dataclasses (Notification, DeliveryResult)
  • contracts.py: the NotificationChannel ABC (internal family) plus Sender/AuditSink protocols (external boundary)
  • mixins.py: a clean RetryMixin
  • channels.py: SmsChannel (plain ABC subclass) and EmailChannel (RetryMixin + ABC combined)
  • service.py: NotificationService, pure composition, accepts any Sender/AuditSink
  • main.py: proves you can swap EmailChannel, SmsChannel, and even an unrelated LegacyWebhookSender without touching the delivery workflow code

Takeaway: don't reach for inheritance just to save retyping a couple lines. ABCs give you an explicit enforced family tree. Protocols mean you only care what an object can do. Mixins are tiny reusable superpowers. Composition is the glue holding it all together.


 

Lesson 5: Factory Patterns, Separating "Making" From "Using"

The Problem

When you instantiate classes directly (EmailNotifier(), SMSNotifier()) right inside your business logic, you get tight coupling. It breaks the Open/Closed Principle and forces you to keep editing the same code every time a new class shows up. The goal is to separate object creation from object usage: client code should depend on clean abstractions, not import or instantiate concrete classes itself.

The Factory Flavors

Simple Factory. One centralized function or static method with basic if/elif selection logic. Good for a small, stable set (2-3 options). Downside: adding a new class still means editing that conditional.

Registry-Based Factory. Keeps an internal dictionary mapping names to classes (_registry[name] = notifier_class). Great for highly extensible systems since new classes register themselves without you ever touching the core .create() method. This is the true Open/Closed Principle in action.

Factory Method Pattern. A base creator class defines the overall workflow (e.g. export()), and delegates the actual object creation to an abstract method (create_report()). Subclasses override just that piece to supply PDFReport, CSVReport, etc. The Pythonic version of this is @classmethod alt-constructors, things like User.from_username() or User.from_dict(), which give clear named ways to build the same class.

Abstract Factory. An interface that creates a whole family of related products together (e.g. create_button() + create_dialog()). Keeps things consistent: DarkUIFactory only ever makes dark buttons/dialogs, LightUIFactory only light ones, so you never accidentally mix incompatible pieces.

Where This Actually Gets Used

Encapsulating messy setup (RBAC example). Instead of callers manually wiring up users, roles, and permissions themselves, UserFactory.create_admin(username, email) handles building the User, setting up the "Administrator" Role, attaching the right Permissions, and handing back the finished object.

Factory + Dependency Injection. These are two different jobs. The factory decides which class to use and assembles it. DI then hands that assembled object into a consuming service (NotificationService(notifier)). End result: the service that receives it never has to know or care about the concrete type, it just talks through the expected protocol.

Takeaway: a factory is basically a wall between construction and consumption. Let the factory deal with the messy imports, class selection, and default wiring, so the rest of the app stays clean, decoupled, and focused only on the actual business logic.


 

Lesson 6: Decorators, Wrapping Behavior Around Your Functions

The Problem

Mixing logging, validation, and security checks straight into your business functions makes everything cluttered, harder to test, and breaks separation of concerns. The goal is to wrap that reusable cross-cutting stuff (logging, timing, RBAC checks) around the core logic, without ever touching the function body itself.

The Building Blocks

  • Functions as first-class objects. Functions can be stored in variables, passed around, returned from other functions. This is what makes decorators possible in the first place.
  • Closures. An inner function that "remembers" values from the scope it was created in. Useful for configurable wrappers, like a multiplier factory.
  • Basic decorator. Function goes in, wrapper comes out. @simple_decorator runs code before/after the target function runs.
  • The proper template. Always use *args, **kwargs, return result, and @functools.wraps. This preserves the original function's metadata, forwards all arguments correctly, and keeps the return value intact.

Common Cross-Cutting Uses

  • Logging: consistently track start/success/failure
  • Timing: measure how long execution takes
  • Validation: reject bad input (e.g. negative IDs)
  • Authentication vs authorization: authentication is "who are you" (login check), authorization is "are you allowed to do this" (RBAC/permission check)

Parameterized Decorators

Three layers deep: factory, then decorator, then wrapper. Example: @require_permission("user.delete") checks both login and permission before letting the function run.

Decorator Stacking Order

Stacked decorators form a pipeline. @A written above @B means the actual call ends up as A(B(function)).

Class Decorators

Used to register classes into a factory-style registry. Example: @register_notifier("email") drops EmailNotifier into a dictionary for easy extensibility.

Project Layout / Lab

  • decorators/: logging, security, timing, validation
  • models/: RBAC domain objects (User, Role, Permission)
  • services/: business operations wrapped in the reusable decorator rules

Things you'd actually build: @log_action, @measure_time, @require_login, @require_permission(code), @validate_positive_id, @audit_action(name).

Takeaway: a decorator is a clean wrapper around your core logic that adds reusable cross-cutting behavior. Pulling logging, validation, and security out of your business functions makes everything simpler, testable, and easier to maintain, and it enforces RBAC while preserving metadata.


 

Lesson 7: SOLID, Keeping Code From Turning Fragile

The Problem

Code that works fine at first can get fragile fast as requirements grow: big classes, duplicated logic, hidden dependencies all make changes risky and testing slow. The goal is to use SOLID to build designs that are easier to read, test, maintain, and extend, refactoring big classes into smaller, focused services with replaceable dependencies.

The 5 Principles

S, Single Responsibility. A class should only have one reason to change. Problem case: a UserManager that mixes validation, hashing, saving to DB, sending notifications, and auditing all in one place. Fix: split into UserValidator, PasswordHasher, UserRepository, Notifier, AuditLogger, then have a UserService coordinate between them.

O, Open/Closed. Open for extension, closed for modification. Problem case: adding a new notification channel means editing the same function every time. Fix: depend on a Notifier protocol instead, so new channels (Email, SMS, Telegram) can be added without ever touching NotificationService.

L, Liskov Substitution. A subtype should be safely swappable for its base type. Good: EmailNotifier and SMSNotifier both properly implement Notifier.send(). Bad: ReadOnlyExporter inherits FileExporter but throws an error when you call export(), which breaks the substitution promise.

I, Interface Segregation. Clients shouldn't be forced to depend on methods they don't use. Problem case: one giant interface forces classes to implement stuff totally irrelevant to them. Fix: split it into smaller, focused protocols, like EmailSender, ReportGenerator, BackupService.

D, Dependency Inversion. High-level modules should depend on abstractions, not concrete classes. Problem case: UserService directly creates a SQLiteUserRepository itself. Fix: inject a UserRepository protocol instead, so now you can swap in a fake or in-memory repo for testing.

Refactoring in Practice

Workflow. Write tests, refactor in small steps, run tests, review the design, repeat. Good refactoring keeps behavior stable, improves naming, cuts duplication, and makes testing easier. Bad refactoring silently changes requirements or adds complexity nobody asked for.

Before/after example. UserManager mixing validation, DB, email, and audit all together gets split into separate helpers (UserValidator, PasswordHasher, UserRepository, EmailNotifier, AuditLogger) with UserService coordinating the whole workflow.

Why it helps testing. Injected dependencies let you swap in fakes, like FakeUserRepository, which skips the real database entirely, so tests for things like duplicate usernames or bad input run fast.

Takeaway: SOLID is a thinking tool, not a magic fix. SRP means one reason to change. OCP means extend safely. LSP means substitute safely. ISP means small focused contracts. DIP means depend on abstractions.


 

Lesson 8: Behavioral Patterns, Strategy, Template Method, Command

The Problem

Business logic gets rigid when it's full of giant if/elif chains, hard-coded algorithms, and workflows/UI actions that are too tightly coupled. The goal is to use behavioral patterns to organize changing behavior without breaking your core classes, enabling runtime flexibility, cleaner workflows, and testable actions.

Strategy Pattern

Encapsulates interchangeable algorithms behind one common contract. It's composition-based: the context object delegates behavior to a strategy object, and you can swap algorithms at runtime without rewriting the context. Example: DiscountStrategy with StudentDiscount, VIPDiscount, NoDiscount. Good for payments (Cash/Card/QR), discounts, policies. Key question to ask: which behavior or algorithm should run?

Template Method Pattern

Defines a stable, fixed workflow in a base class, but lets subclasses override specific steps. It's inheritance-based: the overall sequence stays the same, but individual steps vary. Example: ReportExporter with CSVExporter and JSONExporter. Good for export/import workflows, data pipelines. Key question: which steps in the workflow actually vary?

Command Pattern

Turns a request or action into an actual object. Three pieces:

  • Command: exposes execute() and undo()
  • Receiver: does the real work
  • Invoker: manages execution, history, queueing, and undo

Example: RenameTaskCommand stores the old/new state and supports undo. Good for task operations, history tracking, undo/redo, queued actions. Key question: how should a request be represented and managed?

Comparing the Three

  • Strategy: interchangeable algorithms, composition, runtime flexibility
  • Template Method: stable workflow, inheritance, individual steps vary
  • Command: actions turned into objects, supports history/queue/undo

How They Fit Together

Strategy organizes algorithms (discounts, payments). Template Method organizes workflows (report exports). Command organizes actions (task ops with undo). Put together, the UI/client issues commands, the service applies strategies, and reports follow the template workflow.

Takeaway: use Strategy when multiple algorithms need to be interchangeable (prefer composition). Use Template Method when the workflow is stable but individual steps vary (use inheritance carefully). Use Command when actions need history, queueing, logging, or undo, wrapping requests as objects.

 

OOP II_Homework_Kang Soksambathratanak.pdf
PDF ឯកសារ
ទាញយក អាន
ចែករំលែក
រក្សាទុក
ការផ្សព្វផ្សាយ
ការផ្សព្វផ្សាយ
មតិយោបល់ 0
ចូលគណនី ដើម្បីបញ្ចេញមតិ