Skip to content

Latest commit

 

History

History
1081 lines (756 loc) · 33.8 KB

17.SOLID.md

File metadata and controls

1081 lines (756 loc) · 33.8 KB

Lesson 17: SOLID

"SOLID principles are the compass that navigates developers through the complexities of software architecture"

Content

  1. Definition
  2. Single Responsibility Principle (SRP)
  3. Open/Closed Principle (OCP)
  4. Liskov Substitution Principle (LSP)
  5. Interface Segregation Principle (ISP)
  6. Dependency Inversion Principle (DIP)
  7. Summarise
  8. Let's Refactor!
  9. Quiz
  10. Homework

0. Definition

SOLID principles are a set of design principles in software engineering that, when followed properly, make the software more understandable, flexible, and maintainable. The acronym SOLID stands for five design principles:

  1. S - Single Responsibility Principle (SRP)
  2. O - Open/Closed Principle (OCP)
  3. L - Liskov Substitution Principle (LSP)
  4. I - Interface Segregation Principle (ISP)
  5. D - Dependency Inversion Principle (DIP)

1. Single Responsibility Principle (SRP)

1.1 Definition

A class should have one, and only one, reason to change. This means a class should only have one job or responsibility.

Example

Consider a Report class that generates a report and then prints it. According to SRP, these two actions should be separated into different classes.

# Violating SRP
class Report:
    def generate_report(self, data):
        # Code to generate the report
        pass
    
    def print_report(self, report):
        # Code to print the report
        pass

# Adhering to SRP
class ReportGenerator:
    def generate_report(self, data):
        # Code to generate the report
        pass

class ReportPrinter:
    def print_report(self, report):
        # Code to print the report
        pass

Again, each class should have its own purpose, so the best to write down functionality/architecture of the project on the paper and stick to that in advance.

Example

Consider a UserManagement class that handles user-related operations such as

  • Adding a user
  • Sending a welcome email
  • Save (logging) the activity of the user

Incorrect Example

class UserManagement:
    def add_user(self, user):
        # Code to add the user to the system
        pass

    def send_welcome_email(self, user):
        # Code to send a welcome email to the user
        pass

    def log_activity(self, activity):
        # Code to log user activity
        pass

Think about how we can refactor this, does this class serve its purpose? Is it correct that it handles lots of different operations? Can we refactor it to improve maintainability/readability and scalability?

The answer is that each method is not really related to the UserManagment activity, it has 3 different purposes which could be separated into other classes:

Correct Example

class UserRegistry:
    def add_user(self, user):
        # Code to add the user to the system
        pass

class EmailService:
    def send_welcome_email(self, user):
        # Code to send a welcome email to the user
        pass

class ActivityLogger:
    def log_activity(self, activity):
        # Code to log user activity
        pass

The code above ensures that each class has a single reason to change. Which means that we applied SRP successfully.

2. Open/Closed Principle (OCP)

Software entities (like classes, modules, functions) should be open for extension but closed for modification.

This means that the behavior of a module/class/function can be extended without modifying its source code.

Example

Let's consider an example of a TaxCalculator class that calculates tax based on the type of product. Initially, the class is not following OCP, because every time a new tax type is introduced, the class has to be modified.

Correct Example

class TaxCalculator:
    def calculate_tax(self, product_type, price):
        if product_type == "book":
            return price * 0.05  # 5% tax for books
        elif product_type == "food":
            return price * 0.10  # 10% tax for food
        # More conditions for other product types

Incorrect Example

To follow the OCP, we can define a generic TaxCalculator class and then extend it for each specific tax type.

class TaxCalculator:
    def calculate_tax(self, price):
        pass

class BookTaxCalculator(TaxCalculator):
    def calculate_tax(self, price):
        return price * 0.05

class FoodTaxCalculator(TaxCalculator):
    def calculate_tax(self, price):
        return price * 0.10

# More classes for other product types
...

Example

Let's consider a reporting system where reports can be generated in different formats (e.g., PDF, CSV). Initially, the system is not following OCP because adding a new report format requires modifying existing code.

Incorrect Example

class ReportGenerator:
    def generate_report(self, data, format_type):
        if format_type == "PDF":
            # Generate PDF report
            pass
        elif format_type == "CSV":
            # Generate CSV report
            pass
        # More conditions for other formats

Correct Example

Here, we define a generic ReportGenerator class and then extend it for each specific report format.

class ReportGenerator:
    def generate_report(self, data):
        pass

