Часто код бывает не очень читаемым. Особенно неприятно, когда простой по своей сути код выглядит как огромное количество проверок, специальных случаев и т. д. Эффект, конечно, начинает действовать, когда такого кода не одна процедура, а целые классы и модули. Тогда оказывается, что, например, вместо 500 строк кода модуль занимает 2000. Из этих 2000 строк только небольшое количество присутствовало в первых версиях модуля. Вначале код лишь кратко описывал суть алгоритмов. Потом же в него добавлялись новые возможности, параметры, исправления багов и т. д. Это, наверное, одна из главных причин обилия специальных случаев в коде.
А причина обилия таких случаев и проверок в алгоритме состоит в сложности задачи. Она выражается в большом пространстве входных данных, граничных условиях и т. д. Но если проверки в алгоритме присущи самой задаче (её формулировке), то проверки в коде присущи реализации задачи. Поэтому-то их нет в первых версиях кода, а затем они появляются, чтобы более полно и точно реализовать исходное представление об алгоритме.
Однако такое неполное начальное представление об алгоритме часто схватывает его суть, а последующие дополнения только явно выражают то, что подразумевалось или умалчивалось. Когда читаешь код, часто именно такого понимания хочется достичь: абстрагироваться от деталей всех веток алгоритма и посмтортеть на суть кода.
Есть много способов достичь этого. Некоторые простые и повседневные, а некоторые неочевидные и пока неиспользуемые. Недавно мы с приятелем жаловались друг другу на переполненность почти любого кода проблемами, описанными выше. Мы пришли к нескольким возможным способам побороться с ними, один из наиболее очевидных я хотел бы описать сейчас.
Если кратко, способ заключается в применении элементов функционального программирования (ФП) в императивных языках для выбрасывания части размазанного по процедурам изменяемого состояния.
Для пояснения способа возьмём пример. В этом примере для улучшения читаемости используем 3 средства:
- Замена императивных циклов и проверок на списковые операции и определения списков (list comprehensions или аналогичные конструкции)
- Выделение второстепенного кода во вложенные процедуры
- Явное выделение пред- и пост-условий
Примером послужит такой императивный метод Logger.callHandlers
из модуля
logging из стандартной библиотеки языка Python:
def callHandlers(self, record):
c = self
found = 0
while c:
for hdlr in c.handlers:
found = found + 1
if record.levelno >= hdlr.level:
hdlr.handle(record)
if not c.propagate:
c = None #break out
else:
c = c.parent
if (found == 0) and raiseExceptions and not self.manager.emittedNoHandlerWarning:
sys.stderr.write("No handlers could be found for logger"
" \"%s\"\n" % self.name)
self.manager.emittedNoHandlerWarning = 1
Вначале он, наверное, выглядел примерно так:
def callHandlers(self, record):
c = self
while c:
for hdlr in c.handlers:
if record.levelno >= hdlr.level:
hdlr.handle(record)
c = c.parent
Довольно прозрачно, не правда ли? Но потом он оброс различными проверками, условиями и т. д. Посмотрим на него и попробуем сделать метод более читаемым при помощи некоторых приёмов из области функционального программирования.
Императивные конструкции здесь используются как для вызова Handler.handle
,
наверняка обладающих побочными эффектами, так и для итерации по предкам логгера,
что не особо обосновано. Например, при таком подходе приходится помнить, что
нужно присвоить c
новый логгер, и сделать это нужно именно в конце цикла.
Тот же код, в котором предки получаются функционально:
def callHandlers(self, record):
def affectedBy(log):
if log is None:
return []
else:
return [log] + affectedBy(log.parent)
for log in affectedBy(self):
for hdlr in log.handlers:
if record.levelno >= hdlr.level:
hdlr.handle(record)
Кода стало немного больше, а плюсов, вроде бы, добавилось немного. Это не совсем
так, ведь теперь мы можем определять затронутые обработкой логгеры, не меняя
основной код метода Logger.callHandlers
. Добавим ту часть первоначального
кода, которая блокировала распространение обработки сообщения вверх по иерархии
логгеров. Для этого нужно изменить только функцию получения затрагиваемых
логгеров:
def affectedBy(log):
if log is None:
return []
elif not log.propagate:
return [log]
else:
return [log] + affectedBy(log.parent)
Теперь мы хотим реализовать выдачу предупреждения в случае, если не было найдено
ни одного обработчика. В начальном коде это делалось заведением флага found
,
состояние которого хранило эту информацию. И для этого случая функциональный
подход сделает код более читаемым и менее подверженным ошибкам. Сравните с
первоначальным следующий вариант:
def callHandlers(self, record):
def affectedBy(log): ...
handlers = sum(log.handlers for log in affectedBy(self), [])
found = len(handlers) > 0
for hdlr in handlers:
if record.levelno >= hdlr.level:
hdlr.handle(record)
if not found and raiseExceptions and not self.manager.emittedNoHandlerWarning:
sys.stderr.write("No handlers could be found for logger"
" \"%s\"\n" % self.name)
self.manager.emittedNoHandlerWarning = 1
Ну и наконец, хвост метода про выдачу предупреждения явно мешает чтению и
отвлекает от основной сути. Обернём его в процедуру, вызвав его как своего рода
пост-условие для метода Logger.callHandlers
. Полный улучшенный аналог
первоначального кода выглядит так:
def callHandlers(self, record):
def affectedBy(log):
if log is None:
return []
elif not log.propagate:
return [log]
else:
return [log] + affectedBy(log.parent)
def checkEmittedNoHandlerWarning(found):
if not found and raiseExceptions and not self.manager.emittedNoHandlerWarning:
sys.stderr.write("No handlers could be found for logger"
" \"%s\"\n" % self.name)
self.manager.emittedNoHandlerWarning = 1
handlers = sum(log.handlers for log in affectedBy(self), [])
for hdlr in handlers:
if record.levelno >= hdlr.level:
hdlr.handle(record)
checkEmittedNoHandlerWarning(len(handlers) > 0)
Если скрыть тела вложенных в блок метода функций, он выглядит так:
def callHandlers(self, record):
def affectedBy(log): ...
def checkEmittedNoHandlerWarning(found): ...
handlers = sum(log.handlers for log in affectedBy(self), [])
for hdlr in handlers:
if record.levelno >= hdlr.level:
hdlr.handle(record)
checkEmittedNoHandlerWarning(len(handlers) > 0)
Заметим, что метод по-прежнему императивный, т. к. его назначение — вызвать
другие императивные методы Handler.handle
. И к тому же проверка выдачи
предупреждения использует состояние самого объекта Logger
для хранения флага.
Но с помощью нескольких приёмов FP код метода Logger.callHandlers
стал немного
чище. Здесь слово «чище» использовано в двух значениях:
- Стало меньше изменяемого состояния и присваиваний (чище в смысле ФП)
- Код стал более читаемым, вторичные аспекты меньше заслоняют первичные