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

Сравним процедурный подход и ООП

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

Давайте сразу рассмотрим пример на JavaScript.

const firstName = "Иван";
const lastName = "Иванов";

function getFullName(firstName, lastName) {
  return firstName + " " + lastName;
}

getFullname(firstName, lastName);

В данном примере функция getFullName вернет нам полное имя — Иван Иванов.

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

В ООП вводятся два фундаментальных понятия: класс и объект. Класс — это некоторое описание характеристик объекта. Объект — это конкретный экземпляр класса. Каждый объект имеет свойства и методы. В контексте ООП свойства —  это характеристики объекта, а методы — это действия, которые он может совершать. Рассмотрим их на простом примере.

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

Теперь я возьму свою кошку. Ее зовут Софа, она сиамской породы, у нее голубые глаза, а характер очень ревнивый. В данном случае Софа — это конкретный экземпляр класса кошки.

Ниже пример класса, созданного на языке JavaScript:

class User {
  firstName;
  lastName;
  age;

  constructor(firstName, lastName, age) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }

  getFullName() {
    return this.firstName + " " + this.lastName;
  }

  isAdult() {
    if (this.age >= 18) {
      return true;
    }
  }
}

Создали класс User, который имеет свойства:

  1. firstName — имя
  2. lastName — фамилия
  3. age — возраст

Для класса мы получаем полное имя методом getFullName, а также определяем, взрослый ли перед нами человек — метод isAdult.

Теперь создадим конкретного пользователя Василия Ямщиков, ему 28 лет.

const vasya = new User("Василий", "Ямщиков", 28);

vasya.getFullName();
vasya.isAdult();

Получим полное имя — Василий Ямщиков, скрипт определит его как взрослого (вернет true).

Инкапсуляция в ООП

Суть инкапсуляции в том, что класс является своего рода капсулой, который содержит в себе свойства и методы. Мы не задумываемся о том, что внутри класса, а просто используем публичные методы и свойства для работы с ним. Это похоже на вождение автомобиля. Чтобы им управлять достаточно понимать базовые концепции вождения (как завести, разогнаться, затормозить и т.д.). При этом вовсе не обязательно детально знать его внутреннее устройство.

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

Для того, чтобы делать методы публичными или приватными существуют модификаторы доступы: public и private соответственно.

Рассмотрим большой пример на TypeScript:

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

class Person {
  private _firstName;
  private _lastName;
  private _age;
  private _ID: number;
  private _hobbies: Array<string>;

  constructor(firstName: string, lastName: string, age: number) {
    this._firstName = firstName;
    this._lastName = lastName;
    if (age >= 0) {
      this._age = age;
    } else {
      this._age = 0;
    }
    this._ID = Math.floor(Math.random() * 100);
    this._hobbies = [];
  }

  public get fullName() {
    return this._firstName + " " + this._lastName;
  }

  public get age() {
    return this._age;
  }

  public set age(count: number) {
    this._age += count;
  }

  public get ID() {
    return this._ID;
  }

  public set addHobbie(hobbie: string) {
    this._hobbies.push(hobbie);
  }

  public get hobbies() {
    let userHobbie: string;
    if (this._hobbies.length === 0) {
      userHobbie = "Нет увлечений";
    } else {
      userHobbie = "Увлечения " + this._firstName + ": ";
      this._hobbies.forEach((hobbie) => {
        userHobbie += hobbie + " ";
      });
    }
    return userHobbie;
  }
}

Тут мы создаем класс Person, который описывает какого-то человека. Данный класс имеет приватные свойства:

  1. _firstName — имя
  2. _lastName — фамилия
  3. _age — возраст
  4. _ID — идентификационный номер
  5. _hobbie — список хобби

При создании объекта user на основе класса Person мы единожды с помощью конструктора задаем четыре первых свойства. Имя, фамилия и возраст передается в качестве аргументов, а ID генерируем прямо внутри класса.

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

Примеры таких методов:

  1. user.fullName — геттер для получения полного имени
  2. user.ID — геттер для получения ID
  3. user.age — геттер для получения возраста
  4. user.age = 1 — сеттер для увеличения возраста на один год
  5. user.hobbies — геттер для получения хобби
  6. user.addHobbie = «дача» — сеттер для добавления хобби

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

Наследование позволяет создавать новый классы на основе существующего. Класс, на базе которого создается новый, называется родительским. А сам новый класс — потомком. Рассмотрим на примере.

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

  1. Безалкогольный или алкогольный?
  2. Тип упаковки (стеклянная, пластиковая, картонная)
  3. Объем в литрах

А вот на основе класса Drinks мы создадим класс для сока — Juice. Класс Juice имеет свойства:

  1. Вкус (яблоко, персик и т.д.)
  2. Процент содержания сока
  3. Подходит для детского питания?

Можно сказать что класс напитков Dirnks, расширяет класс продуктов Product. В свою очередь класс соков Juice расширяет класс напитков.

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

class Product {
  private _ID: number;
  private _price: number;

  constructor(price: number) {
    this._price = price;
    this._ID = Math.floor(Math.random() * 100);
  }

  public get ID() {
    return this._ID;
  }