class PDFReportGenerator(ReportGenerator):
    def generate_report(self, data):
        # Generate PDF report logic
        pass

class CSVReportGenerator(ReportGenerator):
    def generate_report(self, data):
        # Generate CSV report logic
        pass

# More classes for other formats

Sometimes people tend to over-engineer the solution by introducing too many layers of abstraction or making the system too generic.

Yes, SOLID principles tend to be a great foundation but please, don't overthink the design of your application, the majority of code can easily be refactored and updated throughout your journey, JUST CODE!

3. Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.

Example

Consider a system with different types of shapes where each shape can calculate its area.

A violation of LSP would occur if a subclass of Shape does not correctly implement the area() method.

Incorrect

class Shape:
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        # Incorrect implementation or not implemented
        pass

It can lead to incorrect behavior when a Circle is used in place of a Shape.

Correct

class Shape:
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius * self.radius

Using this principle can be a challenge in the begging of your application design, but if you use it within a correct approach it can be very predictable and robust for further development

Example

Let's consider a system with different types of birds. Initially, each bird has a fly() method.

A violation of LSP would occur if a subclass of Bird (like Penguin) cannot correctly implement the fly() method.

Incorrect

class Bird:
    def fly(self):
        # Default fly behavior
        pass

class Sparrow(Bird):
    def fly(self):
        # Implementation for flying
        pass

class Penguin(Bird):
    def fly(self):
        # Penguins can't fly!
        raise Exception("Can't fly")

In complex systems fly() would lead to incorrect behavior or runtime errors which potentially could be hard to debug.

This can be refactored by creating separate interfaces for FlyingBird and NonFlyingBird to ensure consistency throughout the code and correct implementation of overriden methods.

Correct

class Bird:
    # Common bird behavior (if any)
    pass

class FlyingBird(Bird):
    def fly(self):
        # Implementation for flying
        pass

class NonFlyingBird(Bird):
    # Other behaviors specific to non-flying birds
    pass

class Sparrow(FlyingBird):
    def fly(self):
        # Sparrow-specific flying behavior
        pass

class Penguin(NonFlyingBird):
    # Penguin-specific behaviors
    pass

Penguin objects are not expected to fly, and thus, the system does not assume that capability, adhering to LSP.

There are lots of rabbit holes for LSP but generally some static linters as mypy handle all of SOLID this for developers, but don't forget to turn on the brain while designing your application ;)

4. Interface Segregation Principle (ISP)

The Interface Segregation Principle (ISP) states that no client should be forced to depend on methods it does not use.

This principle aims to split larger interfaces into smaller, more specific ones so that clients only need to know about the methods that are interesting to them.

Example

Let's consider a printing system where the initial design forces the Printer class to implement functions that are not essential to all types of printers, such as faxing or scanning.

Incorrect

class AllInOnePrinter:
    def print_document(self, document):
        # Print the document
        pass

    def scan_document(self, document):
        # Scan the document
        pass

    def fax_document(self, document):
        # Fax the document
        pass

In this example, even simple printers without scanning or faxing capability have to implement the scan_document and fax_document methods, which violates ISP.

Correct

We refactor the example by creating separate interfaces for each responsibility.

class Printer:
    def print_document(self, document):
        pass

class Scanner:
    def scan_document(self, document):
        pass

class FaxMachine:
    def fax_document(self, document):
        pass

# Yes! That's where multiple inheritacne comes in use
class AllInOneMachine(Printer, Scanner, FaxMachine):
    def print_document(self, document):
        # Print the document
        pass

    def scan_document(self, document):
        # Scan the document
        pass

    def fax_document(self, document):
        # Fax the document
        pass

# Create instances
simple_printer = Printer()
scanner = Scanner()
fax_machine = FaxMachine()

# Using individual component for each class
document = "This is a document."
simple_printer.print_document(document)     # ``Printer``: Printing document
scanner.scan_document(document)             # ``Scanner``: Scanning document
fax_machine.fax_document(document)          # ``FaxMachine``: Faxing document

# All in one can handle three different operations in case we need to use all of them 
all_in_one = AllInOneMachine()
all_in_one.print_document(document)
all_in_one.scan_document(document)
all_in_one.fax_document(document)

It might be hard to understand but the simple explanation will be the following:

  • Printer, Scanner, and FaxMachine are interfaces (or abstract classes) that define specific functionalities. (That is a great example of SRP as well)
  • AllInOneMachine implements all these interfaces, providing the functionality for printing, scanning, and faxing.

