Что такое объектно-ориентированное программирование (ООП)?

Что такое объектно-ориентированное программирование (ООП) Изучение

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

Что такое объектно-ориентированное программирование и зачем оно нужно?

Термин «объектно-ориентированное программирование» был придуман легендой программирования Аланом Кеем в конце 1960-х годов. Он был соразработчиком новаторского объектно-ориентированного языка программирования Smalltalk, на который повлиял Simula, первый язык с функциями ООП. Фундаментальные идеи Smalltalk продолжают влиять на особенности ООП современных языков программирования и по сей день. Языки, на которые влияет светская беседа, включают: Руби, Python, Go и Swift.

Наряду с популярным функциональным программированием (ФП) объектно-ориентированное программирование является одной из преобладающих парадигм программирования. Подходы к программированию можно разделить на два основных направления: «императивное» и «декларативное». ООП — это проявление императивного стиля программирования и, в частности, дальнейшее развитие процедурного программирования:

  1. Императивное программирование : Опишите пошагово, как решить проблему — пример: алгоритм.
    • Структурированное программирование
      • Процедурное программирование
        • объектно-ориентированное программирование
  2. Декларативное программирование : генерировать результаты в соответствии с определенными правилами — пример: SQL-запрос
    • функциональное программирование
    • Специфическое для домена программирование

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

Большинство современных языков — это так называемые мультипарадигмальные языки, которые позволяют программировать в нескольких стилях программирования. С другой стороны, есть языки, которые поддерживают только один стиль программирования; это особенно верно для строго функциональных языков, таких как Haskell:

Парадигма Особенности Особенно подходит для Языки
императив OOP Объекты, классы, методы, наследование, полиморфизм Моделирование, системный дизайн Smalltalk, Java, Ruby, Python, Swift
императив процедурный Поток управления, итерация, процедуры/функции Последовательная обработка данных C, Pascal, Basic
декларативный Функциональный Неизменяемость, чистые функции, лямбда-исчисление, рекурсия, системы типов Параллельная обработка данных, математические и научные приложения, парсеры и компиляторы Lisp, Haskell, Clojure
декларативный Доменный язык (DSL) Выразительный, большой выбор языков Приложения для конкретных доменов SQL, CSS

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

  1. Данные : значения, структуры данных, переменные
  2. Код : выражения, структуры управления, функции

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

Проиллюстрируем разницу на примере. Целочисленная переменная в Java или C++ содержит только значение. Это не структура данных, а «примитив»:

int number = 42;

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

int successor(int number) {
  return number + 1;
}
// returns `43`
successor(42)

Напротив, в таких языках, как Python и Ruby, все является объектом. Даже простое число включает фактическое значение и набор методов, определяющих операции над значением. Вот пример встроенной в Ruby функции succ :

# returns `43`
42.succ

В первую очередь это удобно тем, что увязывает функционал в один тип данных. Невозможно вызвать метод, который не соответствует типу. Но методы могут сделать еще больше. В Ruby даже цикл for реализован как числовой метод. В качестве примера выведем числа от 51 до 42:

51.downto(42) { |n| print n, ".. " }

Так откуда же берутся методы? Объекты определяются в большинстве языков с помощью классов. Говорят, что объекты создаются из классов, поэтому объекты также называются экземплярами. Класс — это шаблон для создания подобных объектов, использующих одни и те же методы. Таким образом, в чисто ООП-языках классы действуют как типы. Это становится ясно с ((объектно-ориентированное программирование на Python|11RARDEGE89991)); функция типа возвращает класс как тип значения:

type(42) # <class 'int'=""></class>
type('Walter White') # <class 'str'=""></class>

Как работает объектно-ориентированное программирование?

Если вы спросите человека с опытом программирования в несколько семестров, что такое ООП, ответ, скорее всего, будет «как-то связан с классами». На самом деле, однако, классы не суть дела. Основные идеи объектно-ориентированного программирования Алана Кея проще, и их можно резюмировать следующим образом:

  1. Объекты инкапсулируют свое внутреннее состояние.
  2. Объекты получают сообщения через свои методы.
  3. Методы назначаются динамически во время выполнения.
Читайте также:  Локальное и облачное ПО

Мы более подробно рассмотрим эти три критических момента ниже.

Объекты инкапсулируют свое внутреннее состояние

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

Давайте рассмотрим несколько примеров. У нас есть объект car, который представляет автомобиль. Внутри объекта состояние хранится в переменных. Объект управляет значениями переменных; например, можно гарантировать, что запуск двигателя потребляет энергию. Запускаем двигатель автомобиля, отправив стартовое сообщение:

