ООП и диаграммы классов UML

Кратко о том, что такое ООП

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

Объектно-ориентированое программирование активно оперирует следующими понятиями:

Инкапсуляция

Что это и зачем?

Инкапсуляция - упаковка данных и функций в единый компонент. В общем случае, в разных языках программирования термин «инкапсуляция» относится к одному из или обоим определениям:

  • механизм языка, позволяющий ограничить доступ одних компонентов программы к другим.

    Например, ограничивается доступ к переменным объекта класса. В Python, чтобы создать в классе скрытую переменную, такую переменную, что к ней имеют доступ только методы самого класса, нужно перед именем переменной поставить __ (два подчеркивания).

    Давайте рассмотрим пример:

    # coding=UTF-8
    class TestClass:
    
        def __init__(self):
            self.public_variable = "I'm public!"
            self.__private_variable = "I'm too shy to be public!"
    
        def get_public_variable(self):
            return self.public_variable
    
        def get_private_variable(self):
            return self.__private_variable
    
    if __name__ == "__main__":
        test_class = TestClass()
        print(" ".join(["Public variable:", test_class.get_public_variable()]))
        print(" ".join(["Public variable:", test_class.public_variable]))
    
        print(" ".join(["Private variable:", test_class.get_private_variable()]))
        print(" ".join(["Private variable:", test_class._private_variable]))
    

    Если вы запустите этот код, то вы получите следующее:

    Public variable: I'm public!
    Private variable: I'm too shy to be public!
    Traceback (most recent call last):
      File "private_access.py", line 13, in <module>
        print(" ".join(["Private variable:", test_class._private_variable]))
    AttributeError: 'TestClass' object has no attribute '_private_variable'
    

    Удобство инкапсуляции в следующем:

    1. Безопасность: никто не может залезть внутрь класса и записать в переменные все что захочет, тем самым, сломав вашу программу;
    2. Удобство: рефакторинг (переписывании кода). Вы можете начать переписывать класс, переназвать переменные и вам не придется бегать по коду и менять везде test_class.public_variable на test_class.new_public_variable, вам нужно будет поменять всего одну функцию get_public_variable.
  • языковая конструкция, позволяющая связать данные с методами, предназначенными для обработки этих данных.

    Эта концепция очень близка к предыдущей. Давайте посмотрим на два кода:

    # coding=UTF-8
    class PositiveInt:
        __a = 0
    
        def set_a(self, a):
            if a >=0:
                self.__a = int(a)
            else:
                print("Wrong parameter, an internal state won't change." )
    
        def get_a(self):
            return self.__a
    
    if __name__ == "__main__":
        value = PositiveInt()
    
        print(value.get_a())
    
        value.set_a(10)
        print(value.get_a())
    
    
        value.set_a(-10)
        print(value.get_a())
    
    1. # coding=UTF-8
      class PositiveInt:
          a = 0
      
      if __name__ == "__main__":
          value = PositiveInt()
      
          print(value.a)
      
          value.a = 10
          print(value.a)
      
      
          param = -10
          if param > 10:
              value.a = param
          else:
              print("Wrong parameter, an internal stayte won't change." )
      
          print(value.a)
      

      Собственно, оба этих кода делают одно и тоже.

      Давайте представим, что пришел код-ревьюер, который проверял ваш код на чистоту/читаемость/верность стиля и сказал что нужно переименовать __a в __positive_integer, потому что так по названию переменной понятней, зачем она нужна.

      То в случае кода 1 вы поменяете код в трех местах в классе PositiveInt и больше нигде. По сути, внутренности класса поменялись, но никто из тех, кто обращался к этому классу, этого не заметил.

      А в случае 2 помимо самого класса вам придется ходить по всему коду и везде менять имя переменной, что, согласитесь, не очень удобно. А еще это может вызвать кучу ошибок.

Класс

Что это?

Класс - это способ описания сущности, определяющий состояние и поведение, зависящее от этого состояния, а также правила для взаимодействия с данной сущностью (методы и уровни доступа к переменным класса).

Зачем это нужно?

  1. Для создания сложной структуры данных со сложным поведением;
  2. Для поддержки механизмов инкапсуляции, полиморфизма и наследования;
  3. Для удобства. Большая задача разбивается на много функциональных блоков меньшего размера, каждый из который реализуется классом.