Note: Clients that only need a printer can depend on the Printer interface without being forced to know about scanning or faxing and same applies to Scanner and FaxMachine.

If you need to print, you use Printer, to fax FaxMachine , and to scan Scanner is the way to go!

Example

Consider a vehicle control system where the initial design forces all vehicle types to implement functionalities that are not essential for all of them, such as launchMissiles for military vehicles or playMusic for civilian vehicles.

We don't want to have the ability to launch missiles in our car we use day to day for commuting to work, do we?

Incorrect

class VehicleControl:
    def steerLeft(self):
        # Steer the vehicle left
        pass

    def steerRight(self):
        # Steer the vehicle right
        pass

    def launchMissiles(self):
        # Launch missiles (mainly for military vehicles)
        pass

    def playMusic(self):
        # Play music (mainly for civilian vehicles)
        pass

Correct

We refactor the example above by creating separate interfaces for each category of functionalities.

# Parental Inerfaces (Base classes)
class BasicVehicleOperations:
    def steer(self):
        # Steer the vehicle
        pass

class MilitaryOperations:
    def launchMissiles(self):
        # Launch missiles
        pass

class EntertainmentOperations:
    def playMusic(self):
        # Play music
        pass

class CivilianVehicle(BasicVehicleOperations, EntertainmentOperations):
    def steer(self):
        # Implementation for steering
        pass

    def playMusic(self):
        # Implementation to play music
        pass

class MilitaryVehicle(BasicVehicleOperations, MilitaryOperations):
    def steer(self):
        # Implementation for steering
        pass

    def launchMissiles(self):
        # Implementation to launch missiles
        pass

Sometimes applying ISP can might result in a large number of interfaces, each with only a few methods. And this can become a headache for developers, but on the other hand, smaller and more specific interfaces lead to more modular and understandable code.

5. Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) suggests that high-level modules should not depend on low-level modules.

Abstractions should not depend on details, but details should depend on abstractions.

Example

Imagine you have a news reporting system where a NewsReporter class is responsible for reporting news. Initially, it directly uses a RadioChannel class to broadcast news. We want to adhere to DIP to make our NewsReporter more flexible and not tightly coupled to the RadioChannel.

Incorrect

class RadioChannel:
    def broadcast_news(self, news):
        print(f"Broadcasting news on the radio: {news}")

class NewsReporter:
    def __init__(self):
        self.radio_channel = RadioChannel()

    def report_news(self, news):
        self.radio_channel.broadcast_news(news)     

In this example, NewsReporter is directly dependent on RadioChannel, meaning if we want to broadcast news on a different medium, like TV, we'd have to modify the NewsReporter class, violating DIP.

Correct

Let's introduce an abstraction (interface) named BroadcastMedium and make NewsReporter dependent on this interface rather than a concrete class.

class BroadcastMedium:
    def broadcast_news(self, news):
        pass

class RadioChannel(BroadcastMedium):
    def broadcast_news(self, news):
        print(f"Broadcasting news on the radio: {news}")

class TVChannel(BroadcastMedium):
    def broadcast_news(self, news):
        print(f"Broadcasting news on TV: {news}")

class NewsReporter:
    def __init__(self, broadcast_medium: BroadcastMedium):
        self.broadcast_medium = broadcast_medium

    def report_news(self, news):
        self.broadcast_medium.broadcast_news(news)

Now, NewsReporter relies on the BroadcastMedium interface. We can easily broadcast news on the radio, TV, or any other medium that implements the BroadcastMedium interface without changing the NewsReporter class.

Since the main idea is that both high and low level modules should depend on abstractions. It is a great when example we don't care of the implementation details of broadcast_news in BroadcastMedium class, we just call it.

Example

Let's say we have a book reading app where a BookReader class is responsible for reading books. Initially, it's directly using a PDFReader class. We'll apply DIP to make BookReader flexible and not dependent on the PDFReader.

Incorrect

class PDFReader:
    def read_book(self, book):
        print(f"Reading {book} in PDF format.")

class BookReader:
    def __init__(self):
        self.reader = PDFReader()

    def read_book(self, book):
        self.reader.read_book(book)

In this example, BookReader is directly dependent on PDFReader. If we want to read books in a different format, we'd need to change BookReader, violating DIP.

Correct

Introduce an abstraction (interface) named BookFormatReader and make BookReader dependent on this interface.

class BookFormatReader:
    def read_book(self, book):
        pass

class PDFReader(BookFormatReader):
    def read_book(self, book):
        print(f"Reading {book} in PDF format.")

