Императивный и декларативный подход
Ревьюил сейчас финальные проекты ребят по курсу и много говорил о декларативном и императивном подходе в программировании. Понимаете разницу?
На самом деле очень много можно уложить в применение этих подходов.
Мне нравится пример с бутербродом. Когда вы говорите товарищу — дружище, так-так, бросай всё, поднимайся, иди-ка на кухню, левой рукой открывай холодильник, а правой доставай колбасу, затем закрывай холодос, бери нож, отрезай кусок от колбасы, потом от батона, клади колбасу поверх батона и тащи это всё получившееся довольному мне — то вот он императивный подход. Товарищ скорее всего такого не одобрит. Да и вы сами тоже опупеете это всё в таком виде произносить.
А вот фраза вроде «сваргань-ка мне бутерброд, пжалста» — это уже декларативный подход. Товарищ сам вполне разберётся, как обращаться с холодильником, батоном и колбасой. Ему не пришлось выслушивать дурацкую тираду, вам не пришлось её произносить, окружающим не захотелось вас поколотить за занудство. Прекрасно.
Декларативный подход — вы декларируете то, что вам надо, не погружаетесь в детали реализации.
Если переносить это на программирование, то, скажем, задачу распарсить некий запрос на заголовок, содержащий только первую строку, и оставшееся тело запроса, можно разными способами. Можно так — даже не вынося эту логику парсинга в отдельную функцию, разместя её в составе какой-то большей функции обработки запроса:
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)
"""и потом тоже какой-то длинный код..."""Кода стало больше — но код стал лучше.
- Читать стало проще, приятнее, понятнее. Читая строку
header, body = parse_request(request), мы понимаем, что тут происходит. Тут происходит парсинг запроса, а как именно он там этот парсинг происходит сие есть дело десятое, при желании можно перейти в вызываемую функцию и почитать. - Ещё один плюс — возможность переиспользовать
parse_requestещё где-то. - И ещё один — если задача изменится и нам надо будет парсить запрос как-то иначе, нам не надо будет искать этот код где-то в дебрях другого кода, он лежит себе отдельно в своей отдельной маленькой великолепной функции.
А если ещё dataclass или NamedTuple вместо tuple использовать для распаршенного запроса — так вообще будет песня:)
Занимаясь написанием кода, задумывайтесь о том, какой код вы сейчас пишете — императивный или декларативный?
Как сделать этот код более декларативным? Как упростить его?