Homework
Name: Lim Meng Huy
ID: ST-RE00106
Professor: Sek Socheat
LESSON 1
1. Thinking in Objects, Not Just Steps
One of the most important concepts in Object-Oriented Programming (OOP) is learning to think in terms of objects rather than simply following a sequence of steps. Beginners often approach programming by asking, "What should happen first, second, and third?" While this procedural way of thinking works for small programs, it becomes difficult to manage as applications grow larger and more complex.
OOP introduces a different mindset. Instead of focusing only on execution order, developers should ask questions such as: What objects exist in the system? What information does each object own? What responsibilities should each object manage? By organizing software around objects and responsibilities, code becomes easier to understand, maintain, and extend over time.
• Class vs. Object:
A class acts as a blueprint or template that defines the structure and behavior of an object. An object, on the other hand, is a real instance created from that blueprint. For example, a User class defines what a user should contain, while actual users such as Dara and Sophea are objects created from that class.
• self:
The keyword self refers to the current object instance. It allows Python to know which object's data should be accessed or modified. If there are two users in a system, self ensures that Dara's email information remains separate from Sophea's data.
• Instance Variables vs. Class Variables:
Instance variables belong to individual objects. Each user may have a unique email address, username, or phone number, so these values are stored using self. Class variables belong to the class itself and are shared by all objects. For example, tracking the total number of registered users would be done using a class variable because the information applies to the entire class rather than a single object.
2. The Three Flavors of Methods
Python provides three different types of methods that can be attached to a class. Although all of them are functions, each serves a different purpose and helps organize code more effectively.
• Instance Method (self):
Instance methods are the most common type of method. They receive self as their first parameter and are used to read or modify data stored within a specific object. For example, a display() method might show user information, while an update_email() method might change the user's email address.
• Class Method (@classmethod with cls):
Class methods work with the class itself instead of a specific object. They receive cls as the first parameter. A common use case is creating alternative constructors that build and return objects. For example, a class method may accept a username and automatically append @school.edu.kh before creating a new User object.
• Static Method (@staticmethod):
Static methods do not receive either self or cls. They behave like regular helper functions but are grouped inside a class because they are logically related to it. An example would be validating whether a username contains at least three characters before allowing registration.
3. The Mini Project: How Objects Work Together
To demonstrate the benefits of object-oriented design, the lesson introduces a simple Role-Based Access Control (RBAC) system. Instead of storing every permission directly inside a user object, responsibilities are distributed among several classes.
• Permission:
Permission is the smallest building block in the system. It represents a single capability such as "user.delete" or "user.create".
• Role:
A Role contains a collection of permissions. For example, an Admin role may include permissions for creating, updating, viewing, and deleting users.
• User:
A User contains one or more roles rather than directly storing permissions.
When the program executes:
user.has_permission("user.delete")
the user asks its assigned roles whether they contain the requested permission. The roles then examine their permissions and return either True or False.
This demonstrates abstraction. The main program does not need to understand how permissions are checked internally. It simply asks a question and receives an answer.
4. Structuring the Project
As software grows larger, storing everything in a single file becomes difficult to manage. The lesson demonstrates a cleaner project structure.
Typical structure:
models/
user.py
role.py
permission.py
__init__.py
main.py
The models directory contains the application's core objects, while main.py serves as the entry point where objects are created and tested.
The Takeaway
Good software engineering is not simply about making code work. It is about writing code that remains maintainable as requirements grow. Clear naming, proper file organization, data cleaning, and allowing objects to manage their own responsibilities all contribute to building scalable software.
LESSON 2
1. Data-Centric vs. Behavior-Centric: The Big Design Choice
Not all classes serve the same purpose. Some classes are primarily responsible for storing information, while others exist to manage actions, workflows, and business rules.
• Data-Centric Classes (Use @dataclass):
Data-centric classes mainly store information. Examples include Permission objects, configuration records, and database entities. Writing constructors, string representations, and comparison methods manually for these classes often leads to unnecessary repetitive code. The @dataclass decorator automatically generates this functionality.
• Behavior-Centric Classes (Use a Regular Class):
Behavior-centric classes focus more on actions and rules than simple storage. A User class that manages authentication, permissions, and validations is a good example. These classes generally require greater control over how state changes and how methods operate.
2. The Power (and Traps) of @dataclass
The @dataclass decorator can significantly reduce boilerplate code by automatically generating useful methods.
• Automatically Generated Methods:
Dataclasses automatically create methods such as init(), repr(), and eq(). This saves time and improves readability.
• The Mutable Default Trap:
Developers should never define lists like this:
permissions: list = []
Doing so causes every object to share the same list. Instead, field(default_factory=list) should be used so every instance receives its own independent list.
• Ordering Rule:
Required fields must always appear before fields that contain default values.
• post_init():
This method executes immediately after the auto-generated constructor. It is an ideal location for data validation, normalization, and cleaning before invalid data spreads throughout the system.
• Advanced Features:
- frozen=True creates immutable objects.
- slots=True reduces memory consumption and prevents accidental attribute creation.
- kw_only=True forces developers to use keyword arguments, making object creation clearer and safer.
3. @property: Keeping Attribute Syntax While Adding Defense
Properties allow developers to expose values like normal attributes while secretly running validation logic behind the scenes.
• Getter and Setter Pattern:
External code can access data using simple syntax such as user.email, while validation occurs automatically within the property methods.
• The Infinite Loop Trap:
Inside a setter, developers must assign values to a private variable such as self._email. Assigning directly to self.email will repeatedly call the setter, causing infinite recursion.
• Computed and Read-Only Properties:
Some values should not be stored directly because they can be calculated when needed. Examples include role_count or display_name. Read-only properties ensure that these values always remain accurate and consistent.
4. Putting It Together: Upgrading the RBAC Model
The Role-Based Access Control system from Lesson 1 is redesigned using dataclasses and properties.
• Permission (@dataclass(frozen=True)):
The Permission object becomes immutable. It automatically normalizes and validates permission codes inside post_init().
• Role (@dataclass):
Role stores permissions safely using field(default_factory=list) and provides helper functions such as add_permission().
• User (Regular Class):
The User class remains a traditional class because it manages complex behavior and business rules. It protects attributes using properties and safely delegates authorization checks through roles.
The Takeaway
Good OOP design makes valid use easy and invalid use difficult. Dataclasses simplify data modeling, properties enforce correctness, and choosing the right class structure improves maintainability and reliability.
LESSON 3
1. The Core Object Model: Identity, Type, and Value
Every object in Python is built upon three fundamental concepts.
• Identity:
Identity determines the exact memory location of an object. It can be checked using is or id().
• Type:
Type determines what kind of object something is and what operations it supports. Python provides type() and isinstance() for this purpose.
• Value:
Value represents the actual information stored within an object. Two different objects may contain identical values while still remaining separate entities.
2. Looking Good: str vs. repr
Python provides two special methods for converting objects into text.
• repr() (For Developers):
This representation is intended for debugging and development. It should provide enough information to clearly describe the object's state while avoiding sensitive data such as passwords and API tokens.
• str() (For Humans):
This representation provides a clean and user-friendly output suitable for printing and user interfaces. If str() is not defined, Python automatically falls back to repr().
3. Comparing and Hashing: eq and hash
• Safe Equality:
When implementing eq(), developers should return NotImplemented whenever the other object is incompatible. This allows Python to continue comparison appropriately.
• The Hashing Rule:
If two objects are considered equal, their hash values must also be equal.
• The Mutable Hash Trap:
Hash values should never depend on mutable attributes. If mutable data changes after an object is placed inside a set or dictionary, the data structure can become corrupted. Hash calculations should be based only on stable, immutable values.
4. Custom Containers: Making Classes Behave Like Lists and Dictionaries
Instead of exposing raw internal collections, developers can provide custom container classes that support Python protocols.
• len():
Allows len(directory).
• iter():
Allows looping with for user in directory.
• contains():
Allows use of the in keyword.
• getitem():
Enables indexing and key-based lookups such as directory[0] or directory["dara"].
These methods allow custom classes to behave naturally alongside Python's built-in collections.
5. Operator Overloading: Keeping It Natural
Python allows operators to be customized.
For example:
admin = manager | security
can be implemented through the or() method to merge roles.
Operator overloading should only be used when the meaning is obvious and intuitive. Operators should generally return new objects rather than modifying existing ones.
6. Context Managers: Clean Setup and Teardown
Context managers simplify resource handling using the with statement.
• enter():
Acquires resources and returns the object that will be assigned after the as keyword.
• exit(exc_type, exc_val, tb):
Releases resources, performs cleanup, and records whether execution ended successfully or with an error.
• Handling Errors:
Returning False from exit() ensures that genuine exceptions continue propagating to the caller instead of being silently ignored.
The Takeaway
Python's special methods, often called dunder methods, provide powerful ways to integrate custom classes with built-in language behaviors. Proper implementation results in code that feels natural, readable, and fully aligned with Python conventions.
LESSON 4
1. The Four Lenses of Object Relationships
Before writing code, developers should first understand the relationship between objects. Choosing the wrong relationship often leads to unnecessary complexity, rigid design, and difficult maintenance. This lesson introduces four common ways objects can relate to one another.
• Abstract Base Class (ABC) - "is-a" Relationship:
An Abstract Base Class represents a family tree of related objects. It defines a common contract that all subclasses must follow. For example, EmailChannel and SmsChannel may both be considered NotificationChannels because they belong to the same family and provide similar behavior.
• Composition - "has-a" or "uses-a" Relationship:
Composition occurs when one object contains or uses another object to perform work. Instead of inheriting behavior, the object delegates tasks to helper objects. For example, a NotificationService may use an EmailChannel to send emails. The service is not an email channel itself; it simply uses one.
• Mixin - Behavior Reuse:
A mixin provides a small reusable capability that can be added to many classes. It does not define the identity of a class, but instead provides an extra feature. For example, a RetryMixin may add retry functionality to any communication channel.
• Protocol - "Behaves-Like" Relationship:
Protocols focus on behavior rather than inheritance. If an object provides the required methods, it can be used even if it does not inherit from the same parent class. This allows greater flexibility and encourages loosely coupled designs.
2. Abstract Base Classes (ABCs): The Explicit Contract
Abstract Base Classes allow developers to define strict rules for subclasses.
• Enforcing Required Methods:
Using abc.ABC and @abstractmethod allows developers to specify methods that every subclass must implement.
• Catching Errors Early:
Python prevents the creation of an object if a required abstract method has not been implemented. This helps identify design mistakes early in development.
• Shared Functionality:
ABCs can contain both abstract methods and fully implemented methods. Shared functionality can be placed in the base class so subclasses do not duplicate common logic.
3. Composition over Inheritance
One of the most valuable design principles is preferring composition over inheritance whenever possible.
• The Wrong Approach:
Making NotificationService inherit from EmailChannel creates an incorrect relationship because a notification service is not an email channel.
• The Better Approach:
NotificationService should receive an EmailChannel object through its constructor and delegate email operations to it.
• Benefits:
- Components become interchangeable.
- Different channel types can be substituted easily.
- Unit testing becomes easier.
- Systems become more flexible and maintainable.
4. Mixins: Focused and Reusable Behavior
Mixins allow small pieces of functionality to be reused across unrelated classes.
• Example:
A RetryMixin might provide automatic retry behavior for failed network requests.
• Best Practices:
- Keep mixins focused on a single responsibility.
- Avoid storing state whenever possible.
- Avoid large or complex mixins.
- Use cooperative inheritance when implementing constructors.
• Multiple Inheritance:
Mixins are often placed before the main base class in the inheritance list to ensure Python's Method Resolution Order (MRO) behaves correctly.
5. Protocols: Formal Duck Typing
Protocols bring formal structure to the concept of duck typing.
• Duck Typing Concept:
"If it walks like a duck and quacks like a duck, treat it as a duck."
• Protocol Benefits:
Objects only need to provide the required methods and attributes. They do not need to inherit from a specific base class.
• Real-World Usage:
Protocols are particularly useful when integrating third-party libraries or legacy systems where modifying source code is not possible.
• Runtime Checking:
The @runtime_checkable decorator allows isinstance() checks against protocols, although static type checking tools remain important.
6. The Mini-Project Architecture
This lesson combines all concepts into a clean notification system.
Project structure:
models.py
contracts.py
mixins.py
channels.py
service.py
main.py
• models.py:
Contains immutable data objects such as Notification and DeliveryResult.
• contracts.py:
Defines shared contracts using ABCs and Protocols.
• mixins.py:
Contains reusable behavior such as RetryMixin.
• channels.py:
Implements concrete notification channels.
• service.py:
Coordinates communication using composition.
• main.py:
Demonstrates how different notification mechanisms can be swapped without modifying business logic.
The Takeaway
Inheritance should not be used simply to avoid typing a few extra lines of code. ABCs provide strict contracts, Protocols provide flexibility, Mixins provide reusable behavior, and Composition connects everything together in a clean and maintainable design.
LESSON 5
1. Core Focus and The Object Creation Problem
As applications grow, object creation can become scattered throughout the codebase. Directly creating objects inside business logic leads to strong coupling and reduced flexibility.
• The Core Problem:
When business code directly creates objects such as EmailNotifier() or SMSNotifier(), every new notifier type requires modifying existing code.
• The Design Goal:
Separate object creation from object usage. Business logic should depend on abstractions rather than specific implementations.
2. Key Factory Patterns and Implementations
A. Simple Factory
The Simple Factory centralizes object creation.
• Mechanism:
A method receives input and returns the appropriate object.
Example:
create_notifier("email")
returns an EmailNotifier object.
• Advantages:
- Easy to understand.
- Centralized creation logic.
- Reduces duplicate code.
• Limitation:
Every new type still requires modification of the factory's conditional statements.
B. Registry-Based Factory
Registry-based factories improve extensibility.
• Mechanism:
A dictionary maps names to classes.
Example:
_registry["email"] = EmailNotifier
• Benefits:
- New classes can register themselves.
- Factory logic remains unchanged.
- Supports the Open/Closed Principle.
C. Factory Method Pattern
Factory Method allows subclasses to control object creation.
• Mechanism:
A base class defines a workflow while subclasses decide which object to create.
• Example:
Report exporters may share an export workflow while choosing different report formats.
• Alternative Constructors:
Class methods such as User.from_dict() provide alternative ways to build objects.
D. Abstract Factory
Abstract Factory creates families of related objects.
• Example:
A DarkUIFactory creates dark buttons and dark dialogs.
A LightUIFactory creates light buttons and light dialogs.
• Benefits:
- Guarantees compatibility within product families.
- Avoids mixing inconsistent components.
3. Practical Applications and Integration
A. Complex Setup Encapsulation
Factories simplify complex object creation.
Instead of manually creating users, roles, and permissions every time, a factory can assemble everything automatically.
Example:
UserFactory.create_admin()
returns a fully prepared administrator account.
B. Factory and Dependency Injection
Factories and Dependency Injection complement each other.
• Factory responsibilities:
- Selecting implementations.
- Creating objects.
- Applying default configurations.
• Dependency Injection responsibilities:
- Providing created objects to services.
This separation improves maintainability and testability.
The Takeaway
Factories act as a barrier between object creation and object usage. They hide construction details and allow business logic to remain clean, flexible, and focused on solving domain problems.
LESSON 6
1. Core Focus and The Separation Problem
Modern applications often require logging, validation, authorization, monitoring, and auditing. Embedding these concerns directly inside every business function quickly creates clutter.
• The Core Problem:
Business methods become difficult to read and test when unrelated concerns are mixed together.
• The Design Goal:
Separate reusable cross-cutting functionality from core business logic.
2. Key Decorator Concepts and Implementations
A. Functions as First-Class Objects
Python treats functions as ordinary values.
Functions can:
- Be assigned to variables.
- Be passed as arguments.
- Be returned from other functions.
This capability forms the foundation of decorators.
B. Closures
A closure occurs when an inner function remembers values from an outer function.
This allows decorators to maintain configuration information even after the outer function has finished executing.
C. Basic Decorator
A decorator wraps another function and adds behavior before or after execution.
Example:
@simple_decorator
allows additional functionality without modifying the original code.
D. Professional Decorator Template
A professional decorator should:
- Accept *args and **kwargs.
- Return results correctly.
- Use @wraps.
- Preserve metadata.
This ensures compatibility and maintainability.
E. Cross-Cutting Concerns
Decorators commonly handle:
• Logging:
Tracking execution flow.
• Timing:
Measuring execution duration.
• Validation:
Checking input correctness.
• Authentication:
Verifying user identity.
• Authorization:
Checking permissions before allowing access.
F. Parameterized Decorators
Some decorators require configuration.
Example:
@require_permission("user.delete")
The decorator receives a parameter and uses it during execution.
G. Decorator Order
Decorator stacking follows a nested execution model.
Example:
@A
@B
function()
becomes:
A(B(function))
Understanding this order is essential when combining behaviors.
H. Class Decorators
Decorators can also modify classes.
A common use case is automatically registering classes inside factory registries.
3. Practical Applications and Integration
Project structure:
decorators/
models/
services/
Students implement:
• @log_action
Tracks execution events.
• @measure_time
Measures performance.
• @require_login
Blocks unauthorized access.
• @require_permission
Enforces RBAC permissions.
• @validate_positive_id
Validates identifiers.
• @audit_action
Records security-sensitive operations.
The Takeaway
Decorators provide a powerful mechanism for attaching reusable behavior around existing functions. They improve separation of concerns, reduce code duplication, and keep business logic focused on its primary responsibility.
LESSON 7
1. Core Focus and The Maintainability Problem
Software often becomes difficult to maintain as new requirements are added. Large classes accumulate responsibilities, duplicated logic spreads through the project, and dependencies become tightly coupled.
• The Core Problem:
Growing complexity makes systems fragile and difficult to modify.
• The Design Goal:
Apply SOLID principles to create systems that are easier to read, test, extend, and maintain.
2. Key SOLID Principles and Implementations
A. Single Responsibility Principle (SRP)
• Rule:
A class should have only one reason to change.
• Problem:
A single UserManager may perform validation, storage, notifications, and auditing simultaneously.
• Solution:
Split responsibilities into focused classes.
Examples:
- UserValidator
- PasswordHasher
- UserRepository
- AuditLogger
- Notifier
B. Open / Closed Principle (OCP)
• Rule:
Software should be open for extension but closed for modification.
• Problem:
Every new notification type requires editing existing business logic.
• Solution:
Depend on abstractions rather than concrete implementations.
New notifiers can be added without changing existing code.
C. Liskov Substitution Principle (LSP)
• Rule:
Subtypes must behave correctly when substituted for base types.
• Good Example:
EmailNotifier and SMSNotifier both fulfill the Notifier contract.
• Bad Example:
A subclass that throws exceptions when required methods are called violates LSP.
D. Interface Segregation Principle (ISP)
• Rule:
Clients should not depend on methods they do not use.
• Problem:
Large interfaces force unnecessary implementations.
• Solution:
Break large interfaces into smaller focused contracts.
E. Dependency Inversion Principle (DIP)
• Rule:
High-level modules should depend on abstractions.
• Problem:
UserService directly creates database objects.
• Solution:
Inject repository abstractions.
This allows fake repositories to replace real databases during testing.
3. Practical Applications and Integration
A. Refactoring Workflow
Typical process:
1. Write tests.
2. Refactor small sections.
3. Run tests.
4. Review results.
5. Repeat.
Refactoring should improve structure while preserving behavior.
B. Before and After Architecture
Before:
- Large monolithic classes.
- Tight coupling.
- Difficult testing.
After:
- Smaller focused services.
- Clear responsibilities.
- Replaceable dependencies.
C. Benefits for Testing
Dependency injection allows mock and fake implementations.
Benefits:
- Faster tests.
- Reduced setup complexity.
- Better isolation.
- Improved reliability.
The Takeaway
SOLID principles provide a framework for building maintainable software.
SRP = One reason to change.
OCP = Extend without modifying.
LSP = Subtypes remain compatible.
ISP = Small focused contracts.
DIP = Depend on abstractions.
LESSON 8
1. Core Focus and The Behavioral Problem
Systems often become rigid when behavior is embedded directly into classes using long chains of conditions and tightly coupled workflows.
• The Core Problem:
Business rules become difficult to modify or extend.
• The Design Goal:
Use behavioral patterns to organize changing behavior in a flexible and maintainable way.
2. Key Behavioral Patterns and Implementations
A. Strategy Pattern
The Strategy Pattern encapsulates algorithms inside separate objects.
• Purpose:
Allow algorithms to be swapped at runtime.
• Example:
Different discount calculations:
- StudentDiscount
- VIPDiscount
- NoDiscount
• Advantages:
- High flexibility.
- Easier testing.
- Reduced conditional logic.
• Key Question:
Which algorithm should be executed?
B. Template Method Pattern
Template Method defines a standard workflow while allowing subclasses to customize individual steps.
• Purpose:
Maintain consistent workflows while supporting specialized behavior.
• Example:
Report exporting process.
Base class:
ReportExporter
Subclasses:
- CSVExporter
- JSONExporter
• Key Question:
Which steps should vary while preserving the overall workflow?
C. Command Pattern
The Command Pattern represents actions as objects.
• Components:
Command:
Defines execute() and undo().
Receiver:
Performs actual work.
Invoker:
Stores and executes commands.
• Example:
RenameTaskCommand stores both old and new task names and supports undo functionality.
• Benefits:
- History tracking.
- Undo and redo.
- Queuing.
- Logging.
• Key Question:
How should actions be represented and managed?
3. Practical Applications and Integration
A. Pattern Comparison
Strategy Pattern:
- Algorithm focused.
- Uses composition.
- Runtime flexibility.
Template Method:
- Workflow focused.
- Uses inheritance.
- Controlled customization.
Command Pattern:
- Action focused.
- Uses encapsulated requests.
- Supports history and undo.
B. Combined Architecture
A complete system may use all three patterns together.
Example:
• Commands manage user actions.
• Strategies determine business algorithms.
• Template methods control reporting workflows.
This creates a flexible architecture that remains organized as requirements grow.
The Takeaway
Behavioral design patterns help software adapt to change.
• Strategy Pattern:
Use when multiple algorithms must be interchangeable.
• Template Method Pattern:
Use when workflows remain stable but individual steps vary.
• Command Pattern:
Use when actions require history tracking, logging, queuing, or undo functionality.
Together, these patterns improve flexibility, maintainability, and code organization while keeping business logic clean and reusable.
មតិយោបល់ 0