Как аналитику научиться читать код без навыков программирования

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

А зачем аналитикам читать код?

Я верю в то, что если ты работаешь в IT в технической команде, то ты — инженер.
А если уж ты инженер, то ты должен уметь не только в бизнес и анализ, но и в техническую сторону вопроса.

Проще говоря, умение читать код (а равно понимать то, как там все устроено «под капотом», какие есть паттерны проектирования и т.д.) сильно поможет при:

  • проектировании архитектуры;

  • разборе инцидентов с продакшена;

  • погружении в новый домен или рефакторинге легаси-систем.

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

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

Способ 1

Не благодарите

Не благодарите

Отучитесь на курсах программирования. Это будет не быстро, но вас будет мотивировать потраченная на это куча денег. А может и не будет.

Субъективно, проще всего начинать с того, что на хайпе. Интересно будет вернуться сюда и прочитать это лет так через 15, но сегодня я бы безальтернативно выбрал Python в качестве первого языка. Для быстрого освоения базовых принципов — самое то.

Как альтернатива платным курсам существует масса бесплатных материалов в интернете, которые ничем не уступают платным по качеству.

Второе субъективное ощущение — проще сразу учиться на практических кейсах. Как раз то, чего полно в интернете и бесплатно. Решение вороха задач на сортировку массива двадцатью пятью методами это замечательно, но не мотивирует ввиду отсутствия наглядного результата. А «базу» вы потом всегда успеете подтянуть, главное, чтобы в вас разгорелся интерес и желание развиваться «вглубь» технического материала (именно так и случилось у меня в свое время, я вообще первым делом полез изучать ML на Python, не разбираясь даже в синтаксисе языка, благо, что курс вышмата еще помнил на тот момент).

Способ 2

LLM. Копируете фрагмент кода, закидываете в нейронку с вопросом «Расскажи подробно, что выполняет этот фрагмент кода на <…подставьте сюда язык программирования…>», получаете расписанный по строчкам ответ.

Способ 3

Ну, а теперь я собственно изложу свое видение того, как можно быстро ознакомиться с базой любого языка.

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

Основы синтаксиса на примере Python

  1. Основы синтаксиса

    • Комментарии:  Как комментировать код?

      # Это однострочный комментарий
      """ Это многострочный комментарий """
    • Переменные:  Как объявлять переменные и присваивать им значения?

      x = 10  # целое число
      y = 3.14  # вещественное число
      name = "Python"  # строка
      is_true = True  # булево значение
    • Типы данных:  Основные типы данных (целые числа, вещественные числа, строки, списки, словари, множества и т.д.)

      my_list = [1, 2, 3, 4, 5]  # список
      my_dict = {'key1': 'value1',
                 'key2': 'value2'}  # словарь
      my_set = {1, 2, 3}  # множество
    • Операторы:  Арифметические операторы, операторы сравнения, логические операторы.

      a = 5 + 3  # сложение
      b = 8 - 2  # вычитание
      c = 2 * 3  # умножение
      d = 9 // 2  # целочисленное деление
      e = 9 % 2  # остаток от деления
      
      f = a > b  # сравнение (больше)
      g = a < b  # меньше
      h = a >= b  # больше или равно
      i = a <= b  # меньше или равно
      j = a == b  # равенство
      k = a != b  # неравенство
      
      l = (a > b) and (c < d)  # логическое И
      m = (a > b) or (c < d)  # логическое ИЛИ
      n = not (a > b)  # отрицание
  2. Управляющие конструкции

    • Условия:  Условные операторы if,  elif,  else.

      age = 18
      if age < 18:
          print("Вы несовершеннолетний.")
      elif age == 18:
          print("Вам ровно 18 лет.")
      else:
          print("Вы совершеннолетний.")
    • Циклы:  Цикл for и цикл while.

      # Цикл for
      fruits = ["яблоко", "апельсин", "банан"]
      for fruit in fruits:
          print(fruit) # яблоко, апельсин, банан
      
      # Цикл while
      count = 0
      while count < 5:
          print(count) # 0, 1, 2, 3, 4
          count += 1
  3. Функции

    • Определение функций, передача параметров, возвращение значений.

      def greet(name):
          print(f"Привет, {name}!")
      
      greet("Мир")
      
      def add(a, b):
          return a + b
      
      result = add(2, 3)
      print(result)  # Выведет 5
  4. Модули и пакеты

    • Импорт модулей и пакетов, использование стандартных библиотек.

      import math
      
      print(math.pi)  # Выведет значение числа π
      
      from datetime import datetime
      
      now = datetime.now()
      print(now)  # Выведет текущую дату и время
  5. Работа с файлами

    • Открытие, чтение, запись и закрытие файлов.

      with open('example.txt', 'w') as file:
          file.write("Привет, мир!\n")
      
      with open('example.txt', 'r') as file:
          content = file.read()
          print(content)
  6. Исключения

    • Обработка исключительных ситуаций с помощью блоков try,  except,  finally.

      try:
          x = int(input("Введите число: "))
          y = 1 / x
      except ZeroDivisionError:
          print("Ошибка: Деление на ноль.")
      except ValueError:
          print("Ошибка: Недопустимое значение.")
      finally:
          print("Завершение программы.")

