Solid
The SOLID principles (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion) are fundamental to writing clean, maintainable, and scalable code. Below is a Python example of a Pipeline
class that adheres to these principles. The pipeline consists of several blocks, each representing an API call with either an API key or Bearer token for authentication. The code is kept between 100-200 lines and uses subclasses to represent different API calls.
from abc import ABC, abstractmethod
# Interface Segregation Principle: Abstract class for all API blocks
class APIBlock(ABC):
@abstractmethod
def fetch_data(self, input_data):
pass
# Single Responsibility Principle: Each block handles a single API call
class APIBlock1(APIBlock):
def __init__(self, api_key):
self.api_key = api_key
def fetch_data(self, input_data):
# Simulate API call using the api_key
response = f"Data from API 1 with key {self.api_key} based on {input_data}"
return response
class APIBlock2(APIBlock):
def __init__(self, bearer_token):
self.bearer_token = bearer_token
def fetch_data(self, input_data):
# Simulate API call using the bearer_token
response = f"Data from API 2 with bearer token {self.bearer_token} based on {input_data}"
return response
class APIBlock3(APIBlock):
def __init__(self, api_key):
self.api_key = api_key
def fetch_data(self, input_data):
# Simulate API call using the api_key
response = f"Data from API 3 with key {self.api_key} based on {input_data}"
return response
class APIBlock4(APIBlock):
def __init__(self, bearer_token):
self.bearer_token = bearer_token
def fetch_data(self, input_data):
# Simulate API call using the bearer_token
response = f"Data from API 4 with bearer token {self.bearer_token} based on {input_data}"
return response
class APIBlock5(APIBlock):
def __init__(self, api_key):
self.api_key = api_key
def fetch_data(self, input_data):
# Simulate API call using the api_key
response = f"Data from API 5 with key {self.api_key} based on {input_data}"
return response
# Open/Closed Principle: The Pipeline class is open for extension by adding new APIBlock subclasses but closed for modification.
class Pipeline:
def __init__(self, blocks):
self.blocks = blocks
def run(self, initial_input):
data = initial_input
for block in self.blocks:
data = block.fetch_data(data)
return data
# Dependency Inversion Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions.
if __name__ == "__main__":
# Instantiate the pipeline with different API blocks
pipeline = Pipeline([
APIBlock1(api_key="API_KEY_1"),
APIBlock2(bearer_token="BEARER_TOKEN_2"),
APIBlock3(api_key="API_KEY_3"),
APIBlock4(bearer_token="BEARER_TOKEN_4"),
APIBlock5(api_key="API_KEY_5"),
])
# Run the pipeline with initial input
result = pipeline.run(initial_input="Initial Input Data")
print(result)
Explanation:
-
Single Responsibility Principle (SRP): Each API block (
APIBlock1
,APIBlock2
, etc.) is responsible for only one API call. -
Open/Closed Principle (OCP): The
Pipeline
class can be extended with newAPIBlock
subclasses without modifying its code. -
Liskov Substitution Principle (LSP): Each
APIBlock
subclass can be substituted with any other subclass without affecting thePipeline
class's functionality. -
Interface Segregation Principle (ISP): The
APIBlock
abstract class enforces that each block only implements thefetch_data
method it requires. -
Dependency Inversion Principle (DIP): The
Pipeline
class depends on the abstraction (APIBlock
), not on specific implementations.
This solution adheres to the SOLID principles and provides a scalable and maintainable architecture for your pipeline. The total code is within the 100-200 lines range, and the use of subclasses makes it easy to extend the pipeline with new blocks.
When writing clean and maintainable code, several design principles can guide you, one of the most popular being SOLID principles. These principles help improve software architecture and maintainability. Here's a breakdown of SOLID principles with examples in Python:
1. S - Single Responsibility Principle (SRP)
A class should have only one reason to change, meaning it should have only one responsibility or job.
Example:
class Invoice:
def __init__(self, amount):
self.amount = amount
def calculate_total(self):
return self.amount * 1.2 # Add tax (20%)
class InvoicePrinter:
@staticmethod
def print_invoice(invoice):
print(f"Invoice Total: {invoice.calculate_total()}")
# Usage:
invoice = Invoice(100)
InvoicePrinter.print_invoice(invoice)
In this example, the Invoice
class handles the business logic of the invoice, while the InvoicePrinter
class takes care of the printing. Each class has a single responsibility.
2. O - Open/Closed Principle (OCP)
Software entities (classes, modules, functions) should be open for extension but closed for modification.
Example:
from abc import ABC, abstractmethod
class Discount(ABC):
@abstractmethod
def apply(self, total):
pass
class NoDiscount(Discount):
def apply(self, total):
return total
class PercentageDiscount(Discount):
def __init__(self, percentage):
self.percentage = percentage
def apply(self, total):
return total - (total * self.percentage)
# Usage:
discount_strategy = PercentageDiscount(0.1) # 10% discount
total = discount_strategy.apply(100)
print(f"Total after discount: {total}")
Here, the discount strategy can be extended by adding new discount classes without modifying existing code, making it follow the OCP principle.
3. L - Liskov Substitution Principle (LSP)
Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
Example:
class Bird:
def fly(self):
return "Flying"
class Sparrow(Bird):
pass
class Penguin(Bird):
def fly(self):
raise Exception("Penguins can't fly!")
# Usage:
def make_bird_fly(bird: Bird):
return bird.fly()
sparrow = Sparrow()
print(make_bird_fly(sparrow)) # Works fine
penguin = Penguin()
print(make_bird_fly(penguin)) # Throws an error (violates LSP)
In this example, Penguin
violates LSP because it's a Bird
that can't fly. A better design might avoid using fly
for Penguin
or represent it in another way to keep behavior consistent.
4. I - Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
Example:
class Printer(ABC):
@abstractmethod
def print_document(self):
pass
class Scanner(ABC):
@abstractmethod
def scan_document(self):
pass
class MultiFunctionPrinter(Printer, Scanner):
def print_document(self):
print("Printing document")
def scan_document(self):
print("Scanning document")
class SimplePrinter(Printer):
def print_document(self):
print("Printing document")
# Usage:
printer = SimplePrinter()
printer.print_document() # Works without needing to implement scan
In this example, SimplePrinter
implements only the Printer
interface, and isn't forced to implement Scanner
. This adheres to ISP by keeping interfaces small and focused.
5. D - Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Example:
class Database(ABC):
@abstractmethod
def save(self, data):
pass
class MySQLDatabase(Database):
def save(self, data):
print(f"Saving data: {data} to MySQL Database")
class Application:
def __init__(self, db: Database):
self.db = db
def save_data(self, data):
self.db.save(data)
# Usage:
db = MySQLDatabase()
app = Application(db)
app.save_data("Some important data")
Here, the Application
depends on the abstraction Database
, not the concrete MySQLDatabase
. This makes the code more flexible, allowing different types of databases to be used without changing the application logic.
By following these SOLID principles, you can create code that is easier to maintain, extend, and scale.