car.start()

В этот момент объект решает, что делать дальше : если двигатель уже запущен, сообщение игнорируется или выдается соответствующее сообщение. При недостаточном заряде аккумулятора или пустом баке двигатель останавливается. Если все требования соблюдены, двигатель запускается и внутреннее состояние корректируется. Например, логической переменной motor_running присваивается значение true, и заряд батареи уменьшается на величину заряда, необходимого для запуска. Схематично покажем, как мог бы выглядеть код внутри объекта:

# starting car
motor_running = True
battery_charge -= start_charge

Важно, что внутреннее состояние нельзя изменить непосредственно извне. В противном случае мы могли бы установить для motor_running значение true, даже если батарея разряжена. Это было бы волшебством и не отражало бы реальных фактов действительности.

Отправка сообщений/вызов методов

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

# call a method
receiver.method(args)

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

# find a person in our address book
person = contacts.find('Walter White')
# let's call that person's work number
call = phone.call(person.phoneNumber('Work'))
...
# after some time, hang up the phone
call.hangUp()

Динамическое назначение методов

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

Динамическое назначение методов имеет важные последствия для технической реализации функций ООП в языках программирования. На практике вы обычно имеете меньше общего с этим. Тем не менее, давайте рассмотрим пример. Мы моделируем фонарик смартфона как объект -фонарик. Это реагирует на сообщения о включении, выключении и интенсивности :

// turn on flashlight
flashlight.on()
// set flashlight intensity to 50%
flashlight.intensity(50)
// turn off flashlight
flashlight.off()

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

function out_of_order() {
  console.log('Flashlight out of order. Please service phone.')
  return false;
}
flashlight.on = out_of_order;
flashlight.off = out_of_order;
flashlight.intensity = out_of_order;

Если мы затем попытаемся взаимодействовать с фонариком, всегда вызывается out_of_order :

// calls `out_of_order()`
flashlight.on()
// calls `out_of_order()`
flashlight.intensity(50)
// calls `out_of_order()`
flashlight.off()

Откуда берутся объекты? Создание экземпляра и инициализация

До сих пор мы видели, как объекты получают сообщения и отвечают на них. Но откуда берутся объекты? Давайте теперь займемся центральной концепцией инстанцирования. Инстанциация — это процесс, посредством которого объект создается. В разных языках ООП существуют разные механизмы создания экземпляров. Обычно используется один или несколько из следующих механизмов:

  1. Определение литерала объекта
  2. Создание экземпляра с функцией конструктора
  3. Создание экземпляра из класса
Читайте также:  Защита данных и повышение безопасности с помощью аутентификации форм

JavaScript блистает здесь, потому что такие объекты, как числа или строки, могут быть определены непосредственно как литералы. Простой пример: мы создаем экземпляр пустого объекта person, а затем назначаем свойство name и метод приветствия. Затем наш объект может приветствовать другого человека и произносить свое имя:

// instantiate empty object
let person = {};
// assign object property
person.name = "Jack";
// assign method
person.greet = function(other) {
  return `"Hi ${other}, I'm ${this.name}"`
};
// let's test
person.greet("Jim")

Мы создали уникальный объект. Однако часто мы хотим повторить инстанцирование, чтобы создать ряд похожих объектов. Этот случай также может быть легко рассмотрен в JavaScript. Мы создаем так называемую функцию-конструктор, которая собирает объект при его вызове. Наша функция-конструктор с именем Person принимает имя и возраст и при вызове создает новый объект:

function Person(name, age) {
  this.name = name;
  this.age = age;
  
  this.introduce_self = function() {
    return `"I'm ${this.name}, ${this.age} years old."`
  }
}
// instantiate person
person = new Person('Walter White', 42)
// let person introduce themselves
person.introduce_self()

Обратите внимание на использование ключевого слова this. Это также встречается в других языках, таких как Java, PHP и C++, и часто сбивает с толку новичков в области ООП. Короче говоря, это заполнитель для созданного объекта. При вызове метода this ссылается на получателя, т.е. указывает на конкретный экземпляр объекта. В других языках, таких как Python и Ruby, вместо this используется ключевое слово self, которое служит той же цели.

