В своих исследованиях вы, возможно, сталкивались с терминами “атомарная функция” и “Не повторяйтесь (СУХО)”. Сегодня я собираюсь продемонстрировать, как эти концепции работают вместе, чтобы обеспечить легко поддерживаемый, легко тестируемый и красивый код.
Проблема
Часто новички пишут функции, которые просто делают слишком много. Такие функции могут принимать большое количество аргументов, выполнять несколько логических проверок и, возможно, управлять несколькими циклами. Они создают внутренние экземпляры переменных, которые влияют на дальнейшие внутренние операции.
Приведенная ниже функция Python кратко иллюстрирует эту динамику:
import math def find_prime_factors(number): limit = int(math.sqrt(number)) + 1 factors = [(num, int(number / num)) for num in range(1, limit) if number % num == 0] prime_factors = [] if len(factors) == 1: return factors for num in factors[1]: limit = int(math.sqrt(num)) + 1 factors2 = [(x, int(num / x)) for x in range(1, limit) if num % x == 0] if len(factors2) == 1: prime_factors.append(num) else: prime_factors.extend(find_prime_factors(num)) return prime_factors
Функции, разработанные таким образом, имеют проблемы с предсказуемостью, что, в свою очередь, затрудняет их тестирование и устранение неполадок. Если функция не проходит тест или дает неожиданный результат, причина просто не ясна. Конечно, один из внутренних логических тестов мог потерпеть неудачу или внутренний цикл мог вести себя неожиданно. В любом случае, устранение неполадок требует проработки каждой строки до тех пор, пока проблема не будет найдена.
Атомарность
Атомарные функции решают вышеуказанные проблемы, сводя каждую функцию к одной операции. Я рефакторировал find_prime_factors() в набор атомарных функций ниже:
import math def find_prime_factors(number): if is_prime(number): return [1, number] return _find_prime_factors(factor_number(number)[1]) def is_prime(number): return len(factor_number(number)) == 1 def factor_number(number): limit = int(math.sqrt(number)) + 1 return [(num, int(number / num)) for num in range(1, limit) if number % num == 0] def _find_prime_factors(factor_list): primes = [] for num in factor_list: if is_prime(num): primes.append(num) else: primes.extend(find_prime_factors(num)) return primes
Обратите внимание, насколько коротка каждая функция. Они варьируются от одной до семи строк каждая. Часто они возвращают выражение или другую функцию без создания экземпляра переменной, содержащей результирующее значение. Некоторые функции принимают выходные данные других функций в качестве аргументов, позволяя им быть нанизанными вместе, эффективно комбинируя их внутренние коды.
Атомарные функции приносят нам пользу несколькими способами. Во-первых, их легко проверить. Поскольку каждая функция несет единую ответственность, мы можем легко предсказать, каким будет результат каждой функции при заданном входе. Мы знаем результат функции factor_number() для любого заданного числа, не проходя через десятки строк логических тестов. Функция проста в том, что она делает. Поскольку у нас есть прямое ожидание от каждой функции, наши тесты могут быть простыми и краткими.
Во-вторых, поскольку каждая функция проста в тестировании, они также просты в устранении неполадок. Каждая функция имеет так мало строк, что ошибки не имеют места, чтобы скрыть. Если is_prime() дает нам ошибочный результат, а factor_number () – нет, то ошибка должна быть в одной строке is_prime().
В-третьих, точно так же, как атомы объединяются в молекулы, атомные функции объединяются, чтобы иметь различные эффекты. Функция factor_number() используется дважды – один раз для определения того, является ли число простым, и один раз для возврата коэффициентов этого числа. Это также означает, что мы можем легко разработать наш дизайн. Мы можем написать еще дюжину функций, которые используют эти функции различными способами. Поскольку мы можем использовать их бесконечно, мы также можем удовлетворить сухой принцип, используя эти функции вместо того, чтобы повторяться.
В-четвертых, наш конечный результат легче поддерживать. Если мы определим, что factor_number() работает неправильно, мы можем исправить функцию, и эта коррекция повлияет на всю программу. Любая данная проблема может быть прослежена до одной линии. Поскольку атомарные функции приводят к сухому коду, любая проблема, с которой мы сталкиваемся, может быть прослежена до нескольких строк кода в одной функции. Это резко сокращает время, необходимое для устранения проблемы.