B02 SOLID Principles

Bad Single Responsibility

# Single Responsibility Principle - Bad Example
# ------------------------------------------------------------------------------
# The Single Responsibility Principle (SRP) says that a class should
# have only one reason to change. This example bundles reading,
# writing and processing into one class, so it breaks SRP.


class MyCustomFileFormat(object):
    """ This class violates the Single Responsibility Principle because:

        1. It reads the file and stores the data
        2. It writes data to the file
        3. It processes the stored data

    """

    def __init__(self, filename):
        self.filename = filename
        self.data = ""

    def read(self):
        # Responsibility: Read the file
        with open(self.filename, "r") as f:
            self.data = f.read()
            return self.data

    def write(self, text):
        # Responsibility: Write to the file
        with open(self.filename, "w") as f:
            f.write(text)

    def process(self):
        # Responsibility: Process the data
        self.data = self.data.upper()
        return self.data


# Create the file reader
reader = MyCustomFileFormat("test_input.myformat")
print(reader.read())

# Process the data
print(reader.process())

# Create the file writer
writer = MyCustomFileFormat("test_output.myformat")
writer.write(reader.data)

Good Single Responsibility

# Single Responsibility Principle - Good Example
# ------------------------------------------------------------------------------
# The Single Responsibility Principle (SRP) states that a class should
# do only one thing. Here the reading, writing and processing logic
# are split into separate classes.
#
# By splitting the class into three classes, we can now reuse the classes in
# other parts of the program. A change in one class will not affect the other
# classes. This makes the code more maintainable and easier to understand.

class MyCustomFileFormatReader(object):
    # Responsibility: Read the file

    def __init__(self, filename):
        self.filename = filename

    def read(self):
        with open(self.filename, "r") as f:
            return f.read()


class MyCustomFileFormatWriter(object):
    # Responsibility: Write to the file

    def __init__(self, filename):
        self.filename = filename

    def write(self, text):
        with open(self.filename, "w") as f:
            f.write(text)


class FileProcessor(object):
    # Responsibility: Process the data

    def __init__(self):
        self.data = ""

    def process(self, data):
        self.data = data.lower()
        return self.data


# Create the file reader
reader = MyCustomFileFormatReader("test_input.myformat")
content = reader.read()
print(content)

# Process the data
processor = FileProcessor()
new_content = processor.process(content)
print(new_content)

# Create the file writer
writer = MyCustomFileFormatWriter("test_output.myformat")
writer.write(new_content)

Bad Open Closed

# Open/Closed Principle - Bad Example
# ------------------------------------------------------------------------------
# The Open/Closed Principle (OCP) states that code should be open for
# extension but closed for modification.
#
# Adding a new format here in the example below requires changing the
# FileProcessor class, so the open/closed principle is violated.

class FileProcessor(object):

    def __init__(self, file_format="upper"):
        self.format = file_format
        self.data = ""

    def process(self, text):

        # This violates the Open/Closed Principle because a new file format
        # cannot be added without modifying the FileProcessor class.

        if self.format == "upper":
            # Responsibility: Process the data
            self.data = text.upper()

        elif self.format == "lower":
            # Responsibility: Process the data
            self.data = text.lower()

        else:
            raise ValueError("Invalid format: {}".format(format))

        return self.data


process = FileProcessor(file_format="upper")
print(process.process("Hello World!"))

Good Open Closed

# Open/Closed Principle - Good Example
# ------------------------------------------------------------------------------
# The Open/Closed Principle (OCP) says that classes should be
# extendable without needing to modify their source.
#
# By using composition, we can extend the functionality of the FileProcessor
# class without changing its code. This allows us to add new file formats
# without modifying the existing class, adhering to the OCP.

class LowercaseFormat(object):

    @staticmethod
    def process(text):
        return text.lower()


class UppercaseFormat(object):
    # Responsibility: Process the data

    @staticmethod
    def process(text):
        return text.lower()


