លំហសិក្សាកម្ពុជា
V1.0
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 |
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
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:
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:
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.
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:
@property: Keep the Nice Syntax, Add Protection Underneath
Without @property, anyone can just do user.email = "junk" and blow up your program later.
Upgrading the RBAC From Lesson 1
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.
Every Object Has 3 Pillars
str vs repr
Both turn your object into text, but for different audiences.
eq and hash
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:
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.
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.
The 4 Lenses
Before writing any code, figure out what the relationship actually is:
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:
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
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.
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.
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
Common Cross-Cutting Uses
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
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.
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.
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:
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
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.
មតិយោបល់ 0