Design Principles in Python/Django
Design principles in Python, like in any programming language, help create clean, maintainable, and efficient code. Here are some key design principles with examples:
1. DRY (Don’t Repeat Yourself)
Avoid duplication of code by abstracting repeated patterns into functions or classes.
Django inherently promotes the DRY principle through its ORM, forms, and admin interface.
Example: Using Serializers
Instead of writing validation logic separately, DRF serializers allow you to encapsulate this logic within the serializer itself.
# serializers.py
from rest_framework import serializers
from .models import Expense, Category, Balance
class ExpenseSerializer(serializers.ModelSerializer):
class Meta:
model = Expense
fields = ['id', 'user', 'category', 'amount', 'description', 'date']
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['id', 'name', 'description', 'user']
class BalanceSerializer(serializers.ModelSerializer):
class Meta:
model = Balance
fields = ['id', 'total_balance']
2. KISS (Keep It Simple, Stupid)
Keep your code as simple as possible. Avoid unnecessary complexity.
Django’s design philosophy emphasizes simplicity and readability.
Example: Using Django Rest Framework Generic Views
Instead of writing complex view logic, you can use DRF’s generic views to handle common patterns like creating, retrieving, and listing objects.
# views.py
from rest_framework import generics
from .models import Expense, Category, Balance
from .serializers import ExpenseSerializer, CategorySerializer, BalanceSerializer
class ExpenseListView(generics.ListCreateAPIView):
queryset = Expense.objects.all()
serializer_class = ExpenseSerializer
class ExpenseDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Expense.objects.all()
serializer_class = ExpenseSerializer
class CategoryListView(generics.ListCreateAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer
class CategoryDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer
class BalanceListView(generics.ListCreateAPIView):
queryset = Balance.objects.all()
serializer_class = BalanceSerializer
3. YAGNI (You Ain’t Gonna Need It)
Don’t add functionality until it’s necessary.
Focus on current requirements rather than future possibilities.
Example: Simple User Profile
Begin with a straightforward user model and add complexity only when necessary.
# models.py
class User(AbstractUser):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
email = models.EmailField(unique=True)
def __str__(self):
return self.username
4. Separation of Concerns
Different parts of your code should have different responsibilities.
Django’s MVC (Model-View-Controller) architecture separates data handling (Models), user interface (Templates), and application logic (Views).
Example: Separating Business Logic from Views
Keep business logic in models or services instead of views.
# models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class AnstractModel(models.Model):
class Meta:
abstract = True
deleted = models.BooleanField(default=False)
date_created = models.DateTimeField('Date created', auto_now_add=True)
date_last_updated = models.DateTimeField('Data last updated', auto_now=True)
def __id__(self) -> int:
return self.id
def delete(self, *args, **kwargs):
self.deleted = True
self.save()
def hard_delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
....
# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import generics
from django.shortcuts import get_object_or_404
from .models import Expense, Category, Balance, User
from .serializers import ExpenseSerializer, CategorySerializer, BalanceSerializer
class ExpenseListView(generics.ListCreateAPIView):
queryset = Expense.objects.all()
serializer_class = ExpenseSerializer
class ExpenseDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Expense.objects.all()
serializer_class = ExpenseSerializer
class CategoryListView(generics.ListCreateAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer
class CategoryDetailView(generics.RetrieveUpdateDestroyAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer
class BalanceListView(generics.ListCreateAPIView):
queryset = Balance.objects.all()
serializer_class = BalanceSerializer
class UserTotalExpensesView(APIView):
def get(self, request, user_id):
user = get_object_or_404(User, id=user_id)
total_expenses = user.get_total_expenses()
return Response({'total_expenses': total_expenses})
# urls.py
from django.urls import path
from .views import ExpenseListView, ExpenseDetailView, CategoryListView, CategoryDetailView, BalanceListView, UserTotalExpensesView
urlpatterns = [
path('expenses/', ExpenseListView.as_view(), name='expense-list'),
path('expenses/<int:pk>/', ExpenseDetailView.as_view(), name='expense-detail'),
path('categories/', CategoryListView.as_view(), name='category-list'),
path('categories/<int:pk>/', CategoryDetailView.as_view(), name='category-detail'),
path('balances/', BalanceListView.as_view(), name='balance-list'),
path('users/<int:user_id>/total-expenses/', UserTotalExpensesView.as_view(), name='user-total-expenses'),
]
5. SOLID Principles
SOLID is an acronym for five design principles intended to make software designs more understandable, flexible, and maintainable.
S: Single Responsibility Principle (SRP)
Each class should have one responsibility.
# model/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.db import models
class AbstractModel(models.Model):
class Meta:
abstract = True
deleted = models.BooleanField(default=False)
date_created = models.DateTimeField('Date created', auto_now_add=True)
date_last_updated = models.DateTimeField('Data last updated', auto_now=True)
def __id__(self) -> int:
return self.id
def delete(self, *args, **kwargs):
self.deleted = True
self.save()
def hard_delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
class User(AbstractUser):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
email = models.EmailField(unique=True)
def __str__(self):
return self.username
class Category(AbstractModel):
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
db_table = "db_category"
name = models.CharField('Name', max_length=100, unique=True)
description = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
return self.name
class Expense(AbstractModel):
class Meta:
verbose_name = "Expense"
verbose_name_plural = "Expenses"
db_table = "db_expense"
user = models.ForeignKey(User, on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=10, decimal_places=2)
description = models.TextField()
date = models.DateField()
class Balance(models.Model):
class Meta:
verbose_name = "Balance"
verbose_name_plural = "Balances"
db_table = "db_balance"
total_balance = models.DecimalField(max_digits=10, decimal_places=2)
O: Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
# middleware.py
from django.http import HttpResponse
class BaseMiddleware:
def process_request(self, request):
raise NotImplementedError
class AuthMiddleware(BaseMiddleware):
def process_request(self, request):
if not request.user.is_authenticated:
return HttpResponse('Unauthorized', status=401)
L: Liskov Substitution Principle (LSP)
Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
# models.py
class Notification:
def send(self):
raise NotImplementedError
class EmailNotification(Notification):
def send(self):
print("Sending email")
class SMSNotification(Notification):
def send(self):
print("Sending SMS")
def notify(notification: Notification):
notification.send()
# Usage
email_notification = EmailNotification()
sms_notification = SMSNotification()
notify(email_notification) # Output: Sending email
notify(sms_notification) # Output: Sending SMS
I: Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
# Interface Segregation by splitting larger views
from django.views import View
from django.http import JsonResponse
class CreateMixin:
def create(self, request, *args, **kwargs):
return JsonResponse({'message': 'Create not implemented'}, status=405)
class ReadMixin:
def read(self, request, *args, **kwargs):
return JsonResponse({'message': 'Read not implemented'}, status=405)
class UpdateMixin:
def update(self, request, *args, **kwargs):
return JsonResponse({'message': 'Update not implemented'}, status=405)
class DeleteMixin:
def delete(self, request, *args, **kwargs):
return JsonResponse({'message': 'Delete not implemented'}, status=405)
class MyView(CreateMixin, ReadMixin, View):
def read(self, request, *args, **kwargs):
return JsonResponse({'message': 'Reading data'})
D: Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
class PaymentService:
def process_payment(self):
raise NotImplementedError
class StripePaymentService(PaymentService):
def process_payment(self):
print("Processing payment with Stripe")
class PayPalPaymentService(PaymentService):
def process_payment(self):
print("Processing payment with PayPal")
# views.py
from .services import PaymentService
class PaymentView(View):
def __init__(self, payment_service: PaymentService):
self.payment_service = payment_service
def post(self, request, *args, **kwargs):
self.payment_service.process_payment()
return JsonResponse({'message': 'Payment processed'})
# Usage
stripe_service = StripePaymentService()
paypal_service = PayPalPaymentService()
stripe_payment_view = PaymentView(stripe_service)
paypal_payment_view = PaymentView(paypal_service)