Объектно-ориентированное программирование (ООП) — методология или стиль программирования, при котором код организуется в логически связанные объекты.

Суть объектно-ориентированного программирования состоит в том, что все программы состоят из объектов. Каждый объект — является определённой сущностью со своими атрибутами и набором методов.

Разработка идет «от объекта» — представим, что необходимо спроектировать каталог продуктов. Сначала необходимо будет описать объекты каталога — продукты, их атрибуты (название, свойства и т.д.), а затем методы объектов — то, что можно делать с продуктами (создавать, изменять, каким-либо иным образом взаимодействовать с ними).

Понятия классов и методов (ООП)

Классы и методы являются основными элементами объектно-ориентированного программирования (ООП). Классы позволяют определять новые типы данных, объединяя данные и функции, работающие с ними, в единое целое. Методы — это функции, связанные с конкретными классами и работающими с их данными.

  1. Определение объекта Объект — это сущность с конкретными характеристиками и функциями

  2. Определение класса Класс — это шаблон для создания объектов. В Python класс определяется с помощью ключевого слова class.

    class Dog:
        pass

    Здесь определен класс Dog, который пока ничего не делает. Ключевое слово pass используется, чтобы указать, что тело класса пустое.

  3. Атрибуты класса Атрибуты класса — это переменные, которые принадлежат классу и доступны всем объектам этого класса. При создании объекта класса, они заполняются конкретными значениями

    class Dog:
        species = "Canis familiaris"  # атрибут класса

    Теперь у класса Dog есть атрибут species, который доступен всем объектам этого класса.

  4. Методы класса Методы — это функции, определенные внутри класса. Они работают с данными объекта или класса.

    class Dog:
        def bark(self):  # метод класса
            print("Гав!")

    Метод bark — это функция, определенная внутри класса Dog. Обратите внимание на аргумент self, который ссылается на текущий объект.

  5. Конструктор Конструктор — специальный метод, вызываемый при создании объекта класса. В Python конструктор называется __init__.

    class Dog:
        def __init__(self, name, age):  # конструктор
            self.name = name  # атрибут объекта
            self.age = age  # атрибут объекта

    Теперь при создании объекта класса Dog необходимо передать ему имя и возраст.

  6. Создание объектов Чтобы использовать класс, нужно создать объект (или экземпляр) этого класса.

    scooby = Dog("Скуби", 3)  # создание объекта
    rex = Dog("Рекс", 5)  # еще один объект

    Оба объекта имеют свои собственные значения атрибутов name и age.

  7. Доступ к атрибутам и методам Доступ к атрибутам и методам объекта осуществляется через точку.

    print(scooby.name)  # выведет "Скуби"
    rex.bark()  # выведет "Гав!"
  8. Наследование Наследование позволяет одному классу унаследовать свойства другого класса.

    class Poodle(Dog):  # Poodle наследует от Dog
        def speak_french(self):
            print("Oui Oui!")

    Класс Poodle теперь обладает всеми методами и атрибутами класса Dog, а также своим собственным методом speak_french.

  9. Полиморфизм Полиморфизм позволяет объектам разных классов реагировать на одни и те же сообщения по-разному.

    class Cat:
        def meow(self):
            print("Мяу!")

    Теперь у нас есть два класса,  Dog и Cat, оба с методами, которые делают разные вещи, хотя называются одинаково.

