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

В этой статье я постараюсь познакомить вас с этой концепцией в более практичной форме, без животных и автомобилей.

Итак, рассмотрим следующую гипотетическую ситуацию: нам поручено разработать программное обеспечение для парковки.

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

public class Vehicle {
  private String identifier;
  private LocalDateTime entryDate;
  private LocalDateTime exitDate;
  protected float price = 5.0f; // hypothetical hour price

  public Vehicle(String identifier) {
    this.identifier = identifier;
    this.entryDate = LocalDateTime.now();
  }

  public String getIdentifier() {
    return this.identifier;
  }

  public double calculatePrice() {
    this.exitDate = LocalDateTime.now();
    return Math.abs(Duration.between(entryDate, exitDate).toHours()) * price;
  }
}

В приведенном выше коде мы передаем номер автомобиля (идентификатор) в конструктор, и время входа определяется автоматически. Инициализация времени входа сама по себе описывает первое преимущество ООП, к которому я здесь подойду.

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

Это искусство скрывать/защищать любую информацию, которую необходимо защитить, полностью контролируемую кодом внутри класса. В этом конкретном случае мы не хотим, чтобы пользователи сами передавали дату, это может быть обработано кодом. Это происходит снова при вызове метода «calculatePrice», так как цена будет рассчитана при вызове метода.

Инкапсуляция тесно связана с другой концепцией ООП:

Абстракция

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

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

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

Хорошо, но как мы можем использовать этот код? Что ж, давайте создадим новый автомобиль! Вы помните единственный параметр, необходимый для создания Транспортного средства?

public class Main {
  public static void main(String[] args) {

    Vehicle vehicle = new Vehicle("DREAMCAR");

    // after 3 hours...
    double totalPrice = vehicle.calculatePrice();
    System.out.println(totalPrice); // 15.0
  }
}

В этом основном методе мы создаем экземпляр класса «Автомобиль» с идентификатором «DREAMCAR». Теперь у нас есть объект, хранящийся в переменной с именем «транспортное средство». Когда мы это делаем, время входа автоматически устанавливается внутри объекта.

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

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

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

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

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

Допустим, владелец хочет принимать грузовики на стоянке. Нужно ли нам писать новый класс со всеми предыдущими функциями? Точно нет.

Мы можем просто создать новый класс Truck и наследовать все функции от Vehicle, расширив его:

public class Truck extends Vehicle {
  public Truck(String identifier) {
    super(identifier);
    super.price = 10.0f;
  }
}

Теперь у нас есть класс Truck со всеми атрибутами и методами Vehicle, и мы можем создать его экземпляр так же, как и с Vehicle:

Truck truck = new Truck("MONSTERTRUCK");

// after 3 hours...
double totalPrice = truck.calculatePrice();
System.out.println(totalPrice); // 30.0

Вы помните, что в классе Vehicle поле «цена» было описано как «защищенное»? Это означает, что только классы в одном пакете могут получить к нему прямой доступ. Это делает недоступным снаружи пакета изменение его значения, но позволяет нам изменять цену для других типов транспортных средств, так как транспортные средства находятся в том же пакете, что и «Транспортное средство».

Возможность изменить это значение знакомит нас со следующей темой.

Полиморфизм

От греческого слова, означающего «многообразный», именно этого и следовало ожидать.

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

Зная это, мы могли бы просто создать список и добавить объект любого типа, поскольку его класс наследуется от транспортного средства, как показано ниже:

List<Vehicle> parkingLot = new ArrayList<>();
parkingLot.add(new Vehicle("MyVehicle"));
parkingLot.add(new Truck("MyBigTruck"));

Теперь владелец хочет принимать велосипеды на стоянке. Но если быть честным (или просто что-то изменить 😁), он хочет взимать фиксированную сумму в размере 7 долларов за время парковки велосипеда. Это может означать, что нам нужно написать новый класс только для велосипеда, но мы этого не делаем!

Переопределение

Одним из преимуществ полиморфизма является переопределение метода подкласса. В данном случае мы хотим изменить функциональность метода «calculatePrice». Позвольте мне показать вам, как:

public class Bicycle extends Vehicle{
    public Bicycle(String identifier) {
        super(identifier);
        super.price = 7.0f;
    }

    @Override
    public float calculatePrice() {
        return this.price;
    }
}

Переопределяя его, мы можем сказать, что это должно просто возвращать фиксированную цену, указанную в классе, и это сохраняет тип транспортного средства и все другие функции:

Bicycle bicycle = new Bicycle("BMX");

// after n hours...
double totalPrice = bicycle.calculatePrice();
System.out.println(totalPrice); // 7.0

Перегрузка

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

Им нужно иметь возможность указать время выхода при расчете цены, поэтому у нас есть два варианта:

  • Напишите совершенно новый метод с другим именем и нужными параметрами;
  • Или мы можем просто перегрузить существующий, чтобы мы могли использовать то же имя метода при его вызове.
public float calculatePrice() {
  this.exitDate = LocalDateTime.now();
  return Math.abs(Duration.between(entryDate, exitDate).toHours()) * price;
}

public float calculatePrice(LocalDateTime exitDate) {
  this.exitDate = exitDate;
  return Math.abs(Duration.between(entryDate, exitDate).toHours()) * price;
}

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

Vehicle vehicle = new Vehicle("DREAMCAR");
// after 3:05 hours...
double totalPrice = vehicle.calculatePrice(LocalDateTime.now().minusMinutes(10));
System.out.println(totalPrice); // 10.0

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

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

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

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

Есть сомнения/вопросы/предложения? Комментарий ниже.