Skip to content

Trially Engineering Principles

This document outlines the core engineering principles at Trially. These principles guide our development practices and help maintain high-quality, maintainable code.

1. Single Responsibility Principle (SRP)

Definition: Functions should have one clear purpose.

Good Example:

def calculate_total_price(items):
    return sum(item.price for item in items)

def apply_discount(total, discount_percentage):
    return total * (1 - discount_percentage / 100)

Bad Example:

def process_order(items, discount_percentage):
    total = sum(item.price for item in items)
    discounted_total = total * (1 - discount_percentage / 100)

    return discounted_total

In the bad example, the function is doing too many things: calculating total, applying discount, and potentially more. It violates SRP.

2. Open/Closed Principle

Definition: Software entities should be open for extension but closed for modification.

Good Example:

from abc import ABC, abstractmethod
from typing import List

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

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

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

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

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

def calculate_total_area(shapes: List[Shape]) -> float:
    return sum(shape.area() for shape in shapes)

In this example, we can add new shapes without modifying existing code.

Bad Example:

def calculate_area(shape_type, *args):
    if shape_type == "rectangle":
        return args[0] * args[1]
    elif shape_type == "circle":
        return 3.14 * args[0] ** 2
    # Adding a new shape requires modifying this function

3. KISS (Keep It Simple, Stupid)

Definition: Write simple code and avoid unnecessary complexity. We usually prefer simplicity and readability over optimization.

Good Example:

def is_even(number: int) -> bool:
    return number % 2 == 0

Bad Example:

def is_even(number):
    return (number & 1) ^ 1 == 1

While the bad example might be marginally faster, it sacrifices readability and simplicity.