Объект

Что это?

Объект - это конкретный экземпляр класса, поля которого проинициализированы.

Наследование

Наследование - это метод расширения функциональности классов и снижения дубликации кода, когда один класс полностью забирает себе (наследует) все поля и методы другого класса (класса родителя) и добавляет новые поля и методы или переопределяет старые, тем самым расширяя/изменяя функциональность класса в сравнении с классом-родителем.

Определения

Рассмотрим простое наследование, пусть класс Derived --> Base. В Python 3 это осуществляется следующим кодом:

class Base:
    pass

class Derived(Base):
    pass

Класс Base в данном случае является базовым классом, родительским классом, надклассом, суперклассом, предком.

Класс Derived по отношению к нему является производным классом, дочерним классом, подклассом, потомком.

Говорят, что Derived наследует, расширяет или специализирует Base.

В языке Python 3 существует единый базовый класс object, который неявно является предком всех объектов вообще.

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

Зачем это нужно?

Давайте рассмотрим пример, когда это может понадобиться. Классы создаются для объединения кода и функций, его обрабатывающих. Однако, несколько классов часто оказываются настолько похожими, что код приходится дублировать.вфаеыучюсщь

class Student(UniversityMember):
    group = None
    passToUniversity = ''
    status = True

    def checkStatus(self):
        return self.status

    def dismiss(self):
        self.status = False
        self.pass_to_university = None

class Teacher(UniversityMember):
    cathedral = None
    passToUniversity = ''
    status = True

    def checkStatus(self):
        return self.status

    def dismiss(self):
        self.status = False
        self.pass_to_university = None

class Administrator(UniversityMember):
    passToUniversity = ''
    status = True

    def checkStatus(self):
        return self.status

    def dismiss(self):
        self.status = False
        self.pass_to_university = None

В данном случае и у студента, и у преподавателя, и у администратора должны быть свойства status и pass_to_university, возможность проверки статуса и возможность увольнения.

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

Наследование классов

Заменим дублирование кода явным наследованием от абстактного класса (см.АДТ) UniversityMember:

class UniversityMember:
    passToUniversity = ''
    status = True

    def checkStatus(self):
        return self.status

    def dismiss(self):
        self.status = False
        self.pass_to_university = None

class Student(UniversityMember):
    group = None

class Teacher(UniversityMember):
    cathedral = None

class Administrator(UniversityMember):
    pass

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

Язык объектно-ориентированного моделирования UML включает в себя не только диаграммы классов, но и множество других диаграмм, позволяющих лучше представить будущую программу.

За более подробной информацией можно обратиться к Wikipedia или пойти в гугл.

В нашем случае при помощи UML отношение классов можно представить следующим образом:

И более полная версия, включающая в себя поля и методы классов:

Перегрузка методов

Любой метод можно переопределить, то есть повторно реализовать в подклассе. В этом случае для экземпляров базового класса будет вызываться базовый метод, а для экземпляров производного -- перегруженный.

class Base:
    def hello():
        print("Hello! I'm base class!")

class Derived(Base):
    def hello():
        print("Hello! I'm derived class!")

b = Base()
d = Derived()
b.hello()       # Hello! I'm base class!
d.hello()       # Hello! I'm derived class!

Этот механизм называется динамическим связыванием методов или полиморфизмом.

В языке Python используется механизм грубого определения типа (утиная типизация):

When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.

Это значит, что если нам нужно вызвать некий метод объекта, то не важно, к какому классу относится этот объект, главное, чтобы он имел метод, который предполагается вызвать.

Подстановочный критерий Барбары Лисков

Правильно используйте наследование!

Механизм наследования используется для моделирования отношений типа "является".

В случае с классами Student, Teacher и Administrator мы могли бы ошибочно сделать Administrator предком Student и Teacher, поскольку это позволяет сэкономить код, да и вроде бы они только расширяют его функциональность...

class Administrator:
    passToUniversity = ''
    status = True

    def checkStatus(self):
        return self.status

    def dismiss(self):
        self.status = False
        self.pass_to_university = None

class Student(Administrator):
    group = None