class EpubReader(BookFormatReader):
    def read_book(self, book):
        print(f"Reading {book} in EPUB format.")

class BookReader:
    def __init__(self, format_reader: BookFormatReader):
        self.format_reader = format_reader

    def read_book(self, book):
        self.format_reader.read_book(book)      # Use the specific format of the reader

Now, BookReader depends on the BookFormatReader interface. We can easily read books in PDF, EPUB, or any other format that implements the BookFormatReader interface without changing the BookReader class.

6. Summarise

I want to share a table with you to which you could refer to during designing your application. It might be not extremly readable, but helpful anyway to bear in mind summarising the key principles of SOLID and all rabit holes you can encounter.

Principle Purpose Advantages Potential Disadvantages
Single Responsibility Principle (SRP) Ensure a class has only one reason to change. - Easier maintenance
- Increased modularity
- Improved readability
- May lead to many small, tightly coupled classes
Open/Closed Principle (OCP) Allow entities to be open for extension but closed for modification. - Flexibility in extension
- Protection against changes
- Reduced risk of bugs
- May introduce abstract layers
- Can lead to over-engineering
Liskov Substitution Principle (LSP) Subtypes must be substitutable for their base types. - Enhanced reliability
- Promotes consistency
- Better code reusability
- Restricts how inheritance is used
- Can make hierarchy design complex
Interface Segregation Principle (ISP) No client should be forced to depend on methods it doesn't use. - Decoupled system
- Increased cohesion
- Easier to understand interfaces
- May increase the number of interfaces
- Potential for duplicate methods
Dependency Inversion Principle (DIP) High-level modules should not depend on low-level modules. - Decoupled architecture
- Easier to refactor and test
- Promotes flexible system
- Increased complexity
- Indirect relations between components

7. Let's Refactor!

Suppose we want to create an app that manages and displays messages in various formats.

Example

class MessageManager:
    def __init__(self, content):
        self.content = content

    def format_message(self, format_type):
        if format_type == "JSON":
            return f"{{'message': '{self.content}'}}"
        elif format_type == "XML":
            return f"<message>{self.content}</message>"

    def display_message(self, format_type):
        formatted_message = self.format_message(format_type)
        print(formatted_message)

# Usage
message_manager = MessageManager("Hello, SOLID!")
message_manager.display_message("JSON")

In order to refactor our application we should use the following steps:

Step 1: Go throughout the code and identify which principle is violated?

class MessageManager:
    def __init__(self, content):
        self.content = content

    # SRP Violation: This class handles multiple responsibilities
    def format_message(self, format_type):
        if format_type == "JSON":
            return f"{{'message': '{self.content}'}}"
        elif format_type == "XML":
            return f"<message>{self.content}</message>"

    # OCP Violation: Modifying the class to add a new format
    # LSP Violation: Subclasses would find it hard to support all format types
    # ISP Violation: Clients that need only message formatting are forced to depend on display functionality too
    def display_message(self, format_type):
        formatted_message = self.format_message(format_type)
        print(formatted_message)

message_manager = MessageManager("Hello, SOLID!")
message_manager.display_message("JSON")

Explanation

  1. Single Responsibility Principle (SRP) Violation: The MessageManager class handles multiple responsibilities, including formatting and displaying messages.
  2. Open/Closed Principle (OCP) Violation: To support a new message format, the format_message method needs to be modified, violating the principle of open for extension, closed for modification.
  3. Interface Segregation Principle (ISP) Violation: Clients that only want message formatting are forced to depend on the display functionality.
  4. Dependency Inversion Principle (DIP) Violation: The MessageManager class has a concrete dependency on message formatting and display, making it inflexible.

Step 2: Address each issue separately

A) Separate Responsibilities (SRP)

class Message:
    def __init__(self, content):
        self.content = content

Explanation

  • SRP: Single responsibility for managing message content.

B) Create Formatter Interface and Implementations (OCP, LSP, ISP)

from abc import ABC, abstractmethod

class MessageFormatter(ABC):
    @abstractmethod
    def format(self, message):
        pass

class JSONFormatter(MessageFormatter):
    def format(self, message):
        return f"{{'message': '{message.content}'}}"

class XMLFormatter(MessageFormatter):
    def format(self, message):
        return f"<message>{message.content}</message>"

Explanation

  • OCP: Open for extension, closed for modification
  • LSP: Subtypes can be substituted without altering the correctness of the program
  • ISP: Clients can choose specific formatters they need without depending on unused methods

C) Implement Display Functionality

