Принцип модульности ООП в Python

Предыдущая статья — Введение в объектно-ориентированное программирование: много клякс.

Добро пожаловать в следующую часть нашей серии статей про объектно-ориентированное программирование. В этой статье мы обсудим модульность написанного нами класса.

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

Пока мы не представили ничего, что с течением времени сделало бы наш код слишком сложным для поддержки или масштабирования, по крайней мере, в рамках того, что мы можем делать с библиотекой PyGame. А как насчет того, чтобы сделать его модульным? Для этого есть действительно простой тест, давайте попробуем его импортировать!

Для этого мы разобьем наш код на два файла. Давайте скопируем класс Blob и импорт библиотеки random в новый файл blob.py:

import random


class Blob:

    def __init__(self, color):
        self.x = random.randrange(0, WIDTH)
        self.y = random.randrange(0, HEIGHT)
        self.size = random.randrange(4,8)
        self.color = color

    def move(self):
        self.move_x = random.randrange(-1,2)
        self.move_y = random.randrange(-1,2)
        self.x += self.move_x
        self.y += self.move_y

        if self.x < 0: self.x = 0
        elif self.x > WIDTH: self.x = WIDTH
        
        if self.y < 0: self.y = 0
        elif self.y > HEIGHT: self.y = HEIGHT 

В первоначальном файле удалим класс Blob, а затем импортируем его из модуля blob.py:

import pygame
import random
from blob import Blob

STARTING_BLUE_BLOBS = 10
STARTING_RED_BLOBS = 3

WIDTH = 800
HEIGHT = 600
WHITE = (255, 255, 255)
BLUE = (0, 0, 255)
RED = (255, 0, 0)

game_display = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Blob World")
clock = pygame.time.Clock()

def draw_environment(blob_list):
    game_display.fill(WHITE)

    for blob_dict in blob_list:
        for blob_id in blob_dict:
            blob = blob_dict[blob_id]
            pygame.draw.circle(game_display, blob.color, [blob.x, blob.y], blob.size)
            blob.move()

    pygame.display.update()
    

def main():
    blue_blobs = dict(enumerate([Blob(BLUE) for i in range(STARTING_BLUE_BLOBS)]))
    red_blobs = dict(enumerate([Blob(RED) for i in range(STARTING_RED_BLOBS)]))
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                quit()
        draw_environment([blue_blobs,red_blobs])
        clock.tick(60)

if __name__ == '__main__':
    main() 

Сразу же мы получаем ошибку в модуле blob.py относительно нашего класса Blob, где у нас появилось несколько неопределенных переменных. Это явно проблема с написанием классов: вам следует избегать использования констант или переменных вне класса. Давайте добавим эти значения в метод __init__, а затем изменим все части, в которых мы использовали эти константы.

Итак, обновим файл blob.py следующим образом:

import random


class Blob:

    def __init__(self, color, x_boundary, y_boundary):
        self.x_boundary = x_boundary
        self.y_boundary = y_boundary
        self.x = random.randrange(0, self.x_boundary)
        self.y = random.randrange(0, self.y_boundary)
        self.size = random.randrange(4,8)
        self.color = color
        

    def move(self):
        self.move_x = random.randrange(-1,2)
        self.move_y = random.randrange(-1,2)
        self.x += self.move_x
        self.y += self.move_y

        if self.x < 0: self.x = 0
        elif self.x > self.x_boundary: self.x = self.x_boundary
        
        if self.y < 0: self.y = 0
        elif self.y > self.y_boundary: self.y = self.y_boundary 

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

def main():
    blue_blobs = dict(enumerate([Blob(BLUE,WIDTH,HEIGHT) for i in range(STARTING_BLUE_BLOBS)]))
    red_blobs = dict(enumerate([Blob(RED,WIDTH,HEIGHT) for i in range(STARTING_RED_BLOBS)]))
    while True:
        ...

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

Есть по крайней мере один момент, в котором мы определенно могли бы дать несколько больше свободы программисту, использующему этот класс. Это определение размера нашей кляксы:

 self.size = random.randrange(4,8)

Есть ли хоть одна причина, по которой не стоит давать программисту простой способ это менять? Мы таких причин не видим. Однако, в отличие от значений x_boundary и y_boundary, нам не обязательно нужен программист, который даст нам значение размера, поскольку мы можем, по крайней мере, использовать разумное начальное значение по умолчанию. То есть мы можем сделать так:

class Blob:

    def __init__(self, color, x_boundary, y_boundary, size_range=(4,8)):
        self.x_boundary = x_boundary
        self.y_boundary = y_boundary
        self.x = random.randrange(0, self.x_boundary)
        self.y = random.randrange(0, self.y_boundary)
        self.size = random.randrange(size_range[0],size_range[1])
        self.color = color

Теперь, если программист захочет изменить размер, он сможет это сделать (но может и не делать). Мы также разрешили программисту изменять скорость кляксы, если есть такое желание:

import random


class Blob:

    def __init__(self, color, x_boundary, y_boundary, size_range=(4,8), movement_range=(-1,2)):
        self.size = random.randrange(size_range[0],size_range[1])
        self.color = color
        self.x_boundary = x_boundary
        self.y_boundary = y_boundary
        self.x = random.randrange(0, self.x_boundary)
        self.y = random.randrange(0, self.y_boundary)
        self.movement_range = movement_range

    def move(self):
        self.move_x = random.randrange(self.movement_range[0],self.movement_range[1])
        self.move_y = random.randrange(self.movement_range[0],self.movement_range[1])
        self.x += self.move_x
        self.y += self.move_y

        if self.x < 0: self.x = 0
        elif self.x > self.x_boundary: self.x = self.x_boundary
        
        if self.y < 0: self.y = 0
        elif self.y > self.y_boundary: self.y = self.y_boundary 

Итак, мы немного разобрали наш класс Blob. Что-нибудь еще бросается в глаза? Нам кажется, что еще надо подумать о границах, в которых наша клякса может существовать.

Могут ли быть примеры, в которых мы бы хотели, чтобы кляксы могли свободно перемещаться вне поля зрения? Безусловно! Но полезен ли этот код для создания границ? Возможно ли, что программисты захотят использовать его довольно часто? Безусловно!

В общем, имеет смысл либо совсем не иметь такого кода, либо выделить его в отдельный собственный метод, например:

import random

class Blob:

    def __init__(self, color, x_boundary, y_boundary, size_range=(4,8), movement_range=(-1,2)):
        self.size = random.randrange(size_range[0],size_range[1])
        self.color = color
        self.x_boundary = x_boundary
        self.y_boundary = y_boundary
        self.x = random.randrange(0, self.x_boundary)
        self.y = random.randrange(0, self.y_boundary)
        self.movement_range = movement_range

    def move(self):
        self.move_x = random.randrange(self.movement_range[0],self.movement_range[1])
        self.move_y = random.randrange(self.movement_range[0],self.movement_range[1])
        self.x += self.move_x
        self.y += self.move_y

    def check_bounds(self):
        if self.x < 0: self.x = 0
        elif self.x > self.x_boundary: self.x = self.x_boundary
        
        if self.y < 0: self.y = 0
        elif self.y > self.y_boundary: self.y = self.y_boundary 

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

В следующей статье серии, посвященной объектно-ориентированному программированию, мы обсудим наследование.

Следующая статья — Введение в объектно-ориентированное программирование: наследование.