class Teacher(Administrator):
    cathedral = None

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

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

Подстановочный критерий Барбары Лисков гласит также, что класс-потомок не только должен уметь делать всё то же, что и предок, но и не должен требовать для этого ничего нового.

Роберт С. Мартин определил этот принцип так:

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

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

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

Множественное наследование

При множественном наследовании у класса может быть более одного предка. В этом случае класс-потомок наследует методы всех предков.

class SuperBase: # Предок предка
        def do(self):
        print('Метод суперпредка!')
class Base1(SuperBase):   # Предок 1
    def do_it(self):
        print('Метод предка 1')
class Base2:   # Предок 2
    def do_it(self):
        print('Метод предка 2')
class Derived(Base1, Base2):   # Наследник
    def do_it_by_myself(self):
        print('Метод наследника')

d = Derived()   # инстанциация

d.do_it_by_myself()  # Если в классе-потомке есть перегруженный метод с искомым названием
                     # то он будет вызван независимо от наличия таких же методов у предков.

d.do_it() # Если такого метода нет, то он ищется в порядке "лествичного права":
                  # в первую очередь у ближайших предков -- слева-направо,
                  # затем у их предков в том же порядке слева-направо, пока не будет найден.
                  # В данном случае будет вызван метод предка 1.

d.do()  # Метод суперпредка вызывается, только если такого нет
        # ни у класса, ни у его ближайших предков

Полиморфизм

Что это?

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

Зачем это нужно?

Звучит сложно.

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

Самые простые варианты, которые приходят в голову — написать три отдельных класса и работать с ними. Или написать один класс, в которым будут все свойства, присущие всем трем типам публикаций, а задействоваться будут только нужные. Но ведь для разных типов аналогичные по логике методы должны работать по-разному. Делать несколько однотипных методов для разных типов (get_news, get_announcements, get_articles), как уже обсуждалось, не есть хорошо. Тут нам и поможет полиморфизм.

# coding=UTF-8


class Publication:
    __title = ""
    __text = ""

    def __init__(self, title, text):
        self.__title = title
        self.__text = text

    def get_title(self):
        return self.__title

    def get_text(self):
        return self.__text

    def set_title(self, title):
        self.__title = title

    def set_text(self, text):
        self.__text = text

    def __str__(self):
        return self.get_str()


class News(Publication):
    __publication_date = ""
    __sources = []

    def __init__(self, title, text, publication_date, sources):
        super().__init__(title, text)
        self.__publication_date = publication_date

        self.set_sources(sources)

    def set_publication_date(self, publication_date):
        self.__publication_date = publication_date

    def get_publication_date(self):
        return self.__publication_date

    def set_sources(self, sources):
        if not isinstance(sources, list):
            self.__sources = [sources]
        else:
            self.__sources = sources

    def get_sources(self):
        return self.__sources

    def get_str(self):
        return " ".join(["News:", self.get_title(), "\n",
                        "Text:", self.get_text(), "\n",
                        "Publication date:", self.get_publication_date(), "\n",
                        "Sources: ", " ".join(self.get_sources()), "\n",
                        "------------------------------------------------\n"])


class Announcement(Publication):
    __out_date = ""

    def __init__(self, title, text, out_date):
        super().__init__(title, text)
        self.__out_date = out_date

    def get_out_date(self):
        return self.__out_date

    def set_out_date(self, out_date):
        self.__out_date = out_date

    def get_str(self):
        return " ".join(["Announcement:", self.get_title(), "\n",
                        "Text:", self.get_text(), "\n",
                        "Out date:", self.get_out_date(), "\n",
                        "------------------------------------------------\n"])

class Article(Publication):
    __authors = []

    def __init__(self, title, text, authors):
        super().__init__(title, text)
        self.set_authors(authors)

    def set_authors(self, authors):
        if not isinstance(authors, list):
            self.__authors = [authors]
        else:
            self.__authors = authors

    def get_authors(self):
        return self.__authors

    def get_str(self):
        return " ".join(["Article:", self.get_title(), "\n",
                        "Text:", self.get_text(), "\n",
                        "Authors:", ", ".join(self.get_authors()), "\n",
                        "------------------------------------------------\n"])