# Add more file formats here...

class FileProcessor(object):

    # Use composition to extend the functionality of the FileProcessor class
    # without modifying the class itself.

    def __init__(self, file_format):
        self.formatter = file_format
        self.data = ""

    def process(self, text):
        self.data = self.formatter.process(text)
        return self.data


processor = FileProcessor(LowercaseFormat())
print(processor.process("Hello World!"))

Bad Liskov Subsitution

# Liskov Substitution Principle - Bad Example
# ------------------------------------------------------------------------------
# The Liskov Substitution Principle (LSP) requires that subclasses
# can stand in for their base class without altering the correctness of the
# program.
#
# Here without the LSP, we have a situation where a subclass (Baby) cannot be
# used in place of its superclass (Human). As a code smell, there is a
# conditional check in the `work` method of the `Human` class that
# checks the type of the object.

class Human(object):

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def eat(self):
        print("{} eating".format(self.name))

    def sleep(self):
        print("{} sleeping".format(self.name))

    def work(self):

        # Code smell: Type checking or conditional logic to determine the
        # behaviour and thus the child class and the parent class are not
        # substitutable. We have divergent behaviour for Human and Baby
        # when the work method is called.

        if type(self) == Baby:
            raise RuntimeError("Too young to work")

        print("{} working".format(self.name))


class Baby(Human):

    def suckle(self):
        print("{} suckling".format(self.name))

Good Liskov Substitution

# Liskov Substitution Principle - Good Example
# ------------------------------------------------------------------------------
# The Liskov Substitution Principle (LSP) means that objects of a
# superclass should be replaceable with objects of its subclasses
# without breaking the program.
#
# In this example, we have a base class `Human` and two subclasses `Adult` and
# `Baby`. Both subclasses implement all the methods of the `Human` class,
# allowing them to be substituted for `Human` without violating the LSP.

class Human(object):

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def eat(self):
        print("{} eating".format(self.name))

    def sleep(self):
        print("{} sleeping".format(self.name))


class Adult(Human):

    # GOOD: Adult can be substituted for Human because it implements all the
    # methods of the Human class and it does not violate the Liskov
    # Substitution Principle.

    def work(self):
        print("{} working".format(self.name))


class Baby(Human):

    # GOOD: Baby can be substituted for Human because it implements all the
    # methods of the Human class and it does not violate the Liskov
    # Substitution Principle.

    def suckle(self):
        print("{} suckling".format(self.name))

Bad Interface Segregation

# Interface Segregation Principle - Bad Example
# ------------------------------------------------------------------------------
# The Interface Segregation Principle (ISP) advises that clients
# should not be forced to depend on methods they do not use.
#
# In this example, we have a base class `Device` that defines methods
# for powering on, powering off, adjusting volume, and brightness. However,
# the `HeadSet` and `Monitor` classes only use a subset of these methods,
# leading to a violation of the ISP.

class Device(object):

    def __init__(self, name):
        self.name = name

    def power_on(self):
        pass

    def power_off(self):
        pass

    def volume_up(self):
        # Code smell: Used only by Headset
        pass

    def volume_down(self):
        # Code smell: Used only by Headset
        pass

    def brightness_up(self):
        # Code smell: Used only by Monitor
        pass

    def brightness_down(self):
        # Code smell: Used only by Monitor
        pass


class HeadSet(Device):

    def __init__(self, name):
        super(HeadSet, self).__init__(name)

    def power_on(self):
        print("Headset powered on")

    def power_off(self):
        print("Headset powered off")

    def volume_up(self):
        print("Headset volume up")

    def volume_down(self):
        print("Headset volume down")


class Monitor(Device):

    def __init__(self, name):
        super(Monitor, self).__init__(name)

    def power_on(self):
        print("Monitor powered on")

    def power_off(self):
        print("Monitor powered off")

    def brightness_up(self):
        print("Monitor brightness up")

    def brightness_down(self):
        print("Monitor brightness down")

