Основные понятия
Что такое ООП
Объектно-ориентированное программирование (далее ООП) представляет собой методологию, в которой программа понимается как совокупность объектов, имеющих определенное состояние и поведение. Каждый из них является экземпляром класса, которые, в свою очередь, образуют иерархическую структуру наследования.
Изначально ООП создавалось с целью перенести привычное нам восприятие реального мира в разработку кода. В повседневной жизни мы используем объекты, имеющие определенный набор характеристик. ООП, в свою очередь, оперирует классами, которые описывают свойства и методы объектов.
Например, в программе, написанной по методологии ООП, можно определить класс автомобиль со свойствами «производитель», «модель», «год выпуска» и методами «ехать», «тормозить», «поворачивать» и так далее.
Перейдем к рассмотрению основных определений ООП.
Что такое класс
Класс – это набор полей и методов для работы с ними, описывающий свойства и поведение определенного объекта. Поля могут быть приватными, то есть недоступными извне, или публичными, которые доступны для просмотра и изменения. Таким образом, классы описывают:
- Свойства (атрибуты) и состояние объекта
- Операции, доступные для взаимодействия с данными (методы).
- Структуры данных объекта.
В отличие от других языков ООП, Go не имеет отдельного обозначения для объектов и классов. Вместо этих сущностей, здесь используются структуры (тип struct), содержащие именованные поля и предоставляющие основной способ создания новых типов данных. Тем не менее, классы и структуры – это не одно и то же.
Что такое объект
Объект – это экземпляр конкретного класса с заданным состоянием (данными) и поведением. Каждый объект существует независимо от других, но способен взаимодействовать с ними при помощи методов.
Для инициализации объектов класса используются специальные функции, называемые конструкторами. Go не предусматривает отдельных инструментов для их создания, как, например init()
в Python. Идиоматический способ определения конструкторов в Go заключается в использовании переменных, заполняющих поля структуры нулевыми значениями, или обычных функций с названием New
.
Для примера создадим структуру Person
с полями Name
и Age
и конструктор для неё:
В Go предпочтительнее возвращать структуры по указателю, чтобы избежать ненужного копирования и позволить изменять поля. Но в некоторых случаях может потребоваться вернуть структуру по значению, например, если нельзя допустить, чтобы вызывающая сторона могла изменить поля исходной структуры.
Отношения между объектами
В этом пункте под объектом будем подразумевать не экземпляр конкретного класса, а более широкое понятие, обозначающее некую сущность в программе (класс, структуру и так далее).
Выделяют несколько видов отношений между объектами. Условно их делят на две группы: is-a (является) и has-a (имеет, содержит). Давайте поближе познакомимся с их особенностями и выясним, чем они отличаются друг от друга.
Ассоциация (has-a)
Ассоциация – это отношение между объектами, при котором один из них может вызвать другой для выполнения действия от его имени. При ассоциации объекты никак не связаны между собой, а отношения между ними могут быть однонаправленными и двунаправленными (когда одни объекты знают о существовании других).
Выделяют две разновидности ассоциации: агрегация и композиция.
Агрегация (has-a)
Агрегация – это отношение «часть-целое» с более сильной зависимостью по сравнению с ассоциацией. В этом случае один объект содержит другой, но последний из них может существовать сам по себе. Например, водитель использует машину, но она может существовать независимо от него и даже иметь несколько владельцев.
Агрегация в Go осуществляется с помощью встраивания одной структуры в другую в качестве одного из полей. При этом поля и методы внутренней структуры недоступны через внешнюю:
Композиция (has-a)
В отличие от агрегации, композиция предполагает строгую зависимость между объектами. При этом подходе объекты существуют только в контексте друг друга, один объект является неотъемлемой частью другого.
К примеру, двигатель является основной составляющей машины, без него она не будет выполнять все необходимые функции. В исключительных случаях это правило может быть сознательно нарушено. Например, если двигатель по какой-то причине необходимо отделить от автомобиля.
Как и в случае с агрегацией, в Go композиция реализуется при помощи встраивания одной структуры в другую, но уже без явного указания имени. При этом поля и методы внутренней структуры доступны через внешнюю:
Принципы ООП
В основе ООП лежат четыре ключевых принципа: наследование, абстракция, полиморфизм и инкапсуляция. Давайте детально изучим каждый из них и посмотрим, как они реализуются в Go.
Наследование (is-a)
Наследование – это принцип ООП, при котором абстрактный тип данных имеет возможность перенимать свойства и функционал существующего типа. Согласно этому определению, класс-наследник обладает всеми полями и методами родительского класса и может добавлять новые. Например, класс автомобиль является наследником для класса транспорт, так как автомобиль является транспортом. Наследование способствует повторному использованию кода и созданию иерархии классов.
Принято считать, что Go не реализует наследование в строгом смысле этого понятия. Вместо него, принято использовать композицию, с которой мы познакомились ранее.
Абстракция
Абстракция – один из важнейших принципов, лежащих в основе понимания ООП. Он заключается в выделении ключевых характеристик объекта, исключении менее значимых и игнорировании деталей реализации. Такой подход позволяет сосредоточиться на том, что объект делает, а не на том, как он это делает.
Абстракция достигается определением абстрактных классов и интерфейсов. Абстрактный класс определяет общие характеристики и поведение объектов, а интерфейс описывает сигнатуры методов без их реализации.
Абстракция имеет несколько ключевых преимуществ для разработки:
- Позволяет упростить сложные системы, выделяя общие характеристики и игнорируя несущественные детали.
- Способствует гибкости и расширяемости, так как система может быть легко дополнена новыми типами, которые реализуют существующие интерфейсы.
- Облегчает повторное использование кода за счет создания моделей объектов.
Полиморфизм
Полиморфизм в ООП – это способность объекта работать с данными разных типов. Полиморфизм позволяет объектам разных классов обрабатываться с использованием общего интерфейса. Выделяется два основных вида полиморфизма: параметрический и ad-hoc (интерфейсный). Первый из них позволяет писать обобщенный код, поддерживающий любые типы данных. При втором подходе объекты разного типа обрабатываются одинаково вне зависимости от их структуры за счет использования интерфейсов. Таким образом, разница между этими двумя видами заключается в способе достижения обобщенности: через параметры типов в первом случае и через интерфейсы во втором. Принято считать, что в Go реализуется ad-hoc полиморфизм, поэтому дальше будем рассматривать именно его.
Отметим, что концепции полиморфизма и абстракции схожи между собой:
- Абстракция предоставляет абстрактный класс или интерфейс для работы с объектом и скрывает детали реализации.
- Полиморфизм, в свою очередь, позволяет создавать реализации этого интерфейса и использовать его для работы с разными объектами.
Перед рассмотрением реализации полиморфизма в Go вспомним важное правило: если определенный тип реализует все методы интерфейса, то этот тип автоматически удовлетворяет ему. Руководствуясь этим соображением, напишем небольшую программу, содержащую интерфейс Animals
с единственным методом Voice()
для работы с разными видами животных:
Написанный выше код показывает классический пример полиморфизма: два типа Dog
и Cat
реализуют один интерфейс Animal
, но каждый из них имеет разное поведение, зависящее от типа. Такой подход способствует расширяемости кода, так как позволяет без труда создавать новые методы интерфейса Animals
и типы, удовлетворяющие ему.
Убедимся в корректной работе программы:
В результате выполнения кода на экран будет выведено:
Инкапсуляция
Принцип инкапсуляции заключается в объединении данных и методов работы с ними в один класс и скрытии деталей его реализации. В правильной реализации этот класс должен ограничивать доступность полей и позволять взаимодействовать с ними только через единый интерфейс.
Для управления доступностью объектов используются модификаторы доступа. С их помощью можно скрыть приватные данные от несанкционированного просмотра и изменения.
В большинстве языков с методологией ООП используется два основных модификатора доступа: public – публичный, то есть доступный из любой части программы, и private – приватный, недоступный для обращения извне.
В Go нет специальных обозначений для модификаторов доступа. Вместо этого принято соглашение о том, что объекты, названия которых начинаются с заглавной буквы, являются экспортируемыми, а с маленькой – приватными, то есть доступными только из того же пакета. Этому правилу подчиняются пять видов объектов: функции, переменные, структуры, их поля и методы.
В коде ниже показаны примеры определения экспортируемых и неэкспортируемых объектов:
Механизм экспорта структур был детально рассмотрен на примере проекта в 9 части самоучителя в пункте «Структуры и пакеты». Советуем обратиться к нему, чтобы закрепить применение инкапсуляции в Go.
Геттеры и сеттеры
Для обеспечения контролируемого доступа к данным класса используются так называемые геттеры (от английского get – получать) и сеттеры (от английского set – устанавливать). Они представляют собой методы для получения и установки соответственно значений полей класса. Иначе говоря, геттеры и сеттеры являются посредниками между классом и пользователем. Если запрашиваемая операция может быть выполнена без нарушения принципа инкапсуляции, то они её делают, иначе – нет. Это обеспечивает безопасность конфиденциальных данных и предоставляет контролируемый инструмент для работы с ними.
Задачи
Пришло время применить изученные за несколько частей самоучителя знания в прикладных задачах. Они довольно содержательные, поэтому разбиты на несколько связанных подзадач, решение которых в конечном счете приведет к созданию небольшой программы. Рекомендуем внимательно читать условия, так как в них содержится много подсказок и ответов на возможные вопросы.
Задача 1: банковская система
Необходимо создать программу, имитирующую работу банковского счета. Она должна содержать структуру Account
с приватными полями: имя владельца и баланс денежных средств, а также конструктор для неё.
Для взаимодействия со счетом нужно реализовать методы:
NewAccount(owner string) *Account
– конструктор для структуры Account
SetBalance(quantity float64) error
– метод установки баланса (сеттер)
GetBalance() float64
– метод получения баланса (геттер)
Deposit(quantity float64) error
– метод зачисления средств на счет
Withdraw(quantity float64) error
– метод вывода средств со счета
Учитывайте, что количество и баланс не могут принимать отрицательные значения. Для этого определите собственные типы ошибок и возвращайте их в методах SetBalance
, Deposit
и Withdraw
.
Подзадача 1: конструктор и пользовательские типы ошибок
Реализуйте структуру Account
с требуемыми полями и конструктор для неё. Также инициализируйте две переменные для описания ошибок отрицательного баланса и количества.
Подсказка: для создания ошибок с заданным текстом используйте функцию errors.New()
.
Решение:
Подзадача 2: геттеры и сеттеры
Определите методы SetBalance
и GetBalance
с указанными в условии сигнатурами.
Решение:
Подзадача 3: депозит и снятие
Создайте методы с сигнатурами из условия для депозита и снятия средств со счета. Помните, что вводимое количество средств и баланс не могут быть отрицательными.
Решение:
Подзадача 4: тестирование программы
Напишите функцию main()
для тестирования написанного кода. Создайте экземпляр структуры Account
, протестируйте все реализованные методы с разными аргументами и проверьте результат их выполнения на наличие ошибок.
Пример решения:
Задача 2: управление складом
В этой задаче требуется написать систему управления складом с товарами. Необходимо создать структуры Product
для представления товара и Storage
для хранения товаров в мапе.
Для взаимодействия с элементами склада используется интерфейс Warehouse
со следующими методами:
– метод добавления нового товара на склад.AddProduct(product Product) error
UpdateQuantity(productID int, quantity int) error
– метод изменения количества товара на складе.
Структура Storage
должна удовлетворять интерфейсу Warehouse
.
Подзадача 1: структуры Product и Storage, интерфейс Warehouse
Создайте структуру Product
с полями ID
, Name
и Quantity
типов int
, string
и int
соответственно, структуру Storage
с полем типа map[int]Product
для хранения товаров и интерфейс Warehouse
с указанными в условии методами.
Решение:
Подзадача 2: метод добавления нового товара на склад
Реализуйте метод AddProduct
с сигнатурой из условия для добавления нового товара на склад. Учитывайте, что если id
добавляемого товара уже присутствует в хранилище, то метод должен вернуть ошибку с поясняющим текстом.
Подсказка: не забудьте создать мапу с помощью функции make
.
Решение:
Подзадача 3: метод изменения количества товара на складе
Напишите код для метода UpdateQuantity
, использующийся для изменения количества товара на складе. Возвращайте ошибку с поясняющим текстом, если id
товара не найден на складе или количество товара приняло отрицательное значение.
Решение:
Подзадача 4: тестирование программы
Напишите функцию main()
для тестирования написанного кода. Создайте экземпляр структуры Storage
, протестируйте методы AddProduct
и UpdateQuantity
с разными аргументами и проверьте результат их выполнения на наличие ошибок.
Пример решения:
Подведём итоги
ООП является мощным и эффективным подходом к разработке программного обеспечения. Он позволяет создавать модульные, гибкие и повторно используемые кодовые базы, что упрощает разработку, поддержку и масштабирование проектов.
Для отработки изученных знаний рекомендуем модифицировать решенные задачи или с нуля создать систему по методологии ООП.
В следующей части детально рассмотрим тип error и механизм обработки ошибок в Go, изучим функции паники и восстановления, а также научимся логировать информацию о состоянии программы.
Содержание самоучителя
- Особенности и сфера применения Go, установка, настройка
- Ресурсы для изучения Go с нуля
- Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
- Переменные. Типы данных и их преобразования. Основные операторы
- Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
- Функции и аргументы. Области видимости. Рекурсия. Defer
- Массивы и слайсы. Append и сopy. Пакет slices
- Строки, руны, байты. Пакет strings. Хеш-таблица (map)
- Структуры и методы. Интерфейсы. Указатели. Основы ООП
- Наследование, абстракция, полиморфизм, инкапсуляция
- Обработка ошибок. Паника. Восстановление. Логирование
- Обобщенное программирование. Дженерики
- Работа с датой и временем. Пакет time
- Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
- Конкурентность. Горутины. Каналы
- Тестирование кода и его виды. Table-driven подход. Параллельные тесты
- Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
Комментарии