sentenz / convention

General articles, conventions, and guides.
https://sentenz.github.io/convention/
Apache License 2.0
4 stars 2 forks source link

Create an article about `CUPID` of `Software Design Principles` #353

Open sentenz opened 2 weeks ago

sentenz commented 2 weeks ago

CUPID

CUPID is an acronym of principles for writing maintainable, scalable, and robust software. CUPID emphasizes human-centered, adaptable over time, and moving beyond rigid principles to flexible, holistic properties.

  1. Components and Features

    • Composable

      Composable code is easy to reuse and integrate, encouraging minimal dependencies and intention-revealing interfaces. This makes it accessible and reduces the chance of conflicting integrations.

    • Unix Philosophy

      The Unix Philosophy stresses that code should do one thing well, similar to the Single Responsibility Principle but from the user's perspective. It advocates for focused, modular design.

    • Predictability

      Predictability refers to code behaving as expected, making it easy to test and reason about. Predictable code should be deterministic and observable, aiding in debugging and maintaining reliability.

    • Idiomatic

      Idiomatic software is written in a way that feels natural within its language or framework, reducing the learning curve and making it easier for developers to navigate unfamiliar code.

    • Domain-Based

      Domain-based design ensures that code aligns closely with the problem it addresses, using terminology and structures that reflect the business or application domain, reducing cognitive friction.

  2. Examples and Explanations

Certainly! Let's illustrate each of the CUPID principles with code examples using a coherent scenario. We'll build a simple weather data processing application in Python that fetches weather data, processes it, and outputs useful insights. This example will help demonstrate how to apply CUPID principles in practice.


Scenario Overview

We are tasked with creating a system that:

Fetches weather data from an API.

Parses and processes the data.

Calculates average temperatures.

Identifies weather patterns.

Outputs the results in a readable format.


  1. Composable

Principle: Components should be designed to be easily combined in various ways.

Implementation:

We'll create small, reusable functions and classes that can be composed together to build the application.

import requests

def fetch_weather_data(api_url):
    response = requests.get(api_url)
    response.raise_for_status()
    return response.json()

def parse_weather_data(data):
    return data['weather_readings']

def calculate_average_temperature(readings):
    temperatures = [reading['temperature'] for reading in readings]
    return sum(temperatures) / len(temperatures)

def identify_weather_patterns(readings):
    # Simple pattern identification
    patterns = set(reading['condition'] for reading in readings)
    return patterns

Usage:

api_url = 'https://api.weather.com/data'
data = fetch_weather_data(api_url)
readings = parse_weather_data(data)
average_temp = calculate_average_temperature(readings)
patterns = identify_weather_patterns(readings)

Explanation:

Each function performs a single task.

Functions can be recombined or extended without modifying their internal logic.

Encourages reusability and flexibility.


  1. Unix Philosophy

Principle: Build simple, modular tools that perform one task well.

Implementation:

Following the Unix philosophy, we'll create command-line tools that can work together using standard input and output streams.

# fetch_data.py
import sys
import requests
import json

def fetch_weather_data(api_url):
    response = requests.get(api_url)
    response.raise_for_status()
    print(json.dumps(response.json()))

if __name__ == '__main__':
    api_url = sys.argv[1]
    fetch_weather_data(api_url)

# parse_data.py
import sys
import json

def parse_weather_data():
    data = json.load(sys.stdin)
    readings = data['weather_readings']
    print(json.dumps(readings))

if __name__ == '__main__':
    parse_weather_data()

# calculate_average.py
import sys
import json

def calculate_average_temperature():
    readings = json.load(sys.stdin)
    temperatures = [reading['temperature'] for reading in readings]
    average = sum(temperatures) / len(temperatures)
    print(average)

if __name__ == '__main__':
    calculate_average_temperature()

Usage:

python fetch_data.py https://api.weather.com/data | \
python parse_data.py | \
python calculate_average.py

Explanation:

Each script is a standalone tool.

They can be chained together using pipes.

Follows the Unix philosophy of composability and simplicity.


  1. Predictable

Principle: Software should behave in a consistent and expected manner.

Implementation:

We'll write pure functions without side effects and include tests to ensure predictable behavior.

def calculate_average_temperature(readings):
    temperatures = [reading['temperature'] for reading in readings]
    return sum(temperatures) / len(temperatures)