Good Interface Segregation

# Interface Segregation Principle - Good Example
# ------------------------------------------------------------------------------
# The Interface Segregation Principle (ISP) breaks large interfaces
# into focused ones.
#
# This example defines a base Device class and several mixins for specific
# functionalities. Each device class inherits from Device and the relevant
# mixins, allowing for a clean separation of concerns and avoiding the need
# for devices to implement methods they do not use.

class Device(object):
    # Defines only the common methods for all devices

    def __init__(self, name, *args, **kwargs):
        super(Device, self).__init__(*args, **kwargs)
        self.name = name

    def power_on(self):
        pass

    def power_off(self):
        pass


class SoundMixin(object):
    # Defines only the methods for all sound devices

    def __init__(self, *args, **kwargs):
        super(SoundMixin, self).__init__(*args, **kwargs)

    def volume_up(self):
        pass

    def volume_down(self):
        pass


class VisualMixin(object):
    # Defines only the methods for all visual devices

    def __init__(self, *args, **kwargs):
        super(VisualMixin, self).__init__(*args, **kwargs)

    def brightness_up(self):
        pass

    def brightness_down(self):
        pass


class TypingMixin(object):
    # Defines only the methods for all typing devices

    def __init__(self, *args, **kwargs):
        super(TypingMixin, self).__init__(*args, **kwargs)

    def press(self):
        pass

    def release(self):
        pass

    def hold(self):
        pass


class Keyboard(Device, TypingMixin):
    pass


class HeadSet(Device, SoundMixin):
    pass


class Speaker(Device, SoundMixin):
    pass


class Monitor(Device, VisualMixin):
    pass


class Television(Device, SoundMixin, VisualMixin):
    pass


class Computer(Device, SoundMixin, VisualMixin, TypingMixin):
    pass

Bad Dependency Inversion

# Dependency Inversion Principle - Bad Example
# ------------------------------------------------------------------------------
# The Dependency Inversion Principle (DIP) dictates that high level
# modules should not depend on low level ones directly.
#
# This example violates DIP by hard-coding a dependency on a specific
# math implementation instead of using an abstraction.

class Math(object):

        @staticmethod
        def add(a, b):
            return a + b

        @staticmethod
        def subtract(a, b):
            return a - b


class Calculator(object):

    def __init__(self):

        # CODE SMELL: Math object is hard-coded (dependency)
        # and not abstracted (injection)

        self.math = Math()

    def add(self, a, b):
        result = self.math.add(a, b)
        return result

    def subtract(self, a, b):
        result = self.math.subtract(a, b)
        return result

Good Dependency Inversion

# Dependency Inversion Principle - Good Example
# ------------------------------------------------------------------------------
# The Dependency Inversion Principle (DIP) encourages depending on
# abstractions rather than concrete classes.
#
# This example demonstrates a simple math class that adheres to DIP. It defines
# an interface (IMath) that specifies the contract for math operations,
# allowing different implementations to be used without changing the
# dependent code (Calculator).


class IMath(object):
    """ Simplified interface for a math class """

    # GOOD: IMath is an abstraction that defines the contract for Math and
    # Calculator (the interface). It has no implementation details.

    @staticmethod
    def add(a, b):
        raise NotImplementedError()

    @staticmethod
    def subtract(a, b):
        raise NotImplementedError()


class Math(IMath):
    # GOOD: Both Math and Calculator depend on abstraction (MathAbc)

    @staticmethod
    def add(a, b):
        return a + b

    @staticmethod
    def subtract(a, b):
        return a - b


class Calculator(object):
    """A simple calculator class

    Args:
        math (IMath): An object that implements the IMath interface

    """

    def __init__(self, math):
        # GOOD: Both Math and Calculator depend on abstraction (MathAbc)
        self.math = math

    def add(self, a, b):
        result = self.math.add(a, b)
        return result

    def subtract(self, a, b):
        result = self.math.subtract(a, b)
        return result