Ако имате нужда от повече от 3 нива на вдлъбнатини, прецакате!

„Ако имате нужда от повече от 3 нива на отстъп, така или иначе сте прецакани и трябва да поправите програмата си.“ Можете да намерите този ред в официалния стил на кодиране на ядрото на Linux от Линус Торвалдс.

Работата с дълбоко вложен код е като да се опитвате да разгадаете безкраен набор от „матрьошки“ — това е безкрайна задача. Като софтуерни инженери, наша отговорност е да напишем код, който е лесен за четене от нашите съотборници. Но понякога се натъкваме на код, който е толкова дълбоко вложен, че се чувства като чудовище, което трябва да бъде победено. Тогава е време да запретнем ръкави и да убием демона на дълбокото вмъкване чрез преработване на кода. Повярвайте ми, вашите връстници ще ви благодарят, че сте улеснили живота им.

Как? Ще обсъдя две техники за рефакторинг на дълбоко вложен код;

  1. Извличане
  2. Инверсия

Екстракция

Дълбоко вложеният код често може да е знак, че блок от код нарушава принципа на Единствена отговорностот SOLID— той прави твърде много неща и е трудно да се разбере какво се случва . В тези случаи може да бъде полезно да извлечете кодови блокове и да ги превърнете в собствени функции. Това не само ще направи вашия код по-организиран и четим, но също така ще улесни повторното му използване и поддръжка в дългосрочен план.

Нека да видим пример за извличане:

#FIXME: extract the process of finding sum
def compute(x: int, y: int) -> int:
    if x > y:
        sum = 0
        for num in range(y, x + 1):
            sum += num
        return sum
    else:
        return 0
def process(x: int, y: int) -> int:
    sum = 0
    for num in range(y, x + 1):
        sum += num
    return sum


def compute(x: int, y: int) -> int:
    if x > y:
        return process(x, y) #extracted function
    else:
        return 0

Инверсия

Когато кодираме, е лесно да се увлечем в манталитета на „щастливия път“ – ние се фокусираме върху успешния резултат и структурираме логиката си съответно. Но това може да доведе до дълбоко вложени кодови блокове, които са трудни за четене и разбиране. Това е мястото, където се намесва моделът на клаузата за охрана (известен още като моделът на изхвърлящия).

Като преобръщаме прогресията на нашата логика и търсим случаи, в които можем да се върнем по-рано, можем да направим нашия код по-четлив и поддържаем. С други думи, ние отхвърляме всички случаи, които не отговарят на нашите критерии за успех, и поддържаме партията (известна още като нашия код) да се движи гладко.

Нека да видим пример за инвертиране на код:

#FIXME: invert and add a guard clause
def compute(x: int, y: int) -> int:
    if x > y:
        sum = 0
        for num in range(y, x + 1):
            sum += num
        return sum
    else:
        return 0
def compute(x: int, y: int) -> int:
    if x <= y: #guard clause
        return 0

    sum = 0
    for num in range(y, x + 1):
        sum += num
    return sum

Сега нека видим какво ще се случи, ако направим и извличането, и инверсията,

def process(x: int, y: int) -> int:
    sum = 0
    for num in range(y, x + 1):
        sum += num
    return sum


def compute(x: int, y: int) -> int:
    if x <= y:
        return 0

    return process(x, y)

Не изглежда ли по-добре? Процесът на рефакторинг подобри четимостта на кода и намали цикломатичната сложност.

Друг пример

Нека да видим друг пример за дълбоко вложен код и да го преработим с помощта на гореспоменатите процедури. Избирам UVa-10070 за илюстриране на примера, който е доста лесен проблем от UVa Online Judge.

проблем

10070 Високосна или невисокосна година и...

Древната раса на Гуламату е много напреднала в своята схема за изчисляване на годината. Те разбират какво е високосна година (година, която се дели на 4 и не се дели на 100, с изключение на това, че годините, които се делят на 400, също са високосна година.) и имат подобни фестивални години. Единият е фестивалът Хулукулу (случва се на години, делими се на 15) и фестивалът Булукулу (случва се на години, делими на 55, при условие че е и високосна година). За дадена година ще трябва да посочите какви свойства имат тези години. Ако годината не е високосна, нито фестивална, отпечатайте реда „Това е обикновена година“. Редът на отпечатване (ако има) на свойствата е: високосна година → huluculu → bulukulu.

Примерно въвеждане

2000
3600
4515
2001

Примерен резултат

This is leap year.

This is leap year.
This is huluculu festival year.

This is huluculu festival year.

This is an ordinary year.

Решение #1 [дълбоко вложен код]

import sys

if __name__ == "__main__":

    first_input = True

    for line in sys.stdin:
        if not first_input: #FIXME
            print()
        else:
            first_input = False

        input_year = int(line)

        is_leap_year = False
        is_huluculu_year = False
        is_bulukulu_year = False

        if input_year % 4 == 0: #FIXME
            if input_year % 100 == 0:
                if input_year % 400 == 0:
                    is_leap_year = True

                    if input_year % 55 == 0: #FIXME
                        is_bulukulu_year = True
            else:
                is_leap_year = True

                if input_year % 55 == 0: #FIXME
                    is_bulukulu_year = True

        if input_year % 15 == 0: #FIXME
            is_huluculu_year = True

        if is_leap_year:
            print("This is leap year.")
        if is_huluculu_year:
            print("This is huluculu festival year.")
        if is_bulukulu_year:
            print("This is bulukulu festival year.")

        if not is_leap_year and not is_huluculu_year and not is_bulukulu_year:
            print("This is an ordinary year.")

Горният код е написан опасно дълбоко вложен умишлено.

Решение #2 [Рефакторинг]

import sys

FLAG_FIRST_INPUT = True


def print_blank_line_if_not_first_input():
    global FLAG_FIRST_INPUT

    if FLAG_FIRST_INPUT:
        FLAG_FIRST_INPUT = False
        return

    print()


def check_leap_year(year: int) -> bool:
    if year % 400 == 0:
        return True
    if year % 100 == 0:
        return False
    if year % 4 == 0:
        return True

    return False


def check_huluculu_year(year: int) -> bool:
    if year % 15 == 0:
        return True

    return False


def check_bulukulu_year(is_leap_year: bool, year: int) -> bool:
    if is_leap_year and year % 55 == 0:
        return True

    return False


def process_input(year: int):
    is_leap_year = check_leap_year(year)
    is_huluculu_year = check_huluculu_year(year)
    is_bulukulu_year = check_bulukulu_year(is_leap_year, year)

    if is_leap_year:
        print("This is leap year.")
    if is_huluculu_year:
        print("This is huluculu festival year.")
    if is_bulukulu_year:
        print("This is bulukulu festival year.")

    if not is_leap_year and not is_huluculu_year and not is_bulukulu_year:
        print("This is an ordinary year.")


if __name__ == "__main__":

    for line in sys.stdin:
        print_blank_line_if_not_first_input()
        process_input(int(line))

Преработих решение №1 чрез извличане на код и инверсия на кода. Ако смятате, че решение №2може да бъде написано по по-четлив начин, моля, не се колебайте да коментирате.

Want to Connect?
If you have any feedback, please ping me on my 

LinkedIn: https://linkedin.com/in/shuhanmirza/