Пример реализации парадигмы ООП:

class Animal:
    def __init__(self, name, sound):
        self._name = name  # Инкапсуляция: защищённый атрибут
        self._sound = sound

    def make_sound(self):
        print(f"{self._name} говорит {self._sound}")

    def sleep(self):
        print(f"{self._name} спит...")

class Dog(Animal):
    def __init__(self, name):
        super().__init__(name, "Гав!")  # Наследование

    def wag_tail(self):
        print(f"{self._name} виляет хвостом...")

class Cat(Animal):
    def __init__(self, name):
        super().__init__(name, "Мяу!")  # Наследование

    def purr(self):
        print(f"{self._name} мурлычет...")

def animal_action(animal):
    animal.make_sound()  # Полиморфизм
    animal.sleep()

doggy = Dog("Скуби-Ду")
catze = Cat("Гарфилд")

animal_action(doggy)  # Скуби-Ду говорит Гав!, Скуби-Ду спит...
animal_action(catze)  # Гарфилд говорит Мяу!, Гарфилд спит...

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

Рассмотрим архитектуру простого приложения из двух сервисов из базы данных.

Простой пример

Простой пример

Видим, что у нас есть внешний сервис, с которого можно отправить запрос GET /api/products в сервис Main для получения списка товаров из таблицы Product базы данных main.

В свою очередь сервис Main при поступлении такого запроса ищет необходимые товары в своей БД, затем направляет запрос POST /api/products в сервис Admin, для, например, дальнейшей обработки записи о том, список каких товаров запрашивал пользователь, и возвращает ответ.

Про REST API, интеграции и базы данных вы можете подробнее прочитать в других статьях.

Точкой входа в исполнение программы, как правило, является файл main.py, если иное не переопределено.

Открываем main.py и видим много непонятных цветных букв.

#--------------------------------------------------------------
# Импортируются основные библиотеки для работы с Flask (Flask),
# управление кросс-доменными запросами (CORS),
# работа с миграциями баз данных (Migrate)
# и объект db (экземпляр SQLAlchemy).
from flask import Flask
from flask_cors import CORS
from flask_migrate import Migrate
from database import db
#--------------------------------------------------------------
# Строка подключения указывает на базу данных PostgreSQL,
# которая находится по адресу host.docker.internal:5434, имя пользователя – root,
# пароль – root, а база данных называется main.
db_uri = "postgresql+psycopg2://root:root@host.docker.internal:5434/main"
#--------------------------------------------------------------
# Создается объект Migrate,
# который будет использоваться для управления миграцией схемы базы данных.
migrate = Migrate()
#--------------------------------------------------------------
# Функция create_app():  и настраивает его:
def create_app():
    # Cоздает экземпляр приложения Flask
    app = Flask(__name__)
    # Устанавливает конфигурацию соединения с базой данных (SQLALCHEMY_DATABASE_URI).
    app.config["SQLALCHEMY_DATABASE_URI"] = db_uri
    # Включает поддержку кросс-доменных запросов через CORS(app).
    CORS(app)
    # Инициализирует объекты db и migrate для использования в приложении.
    db.init_app(app)
    migrate.init_app(app, db)
    # Импортирует и регистрирует эндпоинты из кастомного модуля routes.
    from routes import register_routes
    register_routes(app, db)
    # Возвращает результатом выполнения функции экземпляр приложения
    return app
