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:
Bad Example:
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:
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:
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:
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.