В мире программирования написание эффективного и поддерживаемого кода имеет первостепенное значение для успеха. Именно здесь вступает в действие концепция передовой практики программирования, когда разработчики следуют набору принципов, чтобы их код был не только функциональным, но и устойчивым в долгосрочной перспективе. Одним из таких принципов, который получил значительное распространение в последние годы, является принцип SOLID, который фокусируется на написании кода, который является модульным, масштабируемым и простым в обслуживании. Этот принцип состоит из пяти отдельных принципов, которые разработчики должны понимать и применять для написания высококачественного кода. В этой статье мы рассмотрим важность передовой практики программирования, представим принцип SOLID и обсудим, как применять этот принцип для написания эффективного и удобного в сопровождении кода.

Передовой опыт использования принципов SOLID

Принципы SOLID — это набор из пяти принципов, которые помогают разработчикам писать высококачественный модульный, масштабируемый и простой в обслуживании код. Эти принципы включают в себя:

  1. Принцип единой ответственности (SRP)
  2. Открытый/закрытый принцип (OCP)
  3. Принцип замещения Лисков (LSP)
  4. Принцип разделения интерфейсов (ISP)
  5. Принцип инверсии зависимостей (DIP)

Следуя этим принципам, разработчики могут создавать гибкий, надежный код, который можно легко адаптировать к изменяющимся требованиям. В следующей части мы более подробно рассмотрим каждый из руководящих принципов.

Принцип единой ответственности (SRP)

«У класса должна быть одна и только одна причина для изменения»

Другими словами, каждый компонент вашего кода (часто класс, но также и функция) должен отвечать только за одну вещь. В результате должна быть только одна причина для его изменения. Этот принцип гарантирует, что код является модульным и что изменения в одной области кодовой базы не влияют на другие части кодовой базы. Создайте меньший класс с одной обязанностью , а не универсальный курс. Создание класса, который делает все, усложнит ситуацию, если вы хотите исправить ошибки и добавить новые функции. Этот код ниже является примером из моего проекта Foresight.

@api_view(['POST'])
@permission_classes([AllowAny])
def sign_up(request):
    response = JsonResponse("Registration failed", safe=False)
    
    if request.method == 'POST':
        email = request.data.get('email')
        password = request.data.get('password')
        confirm_password = request.data.get('confirm_password')
        name = request.data.get('name')
        uname = request.data.get('uname')

        # Check if passwords match
        if password != confirm_password:
            response = JsonResponse('Passwords do not match', safe=False)
            return response
.....
@csrf_exempt
@api_view(['POST'])
@permission_classes([AllowAny])
def sign_in(request):
    response = JsonResponse({"message": "Login failed"})
    if request.method == "POST":
        email = request.data.get("email")
        password = request.data.get("password")
        if email and password:
            payload = json.dumps({
                "email": email,
                "password": password,
                "returnSecureToken": True
            })
.....

Из приведенного выше кода видно, что каждая функция обрабатывает только одну функцию. Не создавая функцию, которая делает все, мы можем легче отслеживать коды ошибок. Например, при тестировании функции входа в систему, если есть ошибка, мы можем отследить ее через функцию входа только в бэкэнде и во внешнем интерфейсе, не обращая внимания на другие функции.

Принцип открытия/закрытия (OCP)

«Программные объекты… должны быть открыты для расширения, но закрыты для модификации»

Другими словами, вам не нужно изменять существующий код, чтобы реализовать новые функции; вместо этого просто добавьте то, что вам сейчас нужно. Этот принцип гарантирует простоту расширения и поддержки кода по мере развития требований проекта. Когда дело доходит до сопровождения кода, этот принцип решает проблему устойчивости к изменениям в существующей кодовой системе. Этот код ниже является примером из моего проекта Foresight.

# Before modification