  public get price() {
    return this._price;
  }

  public set price(sum: number) {
    if (sum > 0) {
      this._price = sum;
    } else {
      console.log("Ошибка! Цена не может быть равна 0 и меньше");
    }
  }
}

class Drinks extends Product {
  private _isAlcohol: boolean;
  private _packType: string;
  private _volume: number;

  constructor(
    price: number,
    isAlcohol: boolean,
    packType: string,
    volume: number
  ) {
    super(price);
    this._isAlcohol = isAlcohol;
    this._packType = packType;
    this._volume = volume;
  }
}

class Juice extends Drinks {
  private _taste: string;
  private _juiceRatio: number;
  private _isBabyJuice: boolean;

  constructor(
    price: number,
    isAlcohol: boolean,
    packType: string,
    volume: number,
    taste: string,
    juiceRatio: number,
    isBabyJuice: boolean
  ) {
    super(price, isAlcohol, packType, volume);
    this._taste = taste;
    this._juiceRatio = juiceRatio;
    this._isBabyJuice = isBabyJuice;
  }
}

const appleJuice = new Juice(100, false, "картонная", 2, "яблоко", 80, true);

Пример выше показывает, как реализовать класс Juice на основе Drinks и Product на языке TypeScript. Для наследования используется ключевое слово extends. В начале указываем какой новый класс хотим создать, а в конце от какого класса наследуем. В конце кода создаем объект яблочный сок, который имеет свойства:

  1. Стоимость: 100 рублей
  2. Безалкогольный напиток
  3. Тип упаковки: картонная
  4. Объем: 2 литра
  5. Вкус: яблоко
  6. Содержание сока: 80 %
  7. Подходит для детского питания

Все методы из родительского класса — доступы и в потомке. Если мы захотим узнать артикул и цену сока, то воспользуемся геттерами из класса Product.

appleJuice.ID; // получим ID: 24
appleJuice.price; // получим  цену: 100 руб.

Полиморфизм в ООП

Полиморфизм — это способность объекта принимать множество форм. Если прямо вникать в название (poly — много, morf — форм), то можно догадаться, что это что-то многообразное. В ООП полиморфизм позволяет одному и тому же коду работать с разными типами данных. По традиции обратимся еще раз к примеру.

Полиморфизм бывает двух видов:

  1. Мнимый (ad-hoc) — основывается на различении типов
  2. Истинный (параметрический)

Ad-hoc полиморфизм

Ad-hoc является самым простым типом полиморфизма. Он связан с тем, какие типы данных мы передаем в функцию в качестве аргументов. Вот пример на JavaScript:

// Ad-hoc полиморфизм

class Calc {
  sum(x: number, y: number): number {
    return x + y;
  }
  sum(x: string, y: string): string {
    return x + y;
  }
}

let myCalc = new Calc();

myCalc.sum(4, 3); // вернет 3
myCalc.sum("cat", "dog"); // вернет catdog

В примере выше мы используем перезагрузку методов. У нас два метода sum с одинаковым названием, при этом в первом случае он вернет сумму двух чисел, а во втором «склеит» две строки между собой.

Параметрический полиморфизм

Возьмем наш продуктовый магазин из раздела про наследование. Для родительского класса Product создали метод info, чтобы узнать информацию о продукте (ID и цену). Далее создали три объекта на основе созданных классов и вызвали для них один и тот же метод info. Данный метод сработал для все объектов, хотя объявлялся только в Product.

// Параметрический полиморфизм

class Product {
  // код
  // функция для информации о продукте
  info() {
    console.log(`Продукт с ID: ${this._ID} и ценой: ${this._price} руб.`);
  }
}

class Drinks extends Product {
  // код
}

class Juice extends Drinks {
  // код
}

// Создаем объекты
const myProduct = new Product(500);
const myDrinks = new Drinks(300, true, "стеклянная", 0.5);
const myJuice = new Juice(100, false, "картонная", 2, "яблоко", 80, true);

myProduct.info(); // Продукт с ID: 49 и ценой: 500 руб.
myDrinks.info(); // Продукт с ID: 93 и ценой: 300 руб.
myJuice.info(); // Продукт с ID: 65 и ценой: 100 руб.

Все работает, но хотелось, чтобы для напитков и соков выводился не просто «Продукт», а соответственно «Напиток» и «Сок». Для этого переопределим наследуемые классы.

class Drinks extends Product {
  // код
  // переопределяем метод
  info() {
    console.log(`Напиток с ID: ${this.ID} и ценой: ${this.price} руб.`);
  }
}

class Juice extends Drinks {
  // код
  // переопределяем метод
  info() {
    console.log(`Сок с ID: ${this.ID} и ценой: ${this.price} руб.`);
  }
}

myProduct.info(); // Продукт с ID: 49 и ценой: 500 руб.
myDrinks.info(); // Напиток с ID: 93 и ценой: 300 руб.
myJuice.info(); // Сок с ID: 65 и ценой: 100 руб.

Таким образом одна и та же функция info для разных классов работает по разному. В нашем примере меняется только вывод в консоль, но на практике может быть логика куда сложнее.

 

 

Комментарии

0

Без регистрации и смс