This blog post summary covers several key object-oriented programming (OOP) and design principles, with a focus on Python implementation and idiom.
Class Construction and Separation of Concerns
- Internal vs. External Data: When constructing a class, data passed as arguments from the user (like initial values) are external. Internal operational data, such as a running count or a list of entries, should be initialized automatically within the class (
__init__) and not passed as arguments. - Single Responsibility Principle (SRP): This is demonstrated by separating the core logic of a class (e.g., a
Journalmanaging entries) from the concern of persistence (saving/loading data). A separatePersistenceManagerclass (or static method likePM.save_to_file) handles file operations, keeping the journal class focused.
SOLID Principles
Open-Closed Principle (OCP)
- Problem: Modifying a filtering class (
ProductFilter) every time a new specification (like a new color or size) is needed violates the OCP, which states that software entities should be open for extension, but closed for modification. - Solution: Specification Pattern: This pattern uses inheritance and composition to allow for extension without modification.
- A generic
Specificationclass defines anis_satisfied(self, item)method. - Specific rules (e.g.,
ColorSpecification) inherit fromSpecificationand implement the check. - A
BetterFilterclass takes a product list and aSpecificationobject, applying the filter generically. - Combinators (like
AndSpecificationand the__and__operator overload) allow complex rules to be built from simple ones.
- A generic
Liskov Substitution Principle (LSP)
- Principle: Subtypes must be substitutable for their base types without altering the correctness of the program.
- Violation Example: If a
Squareclass is derived from aRectangleclass, setting thewidthof theSquaremight also change itsheight(to maintain the square property). This breaks any function expecting aRectanglewhere changing the width is not expected to change the height, thus violating the LSP. The interface of the base class (Rectangle) must be honored by the derived class (Square).
Design Patterns
Builder Pattern (Fluent Builder)
- Purpose: To simplify the creation of complex objects with many optional parameters, avoiding “telescoping constructors” (long parameter lists).
- Fluent Builder: Uses method chaining to construct an object step-by-step.
- It uses nested specialized builder classes (
PersonJobBuilder,PersonAddressBuilder) that inherit from a basePersonBuilder. - The specialized builders return
selfor a new builder to allow chaining, often exposed as@propertymethods (e.g.,.works,.lives) on the base builder for a clean, fluent syntax.
- It uses nested specialized builder classes (
Factory Pattern (and Python Idioms)
- Goal: To control and encapsulate the logic for object creation. A factory defines how to build the product, not what the product is (the class is the blueprint).
- Python Perspective: Python’s features like duck typing, first-class functions (lambdas), and closures often allow for a simpler, less verbose approach to configuration and object creation compared to the complex class hierarchies of the Abstract Factory pattern in other languages. However, the abstract factory can still be necessary for complex, type-heavy systems (like some OOP-heavy plugin architectures).
Prototype Pattern
- Goal: Create new objects by cloning (deep copying) an existing “prototype” object instead of creating them from scratch.
- Implementation: A
copy.deepcopy()of the prototype is made, and then only the unique parts (e.g., a new employee’s name and office suite) are modified.
Singleton Pattern
- Goal: Ensure that a class has only one instance and provides a global point of access to it.
- Python Implementation: Achieved by overriding the
__new__method and using a class-level variable (cls._instance) to store the single instance. Decorators or metaclasses can also be used.
State and Typing
State vs. Statelessness
- Stateless: An object or function that does not store any information that persists between calls. Its output depends only on its input (e.g., a simple
add(a, b)function). - Stateful: An object that maintains internal information (state) that is modified by and affects subsequent calls (e.g., an
Adderclass with a runningself.total).
Duck Typing and Protocols
- Duck Typing: In Python, an object’s type is less important than what methods and attributes it has (“If it walks like a duck and quacks like a duck…”).
- Protocols (via
typing.Protocol): Used to address potential runtime bugs from duck typing. AProtocoldefines an expected interface (e.g.,Quackable). This allows static type checkers (like Mypy) to verify at compile-time that an object passed to a function has the necessary methods, even without explicit inheritance.
Class vs. Instance and Metaclasses
self vs. cls
self: Refers to the instance (object) of the class. Used in instance methods to access or modify instance data.cls: Refers to the class itself. Used in class methods (@classmethod) to access or modify class-level data or to construct new instances of the class (like in a factory method:return cls(4)).- Calling a class method on an existing instance (
p.from_fullname(...)) does not modify that instance; it constructs and returns a new instance.
- Calling a class method on an existing instance (
Metaclass
- A metaclass is the “class of a class.” While a normal class defines how its instances behave, a metaclass defines how classes themselves behave (e.g., controlling class creation, adding methods to all instances automatically).