@csrf_exempt
@api_view(['POST'])
@permission_classes([AllowAny])
def sign_in(request):
    response = JsonResponse({"message": "Login failed"})
    if request.method == "POST":
        email = request.data.get("email")
        password = request.data.get("password")
        if email and password:
            payload = json.dumps({
                "email": email,
                "password": password,
                "returnSecureToken": True
            })
            api_key = "AIzaSyCEzwLyb3JKnc_OTY4CcFWaQNHwVtAn7dg"
            api_url = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword"
            headers = {"Content-Type": "application/json"}
            r = requests.post(api_url, params={"key": api_key}, headers=headers, data=payload)
            
            if r.ok:
                id_token = r.json()["idToken"]
                response = JsonResponse({"idToken": id_token})
                response.set_cookie("idToken", id_token)
            else:
                error_message = r.json().get("error", {}).get("message")
                response = JsonResponse({"message": error_message})
    else:
        response = JsonResponse({"message": "Invalid method"})
    return response
# After modification

@csrf_exempt
@api_view(['POST'])
@permission_classes([AllowAny])
def sign_in(request):
    response = JsonResponse({"message": "Login failed"})
    if request.method == "POST":
        email = request.data.get("email")
        password = request.data.get("password")
        if email and password:
            payload = json.dumps({
                "email": email,
                "password": password,
                "returnSecureToken": True
            })
            api_key = "AIzaSyCEzwLyb3JKnc_OTY4CcFWaQNHwVtAn7dg"
            api_url = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword"
            headers = {"Content-Type": "application/json"}
            r = requests.post(api_url, params={"key": api_key}, headers=headers, data=payload)
            
            if r.ok:
                id_token = r.json()["idToken"]
                refresh_token = r.json()["refreshToken"]
                expires_in = int(r.json()["expiresIn"])
                token_expiration = datetime.utcnow() + timedelta(seconds=expires_in)
                
                response = JsonResponse({"idToken": id_token, "refreshToken": refresh_token})
                response.set_cookie("idToken", id_token)
                response.set_cookie("refreshToken", refresh_token)
                response.set_cookie("tokenExpiration", token_expiration.strftime('%Y-%m-%dT%H:%M:%SZ'))
            else:
                error_message = r.json().get("error", {}).get("message")
                response = JsonResponse({"message": error_message})
    else:
        response = JsonResponse({"message": "Invalid method"})
    return response

Из приведенного выше кода видно, что когда я добавлял функции активного токена и токена обновления, я не модифицировал существующий код. Вместо этого я лишь добавил несколько строк кода в соответствии с необходимыми требованиями и функциями, которые были необходимы.

Принцип замещения Лискова (LSP)

«Функции, использующие указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом».

Проще говоря, когда подкласс переопределяет функцию, которая также определена в его родительском классе, поведение функции должно оставаться одинаковым для пользователей подкласса. Это означает, что подкласс можно использовать в качестве замены базового класса без какой-либо заметной разницы в функциональности. Например, если вы используете функцию, а кто-то модифицирует базовый класс, вы не должны испытывать никаких изменений в том, как работает функция. Эта идея способствует гибкости кода, позволяя вам вносить изменения в систему, не затрагивая существующий код. Поскольку код в моем проекте использует Django Python (в котором редко применяются концепции ООП), я приведу пример с использованием кода с открытым исходным кодом из Интернета.

def animal_leg_count(animals: list):
    for animal in animals:
        if isinstance(animal, Lion):
            print(lion_leg_count(animal))
        elif isinstance(animal, Mouse):
            print(mouse_leg_count(animal))
        elif isinstance(animal, Pigeon):
            print(pigeon_leg_count(animal))
        
animal_leg_count(animals)

Чтобы заставить эту функцию следовать принципу LSP, мы будем следовать этим требованиям LSP, постулированным Стивом Фентоном:

  • Если у суперкласса (Animal) есть метод, который принимает параметр типа суперкласса (Animal).
  • Его подкласс (голубь) должен принимать в качестве аргумента тип суперкласса (тип животного) или тип подкласса (тип голубя).
  • Если суперкласс возвращает тип суперкласса (животное).
  • Его подкласс должен возвращать тип суперкласса (тип животного) или тип подкласса (голубь).