if __name__ == "__main__":

    news = News("Braking news!", "That's a really exiting news!", "12 of November 2016", ["CNN", "BBC"])
    announce = Announcement("New announcement!", "I want to by an elephant!", "15 of December 2016")
    article = Article("We have new investigation", "Мы изобрели зелененький глазовыколупыватель", ["Профессор Бред",
                                                                                                   "Ассистент Капитан Очевидность"])
    strange_list = [news, announce, "Просто кусок непонятного бреда", article]

    for element in strange_list:
        if isinstance(element, Publication):
            print(element)

Метод __str__(self) есть у всех объектов в Python и вызывается когда мы пишем в коде print(some_object), т.е. на самом деле, print(some_object) интерпретатором Python превращается в some_object.__str__(). У каждого объекта в Python есть два очень похожих метода __repr__(self) и __str__(self). Оба этих метода возвращают строку.

  1. __str__(self) возвращает строку, которая кратко в неформальном стиле описывает объект. То, что показывается пользователю, когда он делает print.
  2. __repr__(self) возвращает строку, которая полностью описывает объект. Как правило, по строке, которую возвращает __repr__, можно понять тип объекта и получить всю информацию о его состоянии.

Пример:

# coding=UTF-8
>>> from decimal import Decimal
>>>
>>> a = Decimal(1.2)
>>> print(a) # В этом случае вызовется __str__(self)
1.2
>>> a # А в этом __repr__(self)
Decimal('1.2')
>>>

Вернемся к программе выше:

Собстенно, в Publication есть метод __str__(self) внутри которого вызывается get_str(self). get_str(self) в Publication не реализован.

get_str(self) реализован в потомках Publication. Поэтому, когда мы делаем print(element), то __str__(self) будет вызван из родителя Publication, потому что в детях он не переопределен.

А вот get_str(self) будет вызван уже из потомков, т.к. в Publication он не реализован. Т.е. Publication использует метод, который будет определен только в потомке. Потомок, тем самым, будет менять результат работы предка - это и есть полиморфизм, один вызов, разное поведение.

Упражнение №1: Расшарь соседу

Подозреваю, что после прочтения опуса выше вы чувствуете себя так:

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

Можно использовать бумажку и ручку. Главная задача запомнить названия и зачем каждая концепция нужна, привести примеры (которые были приведены, а еще лучше придумать свои). Если что-то не поняли, обсудите это с соседом.

У вас 10 минут, только не очень громко, другим мешать иначе будете :)

P.S. Если понимаете, что совсем не поняли, перечитайте дома, погуглите в интернете непонятные моменты.

Упражнение №2: Warm up

2.1. Треугольник и прямоугольник

Напишите программу, которая объявляет класс Shape, конструктор которого принимает ширину и высоту.

После этого унаследуйте от него класс Triangle и Rectangle. Реализуйте метод area(), который возвращает площадь этих фигур.

Продемонстрируйте работоспособность программы.

2.2 Дочки-матери

Напишите программу с классом Mother от которого наследуется класс Daughter.

Сделайте так, чтобы результат print(object) был разный.

Воспользуйтесь принципами полиморфизма, наследования и инкапсуляции.

2.3 Животные

Реализйте класс Animal. Внутри объявите поле для имени и возраста.

От класса Animal унаследуйте класс Zebra и Dolphin.

Оба класса могут вернуть описание, содержащее имя, возраст и какую-то доп.информацию, например, что это за вид животного.

Воспользуйтесь принципами полиморфизма, наследования и инкапсуляции.

Упражнение №3. Игра "Арифметические драконы"

Игра "Арифметические драконы" предназначена для обучения детей арифметике. На героя нападает дракон, который задаёт вопрос на сложение (если дракон зелёный), вычитание (красный) или умножение (чёрный).

  1. Разбейтесь по командам по два программиста и сядьте за один компьютер.
  2. Скачайте архив arithmetic_dragons
  1. Реализуйте следующие классы:

и добейтесь работоспособности игры.

  1. Далее можете ввести новых атакующих юнитов:

    • тролля, который задаёт вопрос "Угадай число от 1 до 5"
    • тролля, который задаёт вопрос на простоту числа
    • тролля, который просит разложить число на множители и перечислить их через запятую