#--------------------------------------------------------------
# Создается экземпляр приложения 
app = create_app()
#--------------------------------------------------------------
# Запускается приложение (app) на сервере с включенным режимом отладки (debug=True),
# который слушает все интерфейсы с адресом (host="0.0.0.0") на порту 5000.
if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=5000)

Вспоминаем, что нас интересует работа сервиса, а значит — все возможные взаимодействия (которые в данном кейсе идут по HTTP), следовательно ищем паттерны, указывающие на что-нибудь, похожее на URL-адрес.

С помощью поиска Ctrl+Shift+F открываем окно поиска и ищем паттерны, соответствующие именам эндпоинтов или (для нашего кейса) делаем Ctrl+LeftClick по функции register_routes(app, db ) и видим следующее содержимое:

#--------------------------------------------------------------
# Импортируется функция jsonify из Flask для преобразования данных в JSON-формат,
# библиотека requests для выполнения HTTP-запросов
# и модель Product из модуля models.
from flask import jsonify
import requests
from models import Product
#--------------------------------------------------------------
# Эта функция принимает два аргумента: app (экземпляр приложения Flask)
# и db (объект базы данных).
# Функция предназначена для регистрации маршрутов в приложении.
def register_routes(app, db):
#--------------------------------------------------------------
    # Декоратор @app.route определяет новый маршрут для URL /api/products,
    # который обрабатывает только запросы методом POST.
    @app.route("/api/products", methods=['GET'])
    #--------------------------------------------------------------
    def get_products(request):
        # Сначала выполняется запрос к базе данных для получения всех записей
        # из таблицы Product. Метод query.all() возвращает список объектов Product.
        products = Product.query.all()
        #--------------------------------------------------------------
        try:
          # Затем отправляется POST-запрос в сервис Admin
          # по адресу http://host.docker.internal:8000/api/products.
          # В теле запроса передается информация о найденных товарах в формате JSON.
          response = requests.post('http://host.docker.internal:8000/api/products', json={
              'products_from_database': products
          })
          #--------------------------------------------------------------
          # После отправки запроса проверяется статус ответа.
          # Если запрос завершился неудачно (т.е., если response.ok равно False),
          # то выводится сообщение об ошибке вместе с текстом ответа.
          if not response.ok:
              print(f'Ошибка отправки запроса в сервис Admin: {response.text}')
          #--------------------------------------------------------------
        # Если во время отправки запроса возникает исключение,
        # оно перехватывается блоком except,
        # и выводится соответствующее сообщение об ошибке.
        except Exception as e:
            print(f'Ошибка отправки запроса в сервис Admin: {e}')
        #--------------------------------------------------------------
        # Независимо от успеха или неудачи отправки запроса в сервис Admin,
        # функция возвращает список товаров в формате JSON.
        return jsonify(products)

Первое, что встречаем, это запись @app.route("/api/products", methods=['GET']) — это и есть REST-эндпоинт, на который придет запрос с методом GET.

Под записью находится функция get_products(), принимающая объект запроса (request), и определяемая с помощью ключевого слова def.
В нашем случае тело функции содержит выполнение некоей бизнес-логики: поиск продуктов в базе данных в таблице Product, отправку POST-запроса о результатах поиска в сервис Admin на URL-адрес 'http://host.docker.internal:8000/api/products'и возврат ответа сервису Main.

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

Видим, что в ответе возвращаем переданную внутрь метода jsonify()переменную products, которая содержит в себе результат выполнения последовательности методов у объекта Product. Думаю, что не надо обладать сверхъестественными когнитивными способностями, чтобы понять, что сама по себе запись Product.query.all() намекает, что из таблицыProduct мы что-то запрашиваем (query), в данном случае — все (all()).