Теперь мы можем повторно реализовать функцию animal_leg_count:

def animal_leg_count(animals: list):
    for animal in animals:
        print(animal.leg_count())
        
animal_leg_count(animals)

Функция animal_leg_count меньше заботится о типе переданного животного, она просто вызывает метод leg_count. Все, что ему известно, это то, что параметр должен быть типа Animal, либо класса Animal, либо его подкласса. Класс Animal теперь должен реализовать/определить метод leg_count. И его подклассы должны реализовать метод leg_count:

class Animal:
    def leg_count(self):
        pass

class Lion(Animal):
    def leg_count(self):
        pass

Когда он передается в функцию animal_leg_count, он возвращает количество ног у льва. Видите ли, Animal_leg_count не нужно знать тип Animal, чтобы вернуть количество ног, он просто вызывает метод leg_count типа Animal, потому что по контракту подкласс класса Animal должен реализовать функцию leg_count.

Принцип разделения интерфейсов (ISP)

«Многие клиентские интерфейсы лучше, чем один интерфейс общего назначения»

Четвертый принцип, известный как Принцип разделения интерфейсов (ISP), гласит, что ни один клиент не должен принуждаться полагаться на методы, которые он не использует. Другими словами, интерфейсы должны быть компактными и сфокусированными, а клиенты должны полагаться только на те методы, которые им необходимы. Этот принцип гарантирует, что код прост для понимания и что модификации одной части кодовой базы не повлияют на остальную часть кодовой базы. Эта проблема чаще всего возникает, когда подкласс наследует методы базового класса, которые ему не требуются. Поскольку код в моем проекте использует Django Python (в котором редко применяются концепции ООП), я приведу пример с использованием кода с открытым исходным кодом из Интернета.

import numpy as np
from abc import ABC, abstractmethod

class Mammals(ABC):
    @abstractmethod
    def swim() -> bool:
        print("Can Swim") 

    @abstractmethod
    def walk() -> bool:
        print("Can Walk") 

class Human(Mammals):
    def swim():
        return print("Humans can swim") 

    def walk():
        return print("Humans can walk") 

class Whale(Mammals):
    def swim():
        return print("Whales can swim") 

В данном случае у нас есть абстрактный класс «Млекопитающие», который содержит два абстрактных метода: «ходить» и «плавать». Эти два элемента будут отнесены к подклассу «Человек», а просто «плавать» — к подклассу «Кит».

И действительно, если мы запустим этот код, мы можем получить:

Human.swim()
Human.walk()

Whale.swim()
Whale.walk()

# Humans can swim
# Humans can walk
# Whales can swim
# Can Walk

Подкласс кит все еще может вызывать функцию «прогулка», но этого не следует и следует избегать.

Интернет-провайдер предлагает создать множество клиентских интерфейсов, а не один интерфейс общего назначения. В результате наш пример кода становится таким:

from abc import ABC, abstractmethod

class Walker(ABC):
  @abstractmethod
  def walk() -> bool:
    return print("Can Walk") 

class Swimmer(ABC):
  @abstractmethod
  def swim() -> bool:
    return print("Can Swim") 

class Human(Walker, Swimmer):
  def walk():
    return print("Humans can walk") 
  def swim():
    return print("Humans can swim") 

class Whale(Swimmer):
  def swim():
    return print("Whales can swim") 

if __name__ == "__main__":
  Human.walk()
  Human.swim()

  Whale.swim()
  Whale.walk()

# Humans can walk
# Humans can swim
# Whales can swim

Каждый подкласс теперь наследует только то, что ему нужно, предотвращая вызов внеконтекстного (неправильного) подметода. Это может привести к ошибке, которую трудно обнаружить. Этот принцип тесно связан с другими тем, что он предписывает нам поддерживать содержимое подкласса без компонентов, которые не приносят пользы этому подклассу. Конечная цель состоит в том, чтобы поддерживать порядок в наших классах и сводить к минимуму ошибки.

Принцип инверсии зависимостей (DIP)

«Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракции. Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций».