Test:

# test_calculate_average.py
def test_calculate_average_temperature():
    readings = [
        {'temperature': 20},
        {'temperature': 22},
        {'temperature': 24},
    ]
    assert calculate_average_temperature(readings) == 22

test_calculate_average_temperature()

Explanation:

Function output depends solely on input.

No reliance on external state.

Easy to test and debug.


  1. Idiomatic

Principle: Code should follow the conventions and best practices of the language.

Implementation:

We'll use Pythonic constructs and adhere to PEP 8 style guidelines.

from typing import List, Dict

def calculate_average_temperature(readings: List[Dict[str, float]]) -> float:
    temperatures = [reading['temperature'] for reading in readings]
    return sum(temperatures) / len(temperatures)

Explanation:

Uses type hints for better readability and tooling support.

List comprehensions are preferred over loops for creating lists.

Code formatting follows Python's standard conventions.


  1. Domain-Based

Principle: Design should be driven by the problem domain.

Implementation:

We'll create classes that represent domain concepts like WeatherData and WeatherAnalyzer.

class WeatherData:
    def __init__(self, api_url):
        self.api_url = api_url
        self.readings = []

    def fetch(self):
        response = requests.get(self.api_url)
        response.raise_for_status()
        data = response.json()
        self.readings = data['weather_readings']

class WeatherAnalyzer:
    def __init__(self, readings):
        self.readings = readings

    def calculate_average_temperature(self):
        temperatures = [r['temperature'] for r in self.readings]
        return sum(temperatures) / len(temperatures)

    def identify_patterns(self):
        return set(r['condition'] for r in self.readings)

Usage:

weather_data = WeatherData('https://api.weather.com/data')
weather_data.fetch()

analyzer = WeatherAnalyzer(weather_data.readings)
average_temp = analyzer.calculate_average_temperature()
patterns = analyzer.identify_patterns()

print(f"Average Temperature: {average_temp}")
print(f"Weather Patterns: {patterns}")

Explanation:

Classes represent real-world entities and actions.

Aligns code structure with the problem domain.

Improves code readability and maintainability.


Combined Example

Here's how all principles come together in a cohesive application.

import requests
from typing import List, Dict, Set

class WeatherData:
    def __init__(self, api_url: str):
        self.api_url = api_url
        self.readings: List[Dict[str, float]] = []

    def fetch(self) -> None:
        response = requests.get(self.api_url)
        response.raise_for_status()
        data = response.json()
        self.readings = data['weather_readings']

class WeatherAnalyzer:
    def __init__(self, readings: List[Dict[str, float]]):
        self.readings = readings

    def calculate_average_temperature(self) -> float:
        temperatures = [r['temperature'] for r in self.readings]
        return sum(temperatures) / len(temperatures)

    def identify_patterns(self) -> Set[str]:
        return set(r['condition'] for r in self.readings)

def main():
    # Composable and Unix Philosophy: Using small, reusable components.
    weather_data = WeatherData('https://api.weather.com/data')
    weather_data.fetch()

    analyzer = WeatherAnalyzer(weather_data.readings)
    average_temp = analyzer.calculate_average_temperature()
    patterns = analyzer.identify_patterns()

    # Predictable: Outputs are consistent based on inputs.
    print(f"Average Temperature: {average_temp}")
    print(f"Weather Patterns: {patterns}")

if __name__ == '__main__':
    main()

Key Points:

Composable: The WeatherData and WeatherAnalyzer classes can be used independently or together.

Unix Philosophy: Each class and function has a single responsibility.

Predictable: Functions and methods produce consistent results without side effects.

Idiomatic: The code uses Python best practices, including type hints and following PEP 8.

Domain-Based: The design models real-world concepts, making the code intuitive and aligned with the domain.


Conclusion

By applying the CUPID principles:

Composable: We built modular components that can be reused and combined in various ways.

Unix Philosophy: Each part of the code does one thing well, making it simple and maintainable.

Predictable: Our functions and methods behave consistently, facilitating easier testing and debugging.

Idiomatic: We wrote code that is natural to Python, improving readability for any Python developer.

Domain-Based: We structured our code around real-world entities, enhancing clarity and alignment with business needs.