Теперь мы знаем, что первым делом обратились к таблице Product и запросили из нее все записи.

Делаем Ctrl+LeftClick на Product, попадаем сюда.

from dataclasses import dataclass
from database import db
# Объявляется класс Product, который является моделью для таблицы в базе данных,
# используя SQLAlchemy и Data Classes
@dataclass
#--------------------------------------------------------------
# Класс Product наследуется от db.Model, что делает его моделью SQLAlchemy.
# Это позволяет SQLAlchemy управлять таблицами и колонками в базе данных.
class Product(db.Model):
#--------------------------------------------------------------
    # Аннотации типов для полей класса. Они определяют типы данных для атрибутов.
    # Однако эти строки сами по себе не создают реальные атрибуты класса;
    # они просто указывают тип данных, ожидаемый при создании экземпляра класса.
    id: int
    title: str
    description: str
    #--------------------------------------------------------------
    # Столбцы базы данных
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200))
    description = db.Column(db.String(200))

Видим запись class Product(db.Model):— таким образом мы создали класс «Продукт» с атрибутами, перечисленными ниже (id, title, image). Запись типа id: int или title: str
называется аннотация типов (указывает на тип данных атрибута — числовой, строковый и т.д.).

С помощью декоратора@dataclass автоматизируется создание классов, используемых для хранения данных (проще говоря — можно меньше писать кода).
А с помощью передачи класса db.Model в параметры класса Product (реализуется наследование с точки зрения ООП), мы автоматизируем создание таблицы с продуктами в базе данных.

Затем определяются столбцы этой таблицы.
Запись id = db.Column(db.Integer, primary_key=True) — создается столбец id, типа Integer, значения которого являются первичным ключом в таблице.
Запись title = db.Column(db.String(200)) — создается столбец title, типа String, с ограничением длины содержимого в 200 символов. Столбец description создается аналогично.

Таким образом, мы понимаем, что у нас в БД есть таблица Product с тремя полями: id, title, description.

Возвращаясь к полученным из БД данным, мы видим, что вслед за этим происходит вызов requests.post('http://host.docker.internal:8000/api/products', json={ 'products_from_database': products }) - это и есть REST-клиент, который отправит запрос с методом POST в сервис Admin, который уже каким-то образом эти данные использует в своей бизнес-логике.

Ну и вспоминаем, что в конце у нас стоит ключевое слово return, возвращающее приложению, изначально вызвавшему сервис Main, ответ, так как у нас реализовано синхронное взаимодействие (запрос-ответ).

В зависимости от языка программирования и фреймворка, а также опыта разработчика приведенные листинги кода могут очень сильно отличаться, так же, как и структура и именование каталогов и файлов внутри репозитория с кодом.

Общий же подход к изучению внутреннего устройства систем и сервисов может выглядеть так:

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

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

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

  • Изучить логи, чтобы увидеть, какие события происходят в сервисе и какие ошибки могут возникать.

  • Изучить контроллеры, чтобы понять, какие методы доступны и какие данные они принимают и возвращают. Именно тут мы ищем паттерны, указывающие на что-нибудь, похожее на URL-адрес.

  • Изучить клиенты (адаптеры), чтобы понять, в какие внешние сервисы отправляются запросы, методы этих запросов, а также какие данные они отправляют и принимают в ответ.

  • Изучить репозитории, чтобы понять, в какие таблицы БД (при наличии) есть обращения, структуру этих таблиц и логику запросов в них.

  • Изучить бизнес-логику, чтобы понять, как обрабатываются данные.

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

Эту и другие статьи по системному анализу и IT-архитектуре, вы сможете найти в моем небольшом Telegram-канале:  Записки системного аналитика

© Habrahabr.ru