Картинки на выставку (часть 2)

Способы улучшения качества кода

При выполнении домашнего задания вы скорее всего столкнулись с проблемой изменений своего же собственного кода. На этом занятии вам предстоит научиться делать код более понятным и "гибким" — удобным для внесения изменений.

Рефакторинг

Рефа́кторинг (англ. refactoring) или перепроектирование, переработка кода — это процесс изменения внутренней структуры программы, не затрагивающий её внешнего поведения и имеющий целью облегчить понимание её работы.

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

Просмотрите справочный каталог типичных малых изменений.

Малыми изменениями сложно изменить принципиальную архитектуру программы. Иногда рефакторинг запутанного проекта сделать так сложно, что проще выбросить написанную программу и написать её заново. Поэтому важно изначально хорошо спроектировать архитектуру программного продукта.

Парадигма структурного программирования. Проектирование сверху-вниз

Что же такое структурное программирование? Это − парадигма разработки программ с помощью представления их в виде иерархической структуры блоков. Идея структурного программирования появилась в 1970-х годах у учёного Эдсгера Вибе Дейкстры и была популяризована Никлаусом Виртом, создателем широко известного в школах языка Pascal.

В эту парадигму входит всего три пункта:

  1. Любая программа состоит из трёх типов конструкций: 1. последовательное исполнение; 2. ветвление; 3. цикл.
  2. Логически целостные фрагменты программы оформляются в виде подпрограмм. В тексте основной программы вставляется инструкция вызова подпрограммы. После окончания подпрограммы исполнение продолжается с инструкции, следующей за командой вызова подпрограммы.
  3. Разработка программы ведётся поэтапно методом «сверху вниз».

Первый пункт важен скорее не тем, что в нём есть, а тем, чего в нём нет: в нём нет оператора безусловного перехода goto. Именно это отличает структурное программирование от процедурного (процедурное программирование − синоним императивного).

Благодаря пункту два в языках высокого уровня появились новые синтаксические конструкции − функции и процедуры.

Пункт три − самый важный, и он является сутью парадигмы структурного программирования. Чтобы лучше понять, что представляет собой метод «сверху вниз», рассмотрим конкретный пример. Предположим, наша задача состоит в том, чтобы нарисовать на экране зайчика. Воспользуемся уже имеющейся у нас заготовкой программы с использованием pygame:

import pygame
from pygame.draw import *

pygame.init()

FPS = 30
screen = pygame.display.set_mode((400, 400))

# Здесь мы будем рисовать

pygame.display.update()
clock = pygame.time.Clock()
finished = False

while not finished:
    clock.tick(FPS)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            finished = True

pygame.quit()

В дальнейшем мы не будем воспроизводить весь этот код, а будем работать только над центральной частью. Что ж, давайте ее напишем:

draw_hare()

Вуаля! Программа готова. Да, жаль только, что у нас нет такой функции, поэтому программа не работает. Что ж, придется ее написать. Но прежде чем писать функцию, нужно продумать ее интерфейс.

Проработка интерфейсов функций

Интерфейс функции — это описание того, как функция взаимодействует с окружением: какие параметры принимает и какой результат выдает (речь идет не только о возращаемом значении, но и о действиях, которые функция совершает "вовне" — например, как в нашем случае, вывод на экран изображения). Интерфейс задает стандарт, благодаря которому мы можем данной функцией пользоваться, даже не зная о том, как именно она устроена внутри.

В идеале (впрочем, на практике это не всегда удается) интерфейс функции нужно задать один раз и в дальнейшем не менять. Изменение внутренней логики работы функции без изменения ее интерфейса будет практически незаметным, а вот изменение интерфейса, который уже используется в разных частях программы, может быть довольно болезненным.

Поэтому стоит сразу сделать интерфейс функции достаточно гибким, чтобы он обеспечивал достаточную степень универсальности, но при этом не слишком сложным.

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

draw_hare(surface, x, y, width, height, color)

Подождите, а что значат эти x и y? Это координаты центра зайца или, может быть, кончика левого уха? В каком формате нужно задавать color? Все это должно быть где-то описано...

Документация функций

Интерфейс функции — какие параметры она принимает и что делает — следует описать в документации. В Python есть удобный механизм работы с документацией — документ-строки. В действительности это всего лишь строка в кавычках (обычно эти строки берут в тройные кавычки), записанная в начале функции:

def draw_hare(surface, x, y, width, height, color):
  '''
  Рисует зайца на экране.
  surface - объект pygame.Surface
  x, y - координаты левого верхнего угла изображения
  width, height - ширина и высота изобажения
  color - цвет, заданный в формате, подходящем для pygame.Color
  '''

