លំហសិក្សាកម្ពុជា
V1.0
Final Homework for Advance OOP. The summarize of 8 lessons
| Student Name | Rith Tola |
|---|---|
| Student ID | ST-RE00008 |
| Professor | Sek Socheat |
This lesson introduces the basics of object-oriented programming (OOP) and explains how it differs from procedural programming. Instead of keeping data and functions separate, OOP organizes related data and behavior into classes and objects. Students learn the main concepts and terminology of OOP by building a simple Role-Based Access Control (RBAC) model using three classes: User, Role, and Permission. This project provides practical experience in creating classes, working with objects, and organizing code into separate files.
Class: A blueprint used to create objects.
Object: An instance created from a class.
Attribute: A variable that stores information about an object or a class.
Method: A function defined inside a class.
self: A reference to the current object, used to access its attributes and methods.
Responsibility: The specific task or purpose assigned to a class.
Instance attributes belong to individual objects, so each object can store different values. They are usually initialized inside the initialization method using self. Class attributes are defined directly inside the class and are shared by its instances unless an instance overrides the attribute. For example, each user can have a different username, while a shared system name can be defined as a class attribute.
Instance method: Uses self to access or modify information belonging to a specific object. For example, user.display_info().
Class method: Uses cls to work with the class itself. It is often used to create objects in alternative ways and is defined with the @classmethod decorator. For example, User.from_username().
Static method: Does not require self or cls. It performs a related utility task and is defined with the @staticmethod decorator. For example, User.is_valid_username().
The main project for the early lessons is a simple Role-Based Access Control model. The system has three main classes:
User: Represents a person who uses the system and can be assigned one or more roles.
Role: Represents a set of responsibilities or access rights and contains a list of permissions.
Permission: Represents an allowed action, such as viewing, creating, editing, or deleting information.
A user can perform an action when at least one of their assigned roles contains the required permission. This design demonstrates composition, also known as a has-a relationship: a user has roles, and a role has permissions. It does not require inheritance to represent these relationships.
A well-organized object-oriented project separates code according to its purpose rather than placing everything in one file.
main.py: The entry point of the program. It creates objects and demonstrates how the system works.
models/: A package that contains the classes used in the project.
models/user.py: Defines the User class.
models/role.py: Defines the Role class.
models/permission.py: Defines the Permission class.
models/init.py: Marks the directory as a regular Python package and can expose selected classes for easier importing.
A module is a single Python file, while a package is a directory used to organize related modules. The import statement allows code from one module to be used in another. For example, importing User from the models package makes the class available.
Writing code that works is important, but writing code that is easy to understand and maintain is equally valuable.
Type hints: Indicate the expected types of values, such as username: str.
Docstrings: Explain the purpose of a class, method, or function in a few clear sentences.
Naming conventions: Use PascalCase for class names and snake_case for methods and variables.
Clear responsibilities: Give each class and method a specific purpose to make the code easier to test and maintain.
OOP helps organize a program by grouping related data and behavior into classes. Each class should have a clear responsibility, and its attributes and methods should support that purpose. Understanding classes, objects, and composition provides a foundation for building larger applications that are easier to maintain and extend.
This lesson builds on the object-oriented programming concepts introduced in Lesson 1 by exploring how to create classes that are easier to manage and maintain. It introduces Python decorators and features that reduce repetitive code and control how attributes are accessed and updated. Students learn how to decide whether a class should mainly store information or manage specific behaviors and rules.
Data-centric class: Mainly stores related information with simple validation or formatting. These classes are often suitable for data class decorators.
Behavior-centric class: Focuses on managing information, enforcing rules, and performing actions. These classes may be better implemented as regular Python classes.
The @dataclass decorator automatically generates common methods such as initialization, representation, and equality checks based on the fields defined in a class. This reduces repetitive code and makes data-focused classes easier to write. Other features, such as sorting order, can be enabled with options. Required fields should generally be declared before fields with default values to avoid errors.
Using a mutable object such as a list, dictionary, or set as a shared default can cause problems because different instances may unintentionally share the same data. In data classes, directly using mutable defaults is rejected. The recommended solution is default_factory, which creates a new object for each instance.
default_factory: Creates a fresh default value for each instance, such as an individual collection.
init=False: Excludes a field from the automatically generated constructor.
repr=False: Hides a field from the generated object representation.
compare=False: Excludes a field from equality comparisons and generated ordering comparisons.
metadata: Attaches additional descriptive information to a field for tools or applications that use it.
Post initialization: Runs after the generated constructor initializes the fields. It can be used to validate values, normalize text, or raise errors when the supplied information is invalid.
Class variables: Identify variables shared by the class rather than ordinary data class instance fields. They should be annotated with ClassVar when appropriate.
Initialization variables: Use InitVar to define temporary values passed to the post-initialization method without being stored as regular data class fields.
Frozen: Prevents normal reassignment of fields after an object is created, making it useful for stable value objects.
Slots: Restricts instances to declared attributes and can reduce memory usage in some situations.
Keyword-only: Requires fields to be passed by name when creating an object, making constructor calls easier to read and reducing mistakes caused by argument order.
The @property decorator allows attributes to be accessed using normal attribute syntax while adding validation or other logic behind the scenes. A property setter can remove unnecessary spaces, convert text to lowercase, and check format validity before saving values. A property can also calculate information without storing it separately. A property with no setter is read-only through normal attribute assignment.
Public field: Use it when information can be accessed directly without special rules.
Property: Use it when an attribute needs validation, normalization, or a calculated value.
Method: Use it for an action, an operation that takes parameters, or a process that changes the object's state.
Choose class features based on what the class needs to do. Dataclasses reduce repetitive code when storing information, while properties help control how attributes are accessed or modified. These tools make classes easier to use and help prevent invalid data from entering the system.
This lesson explains how Python uses special methods, also known as dunder methods, to make objects work with familiar syntax such as printing, length checking, membership testing, indexing, operators, and context management. Students learn how to implement these methods so that custom classes behave naturally and consistently.
Every Python object has three important characteristics:
Identity: Identifies a specific object and remains constant throughout its lifetime. The id() function returns its identity, and the is operator checks whether two references point to the same object.
Type: Describes what kind of object it is and what operations it supports. The type() function identifies its type, while isinstance() checks whether an object belongs to a particular class or an appropriate subclass.
Value: Represents the information stored in an object.
Aliasing happens when two variables refer to the same object, meaning changes made through one variable may also be visible through the other.
When accessing an attribute, Python searches for it in the instance attributes first, then in the class, and finally through its base classes according to the Method Resolution Order (MRO). If Python cannot find the requested attribute, it raises an AttributeError.
Python built-in functions and language features rely on special methods to determine how custom objects behave:
Text representation: Implemented through __str__() and __repr__().
Equality and hashing: Implemented through __eq__() and __hash__().
Containers and iteration: Implemented through methods such as __len__(), __iter__(), __contains__(), and __getitem__().
Operators and context: Implemented through operator-overloading methods and context-management methods such as __enter__() and __exit__().
User representation: __str__() provides readable, user-friendly information commonly used by print().
Developer representation: __repr__() provides a more detailed representation useful for debugging and understanding an object's contents.
By default, separate instances of a regular Python class are compared by identity unless equality behavior is customized. A class can define custom equality to compare objects using meaningful information. The hashing contract states that if two objects compare equal, they must have the same hash value. Hash values must not depend on mutable information that could change while the object is stored in a set or used as a dictionary key.
A custom collection can behave like a built-in Python collection by implementing length, iteration, membership, and indexing protocols. Operator overloading allows a class to define what familiar operators mean when used with its objects, such as combining compatible items. When an operation does not support a particular operand type, the method can return NotImplemented to allow Python to try an alternative operation.
Context managers help Python manage resources safely by using dedicated statement blocks. They are useful when a program needs to open files or perform setup and cleanup reliably. The __enter__() method performs setup, while __exit__() runs when the block finishes, allowing cleanup and appropriate exception handling.
Special methods allow custom objects to work naturally with Python's built-in syntax and operations. By implementing the appropriate methods, developers can make their classes easier to use, while following consistent rules for equality, iteration, operators, and resource management.
This lesson explores how objects can work together without depending too heavily on one another. It introduces four important object-oriented programming techniques: abstract base classes, composition, mixins, and structural protocols.
Is-a: A relationship where one class is a specific type of another. This relationship may be suitable for inheritance or an abstract base class.
Has-a: A relationship where an object contains or owns another object. This is commonly represented through composition.
Uses-a: A relationship where an object relies on another object to perform a task.
Behaves-like: A relationship where an object provides the required behavior without necessarily inheriting from a particular class.
An abstract base class defines a common structure that related classes are expected to follow. Abstract base classes use the @abstractmethod decorator to identify required methods. If a subclass does not implement all required abstract methods, Python prevents it from being instantiated.
Inheritance: Allows a class to reuse and extend behavior, representing clear type hierarchies, but can create tight coupling to parent designs.
Composition: Allows a class to work with other objects by keeping them as collaborators, making dependencies easier to replace and test. Constructor injection passes dependencies into an object when it is created rather than having the object build them internally.
A mixin provides a small, reusable capability that can be added to a class through multiple inheritance without representing the main class identity. Python uses the Method Resolution Order to determine how methods are found. Mixins should remain focused and simple, handling initialization cooperatively when necessary.
A structural protocol defines the methods or attributes an object must provide to be used in a particular situation. This approach allows unrelated or third-party classes to satisfy requirements without inheriting from a shared base class. Runtime checks can verify the presence of required members in supported cases, though static type checking and tests remain important.
Choose relationships based on how objects actually work together. Use inheritance when one class is genuinely a type of another, and use composition when an object needs help from other objects. Abstract classes and protocols define expected behavior, while mixins provide small reusable capabilities. Choosing the right approach reduces unnecessary dependencies and makes code easier to test and extend.
This lesson explores how factories help manage object creation in object-oriented programming. When a program creates many different types of objects directly, the code can become difficult to maintain, extend, and test. Factory patterns solve this problem by moving object creation decisions into dedicated functions or classes.
Creating objects directly throughout a program can lead to high coupling, repeated modifications when adding new types, difficult testing, and poor maintainability.
Simple Factory: Centralizes object creation in one function or class method based on input parameters.
Factory Method: Allows subclasses to decide which specific object should be created while keeping the overall creation workflow consistent.
Registry-Based Factory: Stores available object types in a dictionary mapping names to classes or creation functions, avoiding long conditional selection chains and supporting the Open/Closed Principle.
Abstract Factory: Creates families of related objects that are designed to work together consistently.
A factory decides which concrete object to create, whereas dependency injection passes an already-created object into another class. This separation allows services to depend on common interfaces or expected behaviors rather than specific implementations.
Factory patterns separate object creation from the main business logic. A simple factory centralizes creation, a factory method lets subclasses decide which object to create, and an abstract factory creates related groups of objects. Use factories when object creation becomes complicated or needs to change without requiring modifications throughout the application.
Decorators add reusable features to functions, such as logging, timing, validation, and access control, without changing the main function code.
First-class functions: Functions can be stored in variables, passed to other functions, or returned from them.
Closures: Inner functions can remember values from their enclosing functions, even after the outer function has finished executing.
Decorators: A decorator takes a function, wraps extra behavior around it, and returns the modified function.
A professional decorator uses argument unpacking to support diverse function signatures, preserves original return values, and uses functools.wraps to maintain function metadata. Reusable cross-cutting concerns include action logging, execution timing, and security checks that protect sensitive data. Parameterized decorators use additional nested layers to accept configuration arguments, such as permission codes.
Authentication verifies a user's identity, while authorization checks what permissions the user possesses. Systems must authenticate users before evaluating their authorization permissions. Stacked decorators wrap execution from the bottom decorator upward. Class decorators can also add behavior to classes, such as automatically registering notification handlers in a registry factory.
Decorators allow developers to add reusable behavior without mixing it into the main business logic. They are useful for tasks such as logging, timing, validation, and access control. However, security checks must be implemented carefully, because decorators alone do not guarantee that a system is secure.
The five SOLID principles help developers write code that is easier to maintain, test, and extend. This lesson also covers common code smells and systematic refactoring strategies.
Code smells are warning signs indicating that code may be difficult to maintain, including giant classes, long methods, duplicate code, excessive conditional branches, tight coupling, hidden dependencies, large interfaces, and unexpected subclass behavior.
Single Responsibility Principle (SRP): A class should have one clear reason to change, separating tasks like validation, persistence, notification, and auditing into focused components.
Open/Closed Principle (OCP): Code should allow new features through extension without requiring risky modifications to existing core code.
Liskov Substitution Principle (LSP): Child classes must be able to replace their parent classes without breaking program correctness.
Interface Segregation Principle (ISP): Classes should not be forced to depend on methods they do not use, favoring smaller, focused interfaces.
Dependency Inversion Principle (DIP): High-level business logic should depend on abstractions rather than specific concrete implementations.
Refactoring improves code structure without changing its intended behavior through iterative improvements backed by automated tests. Injected dependencies facilitate testing by allowing lightweight fake repositories to replace real databases.
SOLID principles provide guidelines for designing maintainable object-oriented systems. They help developers identify responsibilities, reduce unnecessary dependencies, and make code easier to extend and test. Refactoring should improve the internal structure of a program while preserving its existing behavior, with tests helping to verify that nothing has broken.
This lesson covers three design patterns that help organize changing behavior: Strategy for switching algorithms, Template Method for maintaining workflows with customizable steps, and Command for turning actions into trackable objects.
Behavioral patterns solve problems involving excessive conditional branches, repeated logic, and tight coupling between actions and the code that invokes them.
The Strategy pattern lets programs switch between different algorithms dynamically using composition without modifying the main service. It is best suited for interchangeable calculations, discounts, or payment methods.
The Template Method pattern defines a fixed workflow structure in a base class while allowing subclasses to customize specific steps using inheritance. It is ideal for standardized multi-step processes like report generation.
The Command pattern turns an action request into an object with execution methods, supporting action history, queuing, and logging. Undo operations may also be supported when the command is designed to store or restore the necessary previous state.
The three patterns solve different problems. Use Strategy when you need to switch between interchangeable algorithms, Template Method when a workflow follows the same overall steps but some steps vary, and Command when actions need to be represented as objects for queuing, logging, history, or undo operations. The best pattern is the one that solves the actual design problem without adding unnecessary complexity.
មតិយោបល់ 0