Summary

This document describes object-oriented design principles. It includes examples and explanations of classes, objects, attributes, methods, and other related concepts, like abstraction, encapsulation, inheritance, and polymorphism using the Python programming language.

Full Transcript

OO Design 1 Class and Objects Our system is decomposed into classes/objects A Class is a blueprint for an object. An Object is an entity that contains both data and behavior Objects are instances of classes. Attributes: The data stored within an object represents...

OO Design 1 Class and Objects Our system is decomposed into classes/objects A Class is a blueprint for an object. An Object is an entity that contains both data and behavior Objects are instances of classes. Attributes: The data stored within an object represents the state of the object. Methods: Define the behavior of an object represents what the object can do. 2 Defining a class in Python class Cat: Constructor: a special method used to def __init__(self, name, sex, age, weight, color, texture): initialize a newly created object self.name = name self.sex = sex self.age = age self.weight = weight self.color = color self: refers to the instance self.texture = texture being created def breathe(self): print(f"{self.name} is breathing.") def eat(self, food): print(f"{self.name} is eating {food}.") def run(self, destination): print(f"{self.name} is running to {destination}.") def sleep(self, hours): print(f"{self.name} is sleeping for {hours} hours.") def meow(self): print(f"{self.name} says: Meow!") # Set the attributes of the cats # Define the two cats from the image oscar.name = "Oscar II" oscar = Cat("Oscar", "male", 3, 7, "brown", "striped") oscar.weight = 8 luna = Cat("Luna", "female", 2, 5, "gray", "plain") luna.color = "white" luna.age = 3 # Demonstrate some behaviors for each cat print("Oscar:") # Get the attributes of the cats oscar.meow() print("\nOscar's attributes:") oscar.eat("fish") print(f"Name: {oscar.name}") print(f"Weight: {oscar.weight} kg") print("\nLuna:") luna.run("toy") print("\nLuna's attributes:") luna.sleep(2) print(f"Name: {luna.name}") print(f"Color: {luna.color}") 3 Pillars of OOP Object-oriented programming is based on four pillars, concepts that differentiate it from other programming paradigms Abstraction Encapsulation Inheritance Polymorphism 4 Abstraction Most of the time when you’re creating a program with OO, you shape objects of the program based on real-world objects. Abstraction is a model of a real-world object or phenomenon limited to a specific context Your defined objects don’t represent the originals with 100% accuracy (and it’s rarely required that they do). Only model attributes and behaviors of real objects in a specific context, ignoring the rest. Flight simulation Flight booking app App 5 Inheritance Dogs and Cats have a lot in common: name, sex, age, and color are attributes of both dogs and cats. Dogs can breathe, sleep and run the same way cats do. We can define the base Animal class that would list the common attributes and behaviors. The parent class is called a superclass. Its children are subclasses. Subclasses inherit state and behavior from their parent, defining only attributes or behaviors that differ. The main benefit of inheritance is code reuse The subclasses inherits fields and methods of the superclass. 6 class Animal: def __init__(self, name, sex, age, weight, color): Python Code self.name = name self.sex = sex self.age = age self.weight = weight self.color = color def breathe(self): print(f"{self.name} is breathing.") def eat(self, food): print(f"{self.name} is eating {food}.") def run(self, destination): Define the class to be inherited from print(f"{self.name} is running to {destination}.") def sleep(self, hours): print(f"{self.name} is sleeping for {hours} hours.") class Cat(Animal): def __init__(self, name, sex, age, weight, color, is_nasty): super().__init__(name, sex, age, weight, color) super(): call methods of the self.__is_nasty = is_nasty # Private attribute superclass in a derived class def meow(self): print(f"{self.name} says: Meow!") class Dog(Animal): def __init__(self, name, sex, age, weight, color, best_friend): super().__init__(name, sex, age, weight, color) self.__best_friend = best_friend # Private attribute def bark(self): print(f"{self.name} says: Woof!") Instance methods should contain self as the first parameter cat = Cat("Whiskers", "Female", 3, 4.5, "Tabby", False) dog = Dog("Buddy", "Male", 5, 15.2, "Golden", "John") cat.meow() cat.eat("fish") dog.bark() dog.run("park") 7 Encapsulation To start a car engine, you only need to turn a key or press a button. You don’t need to connect wires under the hood, rotate the crankshaft and cylinders, and initiate the power cycle of the engine. These details should be hidden under the hood of the car Encapsulation is the ability of an object to hide parts of its state and behaviors from other objects, exposing only a limited interface to the rest of the program. To encapsulate something means to make it private, and thus accessible only from within of the methods of its own class. 8 Private attributes In Python, we don't have true private attributes We can simulate a private attribute in python by start with a double underscore (__) Note: this is NOT truly private and there are still ways to access it outside the class! Python class Robot: def __init__(self, name): self.__name = name self.__battery_level = 100 # Private attribute def recharge(self): self.__battery_level = 100 print(f"{self.name} is recharging.")... 9 Setters and Getter Objects should communicate with each other via their public method but do not need to know about how the method is implemented Using Setter and getter methods for accessing and setting private attributes Provide controlled access to an object’s data This prevents code outside the class from setting unrealistic or invalid ages E.g. Enforce rules about what values are acceptable for age. class Robot: def __init__(self, name): self.__name = name self.__battery_level = 100 # Private attribute def recharge(self): self.__battery_level = 100 print(f"{self.name} is recharging.") def get_battery_level(self): return self.__battery_level # getter and setter def get_name(self): return self.__name def set_name(self, name): # check that length of the robot name is greater than 5 characters if len(name) > 5: self.__name = name else: print("Name must be more than 5 characters") 10 What is an Interface in OO? A contract between itself and any class that implements it. Client (Electrical Appliance) Interface (Power outlet) Implementation (source of electrical energy) 11 Interface An interface can provide no implementation at all. The interface defines a contract (method names, number of parameters, and so on) where the developers are required to comply with the rules Only the public methods are considered the interface. Includes the parameters and their types, type of return values, etc. 12 Example SocialLogin is an interface that defines the authenticate() method. Both FacebookLogin and GoogleLogin implement this interface. 13 Abstract class in Python In Python, an abstract class is a class that contains at least one abstract method Inherits from ABC (Abstract Base Class) and Decorated with @abstractmethod, serving as a blueprint for other classes and cannot be instantiated directly. An abstract class serves as a blueprint for other classes. It cannot be instantiated directly (cannot create an object from it). Derived classes should implement its abstract methods. from abc import ABC, abstractmethod class SocialLogin(ABC): @abstractmethod def authenticate(self): pass class FacebookLogin(SocialLogin): def authenticate(self): print("Authenticating with Facebook.") class GoogleLogin(SocialLogin): def authenticate(self): print("Authenticating with Google.") 14 Polymorphism You can think of polymorphism as the ability of an object to “pretend” to be something else Usually a class it extends or an interface it implements. You could create a login function that accepts a SocialLogin object and calls its authenticate() method When passed to the login() function, these objects are treated as “SocialLogin”, but they behaves according to their specific implementation from abc import ABC, abstractmethod class SocialLogin(ABC): @abstractmethod def authenticate(self): pass class FacebookLogin(SocialLogin): def authenticate(self): print("Authenticating with Facebook.") class GoogleLogin(SocialLogin): def authenticate(self): print("Authenticating with Google.") def login(social_login: SocialLogin): social_login.authenticate() login(facebook_login) # Outputs: Authenticating with Facebook. login(google_login) # Outputs: Authenticating with Google. 15 Advantage Flexibility and extensibility Easier to extend with new types without modifying existing code. Code reusability Code that can work with objects of multiple types of SocialLogins def login(social_login: SocialLogin): social_login.authenticate() class InstagramLogin(SocialLogin): def authenticate(self): print("Authenticating with Instagram.") login(InstagramLogin()) # Outputs: Authenticating with Instagram. 16 Coupling and Cohesion in OO Design Coupling refers to the degree of interdependence between software modules or components Cohesion measures how strongly related and focused the responsibilities of a module are. Good OO design should be loosely coupled and highly cohesive Easier to develop Easier to maintain Easier to add new features Less fragile. Class/Module A Class/Module B 17 Dependency In Object-oriented (OO), dependency refers to the relationships and interactions between classes and objects in an object-oriented design. It describes how one class relies on another class to function properly. Dependencies can influence how changes in one class affect other classes, impacting the overall maintainability and flexibility of the system. Types of dependencies in OO Interface, Association, Aggregation, Composition, Inheritance From most loosely coupled to most strongly decoupled 18 Interface Implementation Lowest coupling between the subclasses and the interface from abc import ABC, abstractmethod class SocialLogin(ABC): @abstractmethod def authenticate(self): pass class FacebookLogin(SocialLogin): def authenticate(self): print("Authenticating with Facebook.") class GoogleLogin(SocialLogin): def authenticate(self): print("Authenticating with Google.") 19 Association Represents a Uses-a relationship Represents a relationship where one class is aware of and interacts with another class, but neither class owns or controls the lifecycle of the other. Objects collaborate while remaining independent. class Student: def __init__(self, name): self.name = name def study(self, course): print(f"{self.name} is studying {course.name}") class Course: def __init__(self, name): self.name = name 20 Aggregation Represents a Has-a relationship It implies that one class contains references to instances of other classes, but the contained objects can exist independently and can potentially be shared among multiple containing objects. In this example, if a Book or Library object is deleted, the Author objects continue to exist. class Author: def __init__(self, name): self.name = name class Book: def __init__(self, title, author): self.title = title self.author = author # Aggregation: Book has an Author def display_info(self): print(f"'{self.title}' by {self.author}") 21 Composition Represents a Has-a relationship The lifecycle of the contained (part) object is tightly coupled with the lifecycle of the container object. The part cannot exist independently of the whole, and when the whole is destroyed, so are all of its parts. class Room: def __init__(self, name): self.name = name class House: def __init__(self): self.rooms = [] def add_room(self, room_name): room = Room(room_name) self.rooms.append(room) 22 Inheritance Represents a Is-a relationship Most strongly coupled type of relationship between classes A child class is highly dependent on its parent class. Changes in the parent class directly affect all its subclasses. Subclasses inherit the implementation of the parent class=> tightly bound to the parent's internal workings. class Animal: def __init__(self, name): self.name = name def move(self): print(f"{self.name} is moving.") class Lion(Animal): def roar(self): print(f"{self.name} roars!") 23 Programming to an interface A design principle that emphasizes writing code that depends on abstract interfaces or base classes rather than concrete implementations. Only the public methods are considered the interface. Includes the parameters and their types, type of return values, etc. Advantages: Flexibility: Easily swap implementations without changing client code. Maintainability: Changes to implementations don't affect code using the interface. Clients of the class will not be affected by implementation change 24 SOLID Principle The SOLID principles are a set of five design principles in object-oriented programming aimed at making software designs more understandable, flexible, and maintainable. SRP—Single Responsibility Principle OCP—Open/Close Principle LSP—Liskov Substitution Principle IPS—Interface Segregation Principle DIP—Dependency Inversion Principle 25 Example: Databases MyApp is tightly coupled to MySQLDatabase class MyApp: def __init__(self, data: dict): (Concrete class) self.data = data def save_to_db(self): If we want to change the database type (e.g., db : MySQLDatabase = MySQLDatabase() db.connect() from MySQL to PostgreSQL or MongoDB), db.save(self.data) db.close() we would need to modify the MyApp class. class MySQLDatabase(): def connect(self): print("Connecting to MySQL database") def save(self, data): print(f"Saving data to MySQL: {data}") def close(self): print("Closing MySQL connection") data = {"name": "John Doe", "age": 30} app = MyApp(data) app.save_to_db() 26 Open-closed principle Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification You should be able to extend a class’s behavior without modifying it. Database Interface and Implementations: New database types can be added by implementing the IDatabase interface. MyApp depends on the IDatabase interface, not on concrete implementations. It can work with any current or future database implementation without modification. 27 Example Implementation in Python Depends on class MyApp: def __init__(self, data: dict): (Association) # An interface for database operations self.data = data class Database(ABC): @abstractmethod def save_to_db(self, db: Database): def connect(self): db.connect() pass db.save(self.data) db.close() @abstractmethod def save(self, data): pass data = {"name": "John Doe", "age": 30} app = MyApp(data) @abstractmethod app.save_to_db(MySQLDatabase()) def close(self): app.save_to_db(MongoDB()) pass # Concrete implementation for MongoDB # Concrete implementation for MySQL class MongoDB(Database): class MySQLDatabase(Database): def connect(self): def connect(self): print("Connecting to MongoDB database") print("Connecting to MySQL database") def save(self, data): def save(self, data): print(f"Saving data to MySQL: {data}") print(f"Saving data to MongoDB: {data}") def close(self): def close(self): print("Closing MySQL connection") print("Closing MongoDB connection") Concrete Implementation 28 Dependency Inversion Principle (DIP) High-level modules should not depend on low-level modules. Both should depend on abstractions. High-level modules Modules that contain the business logic of an application. Low-level modules These are modules that handle specific operations E.g. database, logging modules Abstractions Interfaces or abstract classes that define contracts for functionality. 29 Dependency Injection Dependency Injection (DI) is a design pattern and programming technique that aims to reduce the coupling between different parts of a software system. A dependency is any external component that a module, class, or function needs to perform its tasks. Instead of a class creating or managing its own dependencies, these dependencies are provided to the class from an external source. Types of Dependency Injection: Constructor Injection: Dependencies are provided through a class constructor. Setter Injection: Dependencies are provided through setter methods. Method Injection: Dependencies are provided through method parameters. 30 Constructor Injection class MyApp: def __init__(self, data: dict, db: Database): self.data = data self.db = db # Define an abstract base class (interface) for database operations class Database(ABC): def save_to_db(self): @abstractmethod self.db.connect() def connect(self): self.db.save(self.data) pass self.db.close() @abstractmethod data = {"name": "John Doe", "age": 30} def save(self, data): pass # Use constructor injection to save data to MySQL mysql_db = MySQLDatabase() @abstractmethod app_mysql = MyApp(data, mysql_db) def close(self): print("Saving to MySQL:") pass app_mysql.save_to_db() # Use constructor injection to save data to MongoDB mongodb = MongoDB() app_mongo = MyApp(data, mongodb) # Concrete implementation for MongoDB print("\nSaving to MongoDB:") # Concrete implementation for MySQL class MongoDB(Database): app_mongo.save_to_db() class MySQLDatabase(Database): def connect(self): def connect(self): print("Connecting to MongoDB database") print("Connecting to MySQL database") def save(self, data): def save(self, data): print(f"Saving data to MySQL: {data}") print(f"Saving data to MongoDB: {data}") The dependency (the database object) is def close(self): def close(self): injected via the constructor. print("Closing MySQL connection") print("Closing MongoDB connection") Concrete Implementation 31 Example: Setter Injection class MyApp: def __init__(self, data: dict): self.data = data self.db = None # Define an abstract base class (interface) for database operations class Database(ABC): def set_database(self, db: Database): @abstractmethod self.db = db def connect(self): pass def save_to_db(self): if self.db is None: @abstractmethod raise ValueError("Database instance is not set") def save(self, data): self.db.connect() pass self.db.save(self.data) self.db.close() @abstractmethod def close(self): data = {"name": "John Doe", "age": 30} pass # Use setter injection to save data to MySQL app = MyApp(data) mysql_db = MySQLDatabase() app.set_database(mysql_db) print("Saving to MySQL:") app.save_to_db() # Concrete implementation for MySQL # Concrete implementation for MongoDB class MySQLDatabase(Database): class MongoDB(Database): def connect(self): def connect(self): # Use setter injection to save data to MongoDB print("Connecting to MySQL database") mongodb = MongoDB() print("Connecting to MongoDB database") app.set_database(mongodb) def save(self, data): print("\nSaving to MongoDB:") def save(self, data): print(f"Saving data to MySQL: {data}") app.save_to_db() print(f"Saving data to MongoDB: {data}") def close(self): print("Closing MySQL connection") def close(self): print("Closing MongoDB connection") The dependency (the database object) is injected via the setter method Concrete Implementation 32 Method Injection class MyApp: def __init__(self, data: dict): # Define an abstract base class (interface) for database operations self.data = data class Database(ABC): @abstractmethod def save_to_db(self, db: Database): def connect(self): db.connect() pass db.save(self.data) db.close() @abstractmethod def save(self, data): pass data = {"name": "John Doe", "age": 30} app = MyApp(data) @abstractmethod def close(self): # Use dependency injection to save data to pass MySQL mysql_db = MySQLDatabase() print("Saving to MySQL:") app.save_to_db(mysql_db) # Use dependency injection to save data to MongoDB # Concrete implementation for MySQL # Concrete implementation for MongoDB mongodb = MongoDB() class MySQLDatabase(Database): class MongoDB(Database): print("\nSaving to MongoDB:") def connect(self): def connect(self): app.save_to_db(mongodb) print("Connecting to MySQL database") print("Connecting to MongoDB database") def save(self, data): def save(self, data): print(f"Saving data to MySQL: {data}") print(f"Saving data to MongoDB: {data}") def close(self): def close(self): Method injection: print("Closing MySQL connection") print("Closing MongoDB connection") The dependency (the database object) is injected through a method parameter. Concrete Implementation 33 Using a Factory class class DatabaseFactory: The DatabaseFactory class encapsulates the creation @staticmethod def create_database(db_type: str) -> Database: logic for MySQLDatabase and MongoDB. if db_type == "mysql": return MySQLDatabase() elif db_type == "mongodb": return MongoDB() else: raise ValueError(f"Unknown database type: {db_type}") class MyApp: def __init__(self, db: Database): self.db = db def save_to_db(self, data: dict): self.db.connect() self.db.save(data) self.db.close() data = {"name": "John Doe", "age": 30} # Use factory to create MySQL database instance mysql_db = DatabaseFactory.create_database("mysql") app_mysql = MyApp(mysql_db) print("Saving to MySQL:") app_mysql.save_to_db(data) # Use factory to create MongoDB database instance mongodb = DatabaseFactory.create_database("mongodb") app_mongo = MyApp(mongodb) print("\nSaving to MongoDB:") app_mongo.save_to_db(data) 34 Advantages of Dependency injection in OO development Allows for loose coupling between components Easier to swap out implementations without changing the code that uses them. Enhanced Testability By injecting dependencies, you can easily substitute mock objects or stubs during unit testing Dependency injection is a core principle in frameworks like Java Spring In Spring, the Inversion of Control (IoC) container is responsible for creating and managing the lifecycle of objects The dependencies are automatically injected by the container 35 Dependency Injection Java Spring UserService.java UserRepository.java MySQL pom.xml Java Spring IoC Container 36

Use Quizgecko on...
Browser
Browser