algorithm #programinglang #software-engineering
design #patterns
Chapter 1 OOP Object Oriented Programming
Object-oriented programming is a paradigm based on the concept of wrapping pieces of data, and behavior related to that data, into special bundles called objects, which are constructed from a set of “blueprints”, defined by a programmer, called classes.
- OOP Pillars [[OOP#Key Concepts of OOP#]]
Let’s demonstrate Dependency and Composition using Python code examples. This will illustrate how these concepts are implemented and how they differ from each other.
Dependency Example
In the context of OOP, dependency means that one class depends on another class to function properly. This is often referred to as a dependency injection, where an object of one class is passed to another class. The dependent class uses this object to perform its operations.
Dependency Example Code
Explanation:
Car
is dependent on theEngine
class. It does not inherit fromEngine
but relies on an instance ofEngine
to perform itsstart_engine()
method.- This dependency is passed into the
Car
class via the constructor (__init__
method).
Composition Example
Composition is when a class is composed of one or more objects from other classes. This establishes a has-a relationship, where one class contains instances of other classes as its members.
Composition Example Code
Explanation:
- The
Car
class is composed ofEngine
andTire
classes. It creates and maintains its own instances of these classes. - The
Car
class interacts with its internal components (Engine
andTire
) to perform itsdrive()
method. Car
has a stronger relationship withEngine
andTire
compared to dependency. The lifecycle ofEngine
andTire
is controlled byCar
itself.
Key Differences in the Code
-
Dependency Injection:
Car
receives anEngine
object from outside (injected into the constructor).Car
depends onEngine
but does not control its creation or lifecycle.- Example:
car = Car(engine)
—Car
depends on an externalengine
object.
-
Composition:
Car
internally creates and controls its ownEngine
andTire
objects.- The
Car
class has a direct “has-a” relationship with its components (Engine
andTire
).
Conclusion
- Dependency involves passing an existing object to a class, making it depend on that object without being responsible for its creation or lifecycle.
- Composition involves a class creating and owning other objects, leading to a stronger relationship and a “whole-part” hierarchy.
Advantages of Dependency Injection and Composition in OOP
Both Dependency Injection and Composition offer distinct advantages in Object-Oriented Programming (OOP), particularly when it comes to flexibility, maintainability, and code reusability. Let’s outline the specific benefits of each.
Advantages of Dependency Injection
-
Loose Coupling:
- Dependency Injection (DI) reduces the coupling between classes. Instead of creating dependencies directly, dependencies are passed from outside, making the code more modular and flexible.
-
Improved Testability:
- Since dependencies are injected from outside, it becomes easier to swap real objects with mock or stub objects during testing. This allows for isolated unit testing without needing to instantiate real dependencies.
-
Enhanced Flexibility and Reusability:
- With DI, the behavior of classes can be changed without modifying the class itself. This is useful when you want to switch implementations or configurations without touching the main class code.
-
Decoupled Code Maintenance:
- Changes in dependencies have minimal impact on dependent classes, reducing the amount of code that needs to be changed or refactored. You can update a dependency without changing the class that uses it.
-
Promotes Interface-Based Design:
- DI encourages the use of interfaces or abstract classes for dependency declarations, promoting a design that separates implementation from abstraction.
-
Adherence to SOLID Principles:
- DI aligns with the Dependency Inversion Principle (DIP), one of the SOLID principles, which states that high-level modules should not depend on low-level modules, but both should depend on abstractions.
Advantages of Composition
-
Reusability:
- Composition enables code reuse by building complex objects from simpler ones. The same components can be reused across different classes, reducing code duplication and enhancing maintainability.
-
Flexibility in Object Behavior:
- Composition allows for easy modification of an object’s behavior by changing its internal components. It supports adding, removing, or replacing parts at runtime without affecting the overall structure.
-
Improved Encapsulation:
- Composition helps encapsulate behavior within different classes. Each class is responsible for a specific part of the functionality, making it easier to understand, maintain, and debug.
-
Simpler Class Hierarchies:
- With composition, there is no need for deep inheritance hierarchies. This leads to simpler and more intuitive class structures, avoiding the pitfalls of rigid inheritance models.
-
Dynamic Behavior Changes:
- Unlike inheritance, where behavior is fixed at compile-time, composition allows behavior to change dynamically at runtime. This makes it possible to alter an object’s capabilities by reassigning its components.
-
Promotes Modular Design:
- Composition breaks down a complex system into smaller, manageable parts. Each part can be developed, tested, and maintained independently, promoting a modular design approach.
-
Avoids the Fragile Base Class Problem:
- Inheritance can introduce issues when changes in the base class ripple through the subclass hierarchy, potentially breaking the subclasses. Composition avoids this problem since classes only interact through well-defined interfaces.
-
Better Adherence to the Open/Closed Principle:
- Composition allows you to extend an object’s behavior without modifying its existing code. You can add new components or replace existing ones without changing the class itself.
Example Scenario: Choosing Between Dependency Injection and Composition
Imagine you are designing a Car
class that can have different types of engines (e.g., PetrolEngine
, DieselEngine
, or ElectricEngine
). Here’s when you might prefer each approach:
-
Dependency Injection: You can inject the specific engine type into the
Car
class, making it easy to change or configure the engine from outside. This is useful if theCar
class should be agnostic of how the engine is implemented or created. -
Composition: If the
Car
class needs to control its internal components (like starting multiple components simultaneously or configuring them together), using composition makes more sense. This allows theCar
class to directly manage its own internal components.
When to Use Each
-
Use Dependency Injection when:
- You want to reduce the coupling between classes.
- You need flexibility in switching or configuring dependencies.
- You want to promote testability and support different test cases.
-
Use Composition when:
- You want to build a class by aggregating multiple components.
- You need to manage the lifecycle of components within a class.
- You want dynamic behavior changes or a simpler class hierarchy.
Conclusion
Both Dependency Injection and Composition are powerful techniques in OOP that help achieve code modularity, flexibility, and maintainability. They solve different problems and can be used together or independently, depending on the design requirements.
Design patterns
Design patterns are typical solutions to commonly occurring problems in software design. They are like pre-made blueprints that you can customize to solve a recurring design problem in your code.
Difference between Algorithm and Design Pattern
An analogy to an algorithm is a cooking recipe: both have clear steps to achieve a goal. On the other hand, a pattern is more like a blueprint: you can see what the result and its features are, but the exact order of implementation is up to you.
Categorization of Design Patterns by Intent
All design patterns can be categorized based on their intent or purpose. The three main groups of design patterns are:
-
Creational Patterns:
- Purpose: Provide object creation mechanisms that increase flexibility and reuse of existing code.
- Examples: Singleton, Factory Method, Abstract Factory, Builder, Prototype.
-
Structural Patterns:
- Purpose: Explain how to assemble objects and classes into larger structures while keeping the structures flexible and efficient.
- Examples: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy.
-
Behavioral Patterns:
- Purpose: Take care of effective communication and the assignment of responsibilities between objects.
- Examples: Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor.
Design patterns are a toolkit of tried and tested solutions to common problems in software design. Even if you never encounter these problems, knowing patterns is still useful because it teaches you how to solve all sorts of problems using principles of object-oriented design.
Software Design Principles
Code Reuse
Extensibility
Software Design Principles
Software design principles are guidelines and best practices that developers follow to create robust, maintainable, and scalable software systems. These principles help in managing complexity, reducing dependencies, and promoting code reuse. They serve as a foundation for writing clean, understandable, and efficient code.
Here are some of the most widely adopted software design principles:
1. Single Responsibility Principle (SRP)
-
Definition: A class should have only one responsibility and should have only one reason to change. Each class should focus on a single functionality or feature, which makes the code more modular and easier to maintain.
-
Example: Consider a class
Document
that handles both file reading and content formatting. This violates the SRP because it has multiple responsibilities.Instead, refactor it into separate classes:
Each class now has a single responsibility:
FileReader
reads files, andContentFormatter
handles formatting.
2. Open/Closed Principle (OCP)
-
Definition: Software entities (such as classes, modules, functions) should be open for extension but closed for modification. This means you should be able to extend a class’s behavior without modifying its existing code.
-
Example: Suppose you have a
Shape
class that calculates the area of various shapes. Adding new shapes would require modifying the existing code, which violates OCP.Instead, use polymorphism to extend the
Shape
class:Now, adding new shapes like
Rectangle
orTriangle
can be done by extending theShape
class without modifying the existing code.
3. Liskov Substitution Principle (LSP)
-
Definition: Subtypes must be substitutable for their base types without affecting the correctness of the program. This principle ensures that a derived class can replace its base class without changing the desired behavior.
-
Example: Consider a
Bird
base class with a methodfly
. A subclassOstrich
that cannot fly would violate LSP.Refactor using a more appropriate hierarchy:
4. Interface Segregation Principle (ISP)
-
Definition: Clients should not be forced to implement interfaces they do not use. This principle suggests splitting large interfaces into smaller, more specific ones so that clients only need to know about the methods that are relevant to them.
-
Example: Suppose you have an interface
IWorker
with methodswork()
andeat()
. ARobot
class implementingIWorker
would have to implementeat()
, which is not applicable.Split the interface to avoid irrelevant implementations:
5. Dependency Inversion Principle (DIP)
-
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. Also, abstractions should not depend on details. Details should depend on abstractions. This principle helps in decoupling modules and promoting flexible code.
-
Example: Consider a
Light
class that is controlled by aSwitch
class. If theSwitch
class directly depends on theLight
class, it violates DIP.Refactor to depend on an abstraction:
By introducing the
Switchable
interface, theSwitch
class now depends on an abstraction rather than a concrete implementation.
Additional Software Design Principles
-
DRY (Don’t Repeat Yourself):
- Avoid code duplication. Each piece of knowledge should have a single, unambiguous representation in the system.
- Example: If the same piece of code appears in multiple places, refactor it into a separate function or class.
-
KISS (Keep It Simple, Stupid):
- Write simple and straightforward code. Avoid unnecessary complexity, as complex solutions are more prone to errors and harder to maintain.
- Example: Instead of using complex nested loops and conditions, try to break down the logic into smaller, understandable functions.
-
YAGNI (You Ain’t Gonna Need It):
- Avoid implementing features or functionality until they are actually needed.
- Example: Don’t add extra methods or properties to a class based on future use cases unless there is a clear requirement for them.
-
Composition Over Inheritance:
- Favor using composition (has-a relationships) over inheritance (is-a relationships) to achieve greater flexibility and code reuse.
- Example: Instead of using class inheritance to extend functionality, use object composition to mix and match capabilities as needed.
Conclusion
Software design principles are fundamental to writing clean, maintainable, and scalable software. By adhering to these principles, developers can create systems that are easier to understand, modify, and extend, ultimately leading to more robust and reliable software.