Андрей Власовских

Блог Андрея Власовских

Выделение изменений в коде

4 комментария

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

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

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

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

Если кратко, способ заключается в применении элементов функционального программирования (FP) в императивных языках для выбрасывания части размазанного по процедурам изменяемого состояния.

Для пояснения способа возьмём пример. В этом примере для улучшения читаемости используем 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 стал немного чище. Здесь слово чище использовано в двух значениях:

  • Стало меньше изменяемого состояния и присваиваний (чище в смысле FP)
  • Код стал более читаемым, вторичные аспекты меньше заслоняют первичные
Реклама

Written by vlan

2008-01-14 в 10:10

Опубликовано в Uncategorized

Tagged with , , ,

комментария 4

Subscribe to comments with RSS.

  1. Хороший пример. Вот бы ещё вложенные процедуры сворачивались автоматически.

    incubos

    2008-01-14 at 12:08

  2. @incubos: Что ты подразумеваешь под автоматическим сворачиванием вложенных процедур?

    vlasovskikh

    2008-01-14 at 12:16

  3. По сути, выполнен рефакторинг с использованием функциональных возможностей Python. Реализация affectedBy — как раз пример использования функциональных возможностей. В качестве альтернативы можно было бы использовать паттерн Iterator.

    zakharov

    2008-01-15 at 12:43

  4. @zakharov: Я не совсем уверен, что правильно тебя понял. Убедимся, что мы одинаково понимаем отличия шаблона Iterator от интерфейса Iterator. Если шаблон — это хорошо известная проблема в определённом контексте с хорошо известным решением и последствиями, то интерфейс — это абстракция поведения объекта. На мой взгляд, итераторы — как раз такая проблема, которая может быть «схвачена» языком программирования без необходимости решать её как-то особенно.

    Что ты подразумеваешь под использованием шаблона Iterator в этом контексте? В некотором смысле, он использован в примере. Для прохода списка handlers оператором for используется объект-итератор, получаемый через неявный метод __iter__ этого списка. Другое дело, что создание итерируемой структуры данных выполняется функционально.

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

    vlasovskikh

    2008-01-16 at 09:50


Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: