§2. Модульное и объектно-ориентированное программирование
Оглядываясь на историю развития программирования, нельзя не заметить две тенденции:
- смещение акцентов от программирования отдельных деталей к программированию более крупных компонентов;
- смещение акцентов от структур алгоритмов к структурам данных.
Основные понятия ооп
В этом разделе мы рассмотрим следующие понятия: класс, объект, поле, метод и свойство. Классом называется особый тип записи, который может иметь в своем составе поля, методы и свойства. Такой тип называется объектным типом. Например, так выглядит описание класса, содержащего поле MyField целочисленного типа и метод MyMethod, дающий целочисленный результат: type TMyObject = class(TObject) MyField : integer; Function MyMethod : integer; end; Для того чтобы использовать новый тип в программе, следует объявить переменную этого типа. Переменная объектного типа называется экземпляром класса или объектом. Поля объекта – это данные, уникальные для каждого экземпляра. В отличие от полей, методы у всех объектов одного класса общие. Свойства – это поля, доступные не напрямую, а через методы. У каждого класса наряду с методами, присущими только ему, должны присутствовать два общих метода – конструктор и деструктор. Конструктор предназначен для выделения памяти объекту и инициализации его свойств. Общепринято называть конструктор Create. Новый класс порождается от какого-либо класса-предка. Для того чтобы правильно проинициализировать в создаваемом объекте поля, относящиеся к классу-предку, следует сразу же при входе в конструктор вызвать конструктор предка: Constructor TMyObject.Create begin inherited Create; … end; Деструктор выполняет обратное конструктору действие – уничтожает объект и освобождает выделенную ему память. Стандартное название деструктора – Destroy. На первых порах вам почти не придется включать в свои программы явный вызов конструкторов и деструкторов. Дело в том, что любой компонент, попавший при визуальном программировании в ваше приложение из Палитры компонентов, включается в определенную иерархию. Иерархия эта замыкается на форме (TForm): для всех ее составных частей конструкторы и деструкторы вызываются автоматически, незримо для программиста. Создает и уничтожает формы приложение (глобальный объект с именем Application). В файле проекта (.dpr) вы можете увидеть вызов метода CreateForm, предназначенного для этой цели. Что же касается объектов, создаваемых динамически (во время выполнения приложения), то здесь нужен явный вызов конструктора.
Принцип модульности ООП в 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 , то тогда границы будут применены.
В следующей статье серии, посвященной объектно-ориентированному программированию, мы обсудим наследование.