April 9, 2022

Императивный и декларативный подход

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

На самом деле очень много можно уложить в применение этих подходов.

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

А вот фраза вроде «сваргань-ка мне бутерброд, пжалста» — это уже декларативный подход. Товарищ сам вполне разберётся, как обращаться с холодильником, батоном и колбасой. Ему не пришлось выслушивать дурацкую тираду, вам не пришлось её произносить, окружающим не захотелось вас поколотить за занудство. Прекрасно.

Декларативный подход — вы декларируете то, что вам надо, не погружаетесь в детали реализации.

Если переносить это на программирование, то, скажем, задачу распарсить некий запрос на заголовок, содержащий только первую строку, и оставшееся тело запроса, можно разными способами. Можно так — даже не вынося эту логику парсинга в отдельную функцию, разместя её в составе какой-то большей функции обработки запроса:

def process_client_request(request: str):
    """какой-то длинный код тут..."""
    """teletype.in не умеет в комменты с символа диеза..."""
    delimeter_position = request.find("\n")
    header = request[:delimeter_position]
    body = request[delimeter_position:]
    """и потом тоже какой-то длинный код..."""

Грустно? Ну невесело. Много сложного кода. Какие-то индексы находятся, слайсы по ним ищутся. Там, где сложный код — больше вероятность ошибок, сложнее поддерживать код, дольше, дороже, неприятнее.

Когда мы с find ищем позицию в строке какой-то подстроки — то непонятно, зачем мы это делаем. Может, мы хотим проверить вхождение подстроки в строку. Может, мы хотим обрезать строку. Может, хотим разбить её. Может ещё что-то. Чёрт его знает, надо читать окружающий код, чтобы понять нас. Это сложно. Надо думать. Думать — тяжело.

Так лучше:

def process_client_request(request: str):
    """какой-то длинный код тут..."""
    header, body = request.split("\n", 1)
    """и потом тоже какой-то длинный код..."""

А почему так лучше?

Потому что split это понятный метод разбивки строки на подстроки, это декларативный подход, скрывающий реализацию. Это — декларативно и просто, и думать не надо, чтобы понять, что в коде происходит.

А можно ещё проще, ещё декларативнее:

def parse_request(request: str) -> tuple[str, str]:
    return request.split("\n", 1)

def process_client_request(request: str):
    """какой-то длинный код тут..."""
    header, body = parse_request(request)
    """и потом тоже какой-то длинный код..."""

Кода стало больше — но код стал лучше.

  1. Читать стало проще, приятнее, понятнее. Читая строку header, body = parse_request(request), мы понимаем, что тут происходит. Тут происходит парсинг запроса, а как именно он там этот парсинг происходит сие есть дело десятое, при желании можно перейти в вызываемую функцию и почитать.
  2. Ещё один плюс — возможность переиспользовать parse_request ещё где-то.
  3. И ещё один — если задача изменится и нам надо будет парсить запрос как-то иначе, нам не надо будет искать этот код где-то в дебрях другого кода, он лежит себе отдельно в своей отдельной маленькой великолепной функции.

Прекрасно.

А если ещё dataclass или NamedTuple вместо tuple использовать для распаршенного запроса — так вообще будет песня:)

Занимаясь написанием кода, задумывайтесь о том, какой код вы сейчас пишете — императивный или декларативный?

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