Нам также нужно новое ключевое слово в JavaScript, чтобы правильно создать экземпляр объекта. Это можно найти, в частности, в Java и C++, которые различают «стек» и «кучу» при хранении значений в памяти. В обоих языках new используется для выделения памяти в куче. Как и Python, JavaScript хранит все значения в куче, поэтому new фактически не нужен. Python показывает, что можно обойтись и без него.

Третий и наиболее распространенный механизм создания экземпляров объектов использует классы. Класс выполняет роль, схожую с функцией-конструктором в JavaScript: оба служат шаблоном, из которого можно создавать экземпляры подобных объектов по мере необходимости. В то же время в таких языках, как Python и Ruby, класс выступает заменой типов, используемых в других языках. Мы показываем пример класса ниже.

Каковы плюсы и минусы ООП?

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

Преимущество: инкапсуляция

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

# list to hold the passengers
bus_passengers = []
# maximum number of passengers
bus_capacity = 12
# add another passenger
def take_bus(passenger)
  if len(bus_passengers) < bus_capacity:
    bus_passengers.append(passenger)
  else:
    raise Exception("Bus is full")

Код работает, но проблематично. Функция take_bus обращается к переменным bus_passengers и bus_capacity, не передавая их в качестве аргументов. Это вызывает проблемы с обширным кодом, поскольку переменные должны быть либо предоставлены глобально, либо переданы при каждом вызове. Также можно «обмануть». Мы можем продолжать добавлять пассажиров в автобус, даже если он на самом деле заполнен:

# bus is full
assert len(bus_passengers) == bus_capacity
# will raise exception, won't add passenger
take_bus(passenger)
# we cheat, adding an additional passenger directly
bus_passengers.append(passenger)
# now bus is over capacity
assert len(bus_passengers) > bus_capacity

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

# can't do this in reality
bus_capacity += 1

Инкапсуляция внутреннего состояния объектов защищает от бессмысленных или нежелательных изменений. Здесь тот же функционал в объектно-ориентированном коде. Мы определяем класс шины и создаем экземпляр шины с ограниченной пропускной способностью. Добавление пассажиров возможно только через соответствующий метод:

class Bus():
  def __init__(self, capacity):
    self._passengers = []
    self._capacity = capacity
  
  def enter(self, passenger):
    if len(self._passengers) < self._capacity:
      self._passengers.append(passenger)
      print(f"{passenger} has entered the bus")
    else:
      raise Exception("Bus is full")
# instantiate bus with given capacity
bus = Bus(2)
bus.enter("Jack")
bus.enter("Jim")
# will fail, bus is full
bus.enter("John")

Преимущество: системы моделирования

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

Читайте также:  Добавление и выполнение хранимых процедур в ADO.NET и MS SQL Server — подробное пошаговое руководство

Наследование через иерархию классов, встречающееся во многих языках ООП, также отражает модели человеческого мышления. Проиллюстрируем последний пункт на примере. Животное — это абстрактное понятие. Животные, которые действительно появляются, всегда являются конкретным выражением вида. В зависимости от вида животные имеют разные характеристики. Собака не может ни лазить, ни летать, поэтому она ограничена в передвижении в двухмерном пространстве:

# abstract base class
class Animal():
  def move_to(self, coords):
    pass
# derived class
class Dog(Animal):
  def move_to(self, coords):
    match coords:
      # dogs can't fly nor climb
      case (x, y):
        self._walk_to(coords)
# derived class
class Bird(Animal):
  def move_to(self, coords):
    match coords:
      # birds can walk
      case (x, y):
        self._walk_to(coords)
      # birds can fly
      case (x, z, y):
        self._fly_to(coords)

Недостатки объектно-ориентированного программирования

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

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

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

Какие языки программирования поддерживают или подходят для ООП?

Почти все мультипарадигмальные языки поддаются объектно-ориентированному программированию. К ним относятся известные в Интернете языки программирования PHP, Ruby, Python и JavaScript. Напротив, принципы ООП в значительной степени несовместимы с реляционной алгеброй, лежащей в основе SQL. Специальные слои перевода, известные как Object Relational Mappers (ORM), используются для преодоления несоответствия импеданса.

Даже чисто функциональные языки, такие как Haskell, обычно не имеют встроенной поддержки ООП. Реализация ООП на C требует больших усилий. Интересно, что Rust — современный язык, которому не нужны классы. Вместо этого struct и enum используются как структуры данных, поведение которых определяется с помощью ключевого слова impl. Для группировки поведения можно использовать так называемые черты; наследование и полиморфизм также отображаются таким образом.

Оцените статью
Блог о программировании
Добавить комментарий