4. DRY (Don't Repeat Yourself)

Definition: Avoid duplicating code.

Good Example:

from typing import Any

def send_notification(user: User, message: str) -> None:
    # Common notification logic

def notify_order_shipped(user: User, order: Order) -> None:
    message: str = f"Your order {order.id} has been shipped."
    send_notification(user, message)

def notify_order_delivered(user: User, order: Order) -> None:
    message: str = f"Your order {order.id} has been delivered."
    send_notification(user, message)

Bad Example:

def notify_order_shipped(user, order):
    # Duplicate notification logic
    pass

def notify_order_delivered(user, order):
    # Duplicate notification logic
    pass

5. Principle of Least Astonishment

Definition: Write intuitive, predictable code.

Good Example:

from typing import List

class UserAccount:
    def __init__(self, username: str, email: str):
        self.username: str = username
        self.email: str = email
        self.is_active: bool = True

    def deactivate(self) -> None:
        self.is_active = False

    def reactivate(self) -> None:
        self.is_active = True

    def update_email(self, new_email: str) -> None:
        self.email = new_email

Bad Example:

class UserAccount:
    def __init__(self, username, email):
        self.username = username
        self.email = email
        self.status = 1  # 1 for active, 0 for inactive

    def toggle(self):
        self.status = 1 - self.status

    def process(self, action, value=None):
        if action == "status":
            self.toggle()
        elif action == "email":
            if self.status == 1:
                self.email = value
            else:
                raise Exception("Cannot update email of inactive account")

In the good example, the code is easy to read and it does exactly what the function name says it does. Additionally, the use of type hints provides clarity about the expected types of inputs and outputs, further reducing the potential for surprises or misunderstandings.

The bad example is not intuitive and can lead to unexpected behavior. The lack of type hints makes it unclear what types of values are expected for each parameter and attribute, potentially leading to type-related errors or confusion.

Clear typing is crucial for adhering to the Principle of Least Astonishment. It provides immediate information about the nature of data being handled, reducing the likelihood of type-related bugs and making the code's behavior more predictable. In the good example, anyone using this class can quickly understand that username and email are strings, is_active is a boolean, and all methods return None. This clarity helps prevent misuse and makes the code more self-documenting.

6. Separation of Concerns

Definition: Organize code into independent modules.

Good Example:

# database.py
def save_to_database(data: str) -> None:
    # Database logic

# api.py
def fetch_from_api(url: str) -> str:
    # API fetching logic

# business_logic.py
def process_data(raw_data: str) -> str:
    # Business logic

# main.py
from database import save_to_database
from api import fetch_from_api
from business_logic import process_data

def main():
    raw_data = fetch_from_api(URL)
    processed_data = process_data(raw_data)
    save_to_database(processed_data)

Bad Example:

# monolith.py
def do_everything():
    # Fetch from API
    # Process data
    # Save to database
    # Handle errors
    # Log results
    # Send notifications
    pass

By following these principles, we ensure that our code at Trially is maintainable, scalable, and easy to understand. New hires should study these principles and strive to apply them in their daily work.

7. Don't Reinvent the Wheel

Definition: Use existing, well-tested solutions for common problems when appropriate, rather than creating new implementations from scratch.

Good Example:

import pendulum

def get_current_year() -> int:
    return pendulum.now().year

Bad Example:

import time

def get_current_year() -> int:
    current_time = time.time()
    seconds_per_year = 365.25 * 24 * 60 * 60
    years_since_epoch = int(current_time / seconds_per_year)
    return 1970 + years_since_epoch

The good example uses the pendulum module, which is well-tested and handles edge cases like leap years. The bad example unnecessarily reimplements this functionality, potentially introducing bugs.

8. Write Self-Documenting Code

Definition: Code should be clear and self-explanatory without excessive comments. Use meaningful variable and function names, and structure your code logically.

Good Example:

from typing import List

class Item:
    def __init__(self, price: float):
        self.price = price

def apply_discount(subtotal: float, discount_percentage: float) -> float:
    return subtotal * (discount_percentage / 100)

def calculate_total_price(items: List[Item], discount_percentage: float) -> float:
    subtotal = sum(item.price for item in items)
    discount = apply_discount(subtotal, discount_percentage)
    return subtotal - discount

Bad Example:

def calc(i, d):
    # Calculate subtotal
    s = sum(p for p in i)
    # Apply discount
    return s - (s * (d / 100))

The good example uses descriptive variable and function names, making the code's purpose clear without comments. The bad example uses abbreviated names, making it harder to understand without additional explanation.

9. Boy Scout Rule

Definition: Always leave the code better than you found it. Make small improvements whenever you work on existing code.

Good Example:

# Original code
def get_user(user_id):
    # ... existing implementation ...

# Improved version
def get_user(user_id: int) -> User:
    """
    Retrieve a user by their ID.

    Args:
        user_id (int): The unique identifier of the user.

    Returns:
        User: The user object if found, otherwise raises a UserNotFoundError.

    Raises:
        UserNotFoundError: If no user with the given ID exists.
    """
    # ... improved implementation ...

In this example, when working on the get_user function, the developer added type hints, a docstring, and potentially improved the implementation.

10. YAGNI (You Aren't Gonna Need It)

Definition: Don't add functionality until it's necessary. Avoid implementing features based on speculation about future needs.

Good Example:

class UserProfile:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

Bad Example:

class UserProfile:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email
        self.favorite_color = None  # We might need this in the future
        self.shoe_size = None  # Just in case we start a shoe store

    def calculate_age(self):
        # We don't need age calculation now, but let's add it anyway
        pass

The good example includes only the necessary attributes. The bad example adds speculative features that aren't currently needed, violating the YAGNI principle.

11. Composition Over Inheritance

Definition: Favor object composition over class inheritance when designing software. This often leads to more flexible designs.

Good Example:

class Engine:
    def start(self):
        return "Engine started"

class Car:
    def __init__(self, engine: Engine):
        self.engine = engine

    def start(self):
        return self.engine.start()

Bad Example:

class Engine:
    def start(self):
        return "Engine started"

class Car(Engine):
    pass

The good example uses composition by including an Engine object within the Car class. This allows for more flexibility, as we can easily change or upgrade the engine without affecting the Car class. The bad example uses inheritance, which creates a tighter coupling between Car and Engine, making it harder to change the engine implementation in the future.

12. Interface Segregation Principle

Definition: Clients should not be forced to depend on interfaces they do not use. In other words, keep interfaces small and focused.

Good Example:

from abc import ABC, abstractmethod

class Printable(ABC):
    @abstractmethod
    def print(self):
        pass

class Scannable(ABC):
    @abstractmethod
    def scan(self):
        pass

class Faxable(ABC):
    @abstractmethod
    def fax(self):
        pass

class ModernPrinter(Printable, Scannable):
    def print(self):
        print("Printing...")

    def scan(self):
        print("Scanning...")

class OldFaxMachine(Printable, Faxable):
    def print(self):
        print("Printing...")

    def fax(self):
        print("Faxing...")

Bad Example:

from abc import ABC, abstractmethod

class AllInOneMachine(ABC):
    @abstractmethod
    def print(self):
        pass

    @abstractmethod
    def scan(self):
        pass

    @abstractmethod
    def fax(self):
        pass

class ModernPrinter(AllInOneMachine):
    def print(self):
        print("Printing...")

    def scan(self):
        print("Scanning...")

    def fax(self):
        raise NotImplementedError("This printer cannot fax")

In the good example, interfaces are segregated, allowing classes to implement only the methods they need. The bad example forces the ModernPrinter to implement a method it doesn't support.

13. Adapter/Wrapper Pattern

Definition: Use an adapter to convert the interface of a class into another interface clients expect. This is especially useful when integrating third-party libraries or services.

Good Example:

# Third-party analytics service
class PosthogAnalytics:
    def track_event(self, event_name, properties):
        print(f"Posthog: Tracked {event_name} with {properties}")

# Our adapter
class AnalyticsService:
    def __init__(self, analytics_provider):
        self.provider = analytics_provider

    def track(self, event_name, data):
        # Here we can add our own logic, data transformation, etc.
        self.provider.track_event(event_name, data)

# Usage in our application
posthog = PosthogAnalytics()
our_analytics = AnalyticsService(posthog)

# Now we use our_analytics throughout our codebase
our_analytics.track("button_click", {"button_id": "submit"})

Bad Example:

# Directly using third-party service in our codebase
posthog = PosthogAnalytics()

# Usage scattered throughout our application
posthog.track_event("button_click", {"button_id": "submit"})

The good example uses an adapter to wrap the third-party analytics service. This approach has several benefits:

  • We can easily switch to a different analytics provider by creating a new adapter.
  • We can add our own logic, such as data transformation or validation, in the adapter.
  • Our codebase is not tightly coupled to the third-party service's interface.

The bad example directly uses the third-party service, which makes it harder to change providers in the future and couples our code tightly to their specific interface.