class MessageDisplayer:
    def __init__(self, formatter: MessageFormatter):
        self.formatter = formatter

    def display(self, message: Message):
        formatted_message = self.formatter.format(message)
        print(formatted_message)

Explanation

DIP: High-level MessageDisplayer depends on abstraction MessageFormatter, not on concrete implementations!

Step 3: Put the code altogether

Refactored

from abc import ABC, abstractmethod

class Message:
    def __init__(self, content):
        self.content = content

class MessageFormatter(ABC):
    @abstractmethod
    def format(self, message):
        pass

class JSONFormatter(MessageFormatter):
    def format(self, message):
        return f"{{'message': '{message.content}'}}"

class XMLFormatter(MessageFormatter):
    def format(self, message):
        return f"<message>{message.content}</message>"

class MessageDisplayer:
    def __init__(self, formatter: MessageFormatter):
        self.formatter = formatter

    def display(self, message: Message):
        formatted_message = self.formatter.format(message)
        print(formatted_message)

message = Message("Hello, SOLID!")
json_formatter = JSONFormatter()
message_displayer = MessageDisplayer(json_formatter)
message_displayer.display(message)

xml_formatter = XMLFormatter()
message_displayer = MessageDisplayer(xml_formatter)
message_displayer.display(message)

Step 4: Pray that your code works

Output

{'message': 'Hello, SOLID!'}
<message>Hello, SOLID!</message>

Explanation

  1. Single Responsibility Principle (SRP): Separated the concerns into different classes: Message for managing message content, MessageFormatter for formatting messages, and MessageDisplayer for displaying messages.

  2. Open/Closed Principle (OCP): Introduced the MessageFormatter interface. New formatters can be added without modifying existing code, adhering to OCP.

  3. Liskov Substitution Principle (LSP): Clients can use instances of derived classes (JSONFormatter, XMLFormatter) through the MessageFormatter interface without affecting the correctness of the program.

  4. Interface Segregation Principle (ISP): Clients that only want to format messages can depend on MessageFormatter without being forced to depend on the display functionality.

  5. Dependency Inversion Principle (DIP): MessageDisplayer depends on the MessageFormatter abstraction, not the concrete implementations. It inverts the traditional dependency from high-level modules to low-level modules.

This refactoring makes the application more maintainable, extensible, and robust by adhering to the SOLID principles.

Example

Suppose we are developing a system for a bookstore that handles different types of book transactions such as selling, renting, and exchanging books.

Initial

class BookstoreManager:
    def __init__(self, books):
        self.books = books

    def process_transaction(self, book_id, transaction_type):
        if transaction_type == "SELL":
            # process selling transaction
            print(f"Selling book with ID: {book_id}")
        elif transaction_type == "RENT":
            # process renting transaction
            print(f"Renting book with ID: {book_id}")
        elif transaction_type == "EXCHANGE":
            # process exchange transaction
            print(f"Exchanging book with ID: {book_id}")

# Usage
books = {"001": "Book 1", "002": "Book 2"}
bookstore_manager = BookstoreManager(books)
bookstore_manager.process_transaction("001", "SELL")

The main skill a software engineer should have is thinking and ability to solve real-world problems using programming. Try to reproduce steps described above and understand which concepts were used in order to refactor our bookstore system.

Refactored

from abc import ABC, abstractmethod

class Book:
    def __init__(self, book_id, title):
        self.book_id = book_id
        self.title = title

class Transaction(ABC):
    @abstractmethod
    def execute(self, book: Book):
        pass

class SellTransaction(Transaction):
    def execute(self, book: Book):
        print(f"Selling book with ID: {book.book_id}")

class RentTransaction(Transaction):
    def execute(self, book: Book):
        print(f"Renting book with ID: {book.book_id}")

class ExchangeTransaction(Transaction):
    def execute(self, book: Book):
        print(f"Exchanging book with ID: {book.book_id}")

class BookstoreManager:
    def __init__(self, books):
        self.books = books

    def process_transaction(self, book_id, transaction: Transaction):
        if book_id in self.books:
            book = self.books[book_id]
            transaction.execute(book)
        else:
            print(f"Book with ID: {book_id} not found.")

# Usage
books = {"001": Book("001", "Book 1"), "002": Book("002", "Book 2")}
bookstore_manager = BookstoreManager(books)
sell_transaction = SellTransaction()
bookstore_manager.process_transaction("001", sell_transaction)

8. Quiz

Question 1:

Which SOLID principle is violated in the Report class?