При создании функции ее документ-строка будет сохранена в специальное поле и будет доступна, например, при вызове функции help:

help(draw_hare)

Теперь мы можем прописать вызов функции со всеми нужными параметрами:

draw_hare(screen, 200, 200, 200, 400, (200, 200, 200))

И вот теперь мы можем приступить к написанию самой функции:

def draw_hare(surface, x, y, width, height, color):
    draw_body()
    draw_head()
    draw_ear()
    draw_ear()
    draw_leg()
    draw_leg()

Аналогично мы должны продумать и интерфейсы функций для рисования отдельных частей зайца. В данном случае представляется разумным, что они должны принимать примерно тот же набор параметров, что и функция draw_hare:

def draw_body(surface, x, y, width, height, color):
    '''
    Рисует тело зайца.
    surface - объект pygame.Surface
    x, y - координаты центра изображения
    width, height - ширина и высота изобажения
    color - цвет, заданный в формате, подходящем для pygame.Color
    '''
    pass


def draw_head(surface, x, y, size, color):
    '''
    Рисует голову зайца.
    surface - объект pygame.Surface
    x, y - координаты центра изображения
    size - диаметр головы
    color - цвет, заданный в формате, подходящем для pygame.Color
    '''
    pass


def draw_ear(surface, x, y, width, height, color):
    '''
    Рисует ухо зайца.
    surface - объект pygame.Surface
    x, y - координаты центра изображения
    width, height - ширина и высота изобажения
    color - цвет, заданный в формате, подходящем для pygame.Color
    '''
    pass


def draw_leg(surface, x, y, width, height, color):
    '''
    Рисует ногу зайца.
    surface - объект pygame.Surface
    x, y - координаты центра изображения
    width, height - ширина и высота изобажения
    color - цвет, заданный в формате, подходящем для pygame.Color
    '''
    pass

Теперь можно закончить функцию draw_hare:

def draw_hare(surface, x, y, width, height, color):
    '''
    Рисует зайца на экране.
    surface - объект pygame.Surface
    x, y - координаты центра изображения
    width, height - ширина и высота изобажения
    color - цвет, заданный в формате, подходящем для pygame.Color
    '''
    body_width = width // 2
    body_height = height // 2
    body_y = y + body_height // 2
    draw_body(surface, x, body_y, body_width, body_height, color)

    head_size = height // 4
    draw_head(surface, x, y - head_size // 2, head_size, color)

    ear_height = height // 3
    ear_y = y - height // 2 + ear_height // 2
    for ear_x in (x - head_size // 4, x + head_size // 4):
        draw_ear(surface, ear_x, ear_y, width // 8, ear_height, color)

    leg_height = height // 16
    leg_y = y + height // 2 - leg_height // 2
    for leg_x in (x - width // 4, x + width // 4):
        draw_leg(surface, leg_x, leg_y, width // 4, leg_height, color)

Функции рисования отдельных частей зайца можно пока сделать совсем простыми (сделать их более красивыми можно будет позже):

def draw_body(surface, x, y, width, height, color):
    '''
    Рисует тело зайца.
    surface - объект pygame.Surface
    x, y - координаты центра изображения
    width, height - ширина и высота изобажения
    color - цвет, заданный в формате, подходящем для pygame.Color
    '''
    ellipse(surface, color, (x - width // 2, y - height // 2, width, height))


def draw_head(surface, x, y, size, color):
    '''
    Рисует голову зайца.
    surface - объект pygame.Surface
    x, y - координаты центра изображения
    size - диаметр головы
    color - цвет, заданный в формате, подходящем для pygame.Color
    '''
    circle(surface, color, (x, y), size // 2)


def draw_ear(surface, x, y, width, height, color):
    '''
    Рисует ухо зайца.
    surface - объект pygame.Surface
    x, y - координаты центра изображения
    width, height - ширина и высота изобажения
    color - цвет, заданный в формате, подходящем для pygame.Color
    '''
    ellipse(surface, color, (x - width // 2, y - height // 2, width, height))


def draw_leg(surface, x, y, width, height, color):
    '''
    Рисует ногу зайца.
    surface - объект pygame.Surface
    x, y - координаты центра изображения
    width, height - ширина и высота изобажения
    color - цвет, заданный в формате, подходящем для pygame.Color
    '''
    ellipse(surface, color, (x - width // 2, y - height // 2, width, height))

Вот что у нас получилось:

Задание недели

Сделайте себе fork проекта, который даст вам преподаватель (это проект одного из ваших товарищей).

Ваша задача сделать рефакторинг этого кода так, чтобы можно было быстро вносить изменения (местоположения объектов, количество, их размер).