В соответствии с принципом инверсии зависимостей (DIP) мы должны полагаться на абстракции (интерфейсы и абстрактные классы), а не на конкретные реализации (классы). Абстракции не должны зависеть от конкретики; скорее, детали должны зависеть от абстракций.

Предположим, у вас есть программное обеспечение, которое принимает определенный набор данных (файл, формат и т. д.) и сценарий для его обработки. Что произойдет, если эта информация изменится? Вам придется переработать свой сценарий, чтобы он соответствовал новому формату. Ретро-совместимость со старыми файлами больше недоступна.

Однако вы можете преодолеть это, создав третью абстракцию, которая принимает информацию в качестве входных данных и отправляет ее другим. По сути, это то, для чего используется API.

Простым примером этой концепции является API (интерфейс прикладного программирования). Этот код ниже является примером из моего проекта Foresight с использованием API.

# This is backend side for API logic

#  in views.py
@csrf_exempt
def sign_out(request):
    response = JsonResponse("Logout successful", safe=False)
    response.delete_cookie("idToken")
    response.delete_cookie("refreshToken")

    return response

# in urls.py (for API endpoint)
urlpatterns = [
    ...
    path('logout', sign_out, name='logout'),
    ...
]
// In client side (React.js) calling API endpoint

import React, { useEffect, useState } from 'react';
import Cookies from 'js-cookie';

function LogoutButton() {
  const [isLoggingOut, setIsLoggingOut] = useState(false);

  function handleLogout() {
    setIsLoggingOut(true);

    fetch('https://foresight-backend-production.up.railway.app/api/auth/logout', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${Cookies.get('idToken')}`
      }
    })

Из приведенного выше кода видно, что на внешнем интерфейсе (на стороне клиента) в React.js вызывается конечная точка API для использования API выхода, который был создан на внутренней стороне с использованием Django и Firebase. На стороне бэкенда видно, что он не зависит от внешнего интерфейса (на стороне клиента), потому что даже без использования внешнего интерфейса мы все еще можем использовать функцию выхода. С другой стороны, внешний интерфейс сильно зависит от внутреннего интерфейса, потому что без API функция выхода не может быть использована, поскольку отсутствует логика для обработки выхода.

Заключение

В заключение, овладение лучшими практиками программирования с принципами SOLID имеет решающее значение для разработки высококачественного программного обеспечения, которое является гибким, удобным в сопровождении и расширяемым. Следуя принципам SOLID, разработчики могут создавать код, который легко модифицировать и обновлять, сводя к минимуму запахи кода и недостатки дизайна, а также максимизируя возможности повторного использования. Принципы SOLID также помогают снизить риск внесения ошибок в кодовую базу, что может сэкономить время и деньги в долгосрочной перспективе. Хотя применение принципов SOLID может потребовать больше усилий и времени, преимущества того стоят. Осваивая принципы SOLID, разработчики могут поднять свои навыки программирования на новый уровень и создавать более качественное программное обеспечение, отвечающее потребностям их клиентов и пользователей.

Ссылки

  1. Каковы принципы SOLID в Java?
    https://www.educative.io/answers/what-are-the-solid-principles-in-java
  2. Правила SOLID в объектно-ориентированном программировании
    https://wearecommunity.io/communities/epam-poland/articles/1190#:~:text=your%20systems%20correctly.-, SOLID%20является%20акронимом%20%20для%20пяти%20основных%20принципов%20из%20Объекта,принципа%20и%20зависимости%20инверсии%20принципа.
  3. Принципы SOLID Java
    https://www.interviewbit.com/blog/solid-principles-java/
  4. Принципы SOLID с Python
    https://medium.com/@m.nusret.ozates/solid-principles-with-python-245e45f9b1f8
  5. SOLID Coding in Python
    https://towardsdatascience.com/solid-coding-in-python-1281392a6a94
  6. Принципы SOLID объясняются на Python с примерами
    https://gist.github.com/dmmeteo/f630fa04c7a79d3c132b9e9e5d037bfd