class Report:
    def generate_pdf_report(self, data):
        pass

    def generate_csv_report(self, data):
        pass

    def print_report(self, report):
        pass

A) Single Responsibility Principle (SRP)
B) Open/Closed Principle (OCP)
C) Liskov Substitution Principle (LSP)
D) Interface Segregation Principle (ISP)

Question 2:

Which SOLID principle is violated in the Vehicle class?

class Vehicle:
    def start_engine(self):
        pass

    def stop_engine(self):
        pass

    def fly(self):
        # Hint: Logic for flying (applicable only for certain vehicles)
        pass

A) Single Responsibility Principle (SRP)
B) Open/Closed Principle (OCP)
C) Liskov Substitution Principle (LSP)
D) Interface Segregation Principle (ISP)

Question 3:

Which SOLID principle is violated in the NewsPublisher class?

class NewsPublisher:
    def publish_news(self, news):
        if self.platform == "Facebook":
            # Publish news to Facebook
            pass
        elif self.platform == "Twitter":
            # Publish news to Twitter
            pass

A) Single Responsibility Principle (SRP)
B) Open/Closed Principle (OCP)
C) Liskov Substitution Principle (LSP)
D) Dependency Inversion Principle (DIP)

Question 4:

Which SOLID principle is violated in the code above?

class Rectangle:
    def set_dimensions(self, width, height):
        self.width = width
        self.height = height

class Square(Rectangle):
    def set_dimensions(self, side):
        self.width = side
        self.height = side

A) Single Responsibility Principle (SRP)
B) Open/Closed Principle (OCP)
C) Liskov Substitution Principle (LSP)
D) Dependency Inversion Principle (DIP)

Question 5:

Which SOLID principle is most likely to be violated if the EmailSender class is used directly in high-level modules?

class EmailSender:
    def send_email(self, content, smtp_server):
        # Logic to send email using ``SMTP`` server
        pass

A) Single Responsibility Principle (SRP)
B) Open/Closed Principle (OCP)
C) Interface Segregation Principle (ISP)
D) Dependency Inversion Principle (DIP)

Question 6:

Which SOLID principle is violated in the UserInterface class?

class UserInterface:
    def display_text(self, text):
        pass

    def play_audio(self, audio):
        # Play audio (not needed for text-based interfaces)
        pass

A) Single Responsibility Principle (SRP)
B) Liskov Substitution Principle (LSP)
C) Interface Segregation Principle (ISP)
D) Dependency Inversion Principle (DIP)

Question 7:

Which SOLID principle is violated in the MediaPlayer class?

class MediaPlayer:
    def play_media(self, file):
        if file.type == "audio":
            # Play audio
            pass
        elif file.type == "video":
            # Play video
            pass

A) Single Responsibility Principle (SRP)
B) Open/Closed Principle (OCP)
C) Liskov Substitution Principle (LSP)
D) Dependency Inversion Principle (DIP)

9. Homework

Task 1: SOLID Recipe Organizer:

Objective: Update an application below to manage a variety of recipes that allow users to add new recipes, store them, and display them in an organized manner. The application should be adaptable to different types of recipes and dietary requirements.

Requirements:

  • Users should be able to create new recipes and specify if they are for a special diet.
  • The application should be able to save recipes to a file or database.
  • Users should be able to retrieve a list of all recipes or search for recipes by various criteria.
  • Users should be able to update and delete recipes.
class RecipeOrganizer:
    def __init__(self):
        self.recipes = []

    def add_recipe(self, title, ingredients, instructions):
        self.recipes.append({
            'title': title,
            'ingredients': ingredients,
            'instructions': instructions
        })

    def display_recipes(self):
        for recipe in self.recipes:
            print(recipe['title'])
            print('Ingredients:', recipe['ingredients'])
            print('Instructions:', recipe['instructions'])

    def save_to_file(self):
        with open('recipes.txt', 'w') as file:
            for recipe in self.recipes:
                file.write(f"{recipe['title']}\n")
                file.write(f"{recipe['ingredients']}\n")
                file.write(f"{recipe['instructions']}\n\n")

# Usage
organizer = RecipeOrganizer()
organizer.add_recipe('Pasta', ['Pasta', 'Tomato'], 'Boil pasta, add tomato sauce')
organizer.display_recipes()
organizer.save_to_file()

Try to identify all violated SOLID principles and re-write this application.

Task 2: Re-write your apps

Rewrite your apps you have created already to match SOLID. Don't forget to split logic into different modules as well.