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

Последнее обновление: 24.11.2023

Объектно-ориентированное программирование основано на четырех основных принципах:

  • Абстракция: абстрактное поведение объектов обобщается в классах

  • Инкапсуляция данных: свойства и методы инкапсулируются в виде классов и скрыты от внешнего доступа.

  • Наследование: свойства и методы могут быть унаследованы одним классом от другого класса

  • Полиморфизм: множество форм - объекты могут принимать различные формы в зависимости от их использования

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

Можно еще провести следующую аналогию. У нас у всех есть некоторое представление о человеке, у которого есть имя, возраст, какие-то другие характеристики. Совокупность подобных характеристик можно назвать шаблоном человека или классом. Конкретное воплощение этого шаблона может отличаться, например, одни люди имеют одно имя, другие - другое имя. И реально существующий человек (фактически экземпляр данного класса) будет представлять объект этого класса.

Существуют классические объектно-ориентированные языки как Java или C#. Также есть языки, который в той или иной мере применяют ООП, но чисто объектно-ориентированными языками не являются, как например, JavaScript.

Рассмотрим вкратце ООП на примере Java и JavaScript.

Абстракция

В объектно-ориентированном программировании основу или асбстракцию для объектов определяют классы. Классы содержат общее состояние и поведение объектов. Например, нам надо представить в программе человека. В большинстве языков программирования для определения классов применяется ключевое слово class. Так, в Java мы могли бы определить следующий класс Person, который представляет человека:

class Person{ }

Аналогичное определение класса человека в JavaScript:

class Person{ }

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

// класс человека
class Person{ 
    String name;    // имя человека
    int age;       // возраст человека
}

Здесь переменная name представляет тип String (строку) и хранит имя человека. Переменная age представляет тип int или число и хранит возраст.

Аналогичное определение в JavaScript:

class Person{

    name;   // имя
    age;    // возраст
}

Человек может производить некоторые действия. Например, человек может идти, спать, питаться и т.д. Это то, что называется поведение объекта. В контексте нашей программы пусть поведение человека ограничено тем, что он говорит, как его зовут и сколько ему лет. Для определения поведения/действий класса определяются методы. Например, добавим в класс Person метод say, с помощью которого человек будет сообщать информацию о себе. Пример на Java:

// класс человека
class Person{ 
    String name;    // имя человека
    int age;       // возраст человека
    // человек сообщает информацию о себе
    public void say(){
        System.out.printf("Меня зовут %s \n", name); 
        System.out.printf("Мне %d лет \n", age);
    }
}

В методе say просто выводим на консоль с помощью метода System.out.printf() данные полей класса.

Аналогичный пример для JavaScript:

class Person{

    name;   // имя
    age;    // возраст
    say(){
        console.log("Меня зовут", this.name);
        console.log("Мне", this.age, "лет");
    }
}

Таким образом, мы определили в классе Person состояние (поля name и age) и поведение (метод say). Теперь мы можем создавать объекты класса Person - конкретных людей, которые будут обладать подобным состоянием и поведением. Обычно для создания объектов применяется конструктор - специальный метод, который выполняет инициализацию объекта. Во многих языках можно использовать конструктор по умолчанию, а можно определить свой. Например, мы хотим, чтобы при создании у объектов (конкретных людей) уже были установлены имя и возраст. Для этого добавим в класс конструктор. В Java метод конструктора называется по имени класса:

class Person{ 
    String name;    // имя человека
    int age;       // возраст человека

    public Person(String name, int age){
        this.name=name;
        this.age = age;
    }

    public void say(){
        System.out.printf("Меня зовут %s \n", name); 
        System.out.printf("Мне %d лет \n", age);
    }
}

Здесь конструктор получает извне через два параметра name и age значения для одноименных переменных.

В JavaScript для определения конструктора применяется метод со специальным именем constructor:

class Person{

    name;   // имя
    age;    // возраст
    constructor(name, age){
        this.name = name;
        this.age = age;
    }
    say(){
        console.log("Меня зовут", this.name);
        console.log("Мне", this.age, "лет");
    }
}

Таким образом, у нас готова асбтракция человека в виде класса Person, и мы ее можем использовать для создания объекта данного класса. Например, создание объектов в Java:

public class Program{
    public static void main(String[] args) {
            
        // создаем объект класса Person
        Person tom = new Person("Tom", 39);
        // обращаемся к методу say объекта
        tom.say();

        // создаем второй объект класса Person
        Person sam = new Person("Sam", 25);
        // обращаемся к методу say объекта
        sam.say();
    }
}
// класс человека
class Person{ 
    String name;    // имя человека
    int age;       // возраст человека

    public Person(String name, int age){
      
        this.name=name;
        this.age = age;
    }

    public void say(){
        System.out.printf("Меня зовут %s \n", name); 
        System.out.printf("Мне %d лет \n", age);
    }
}

Аналогичная программа на JavaScript:

class Person{

    name;   // имя
    age;    // возраст
    constructor(name, age){
        this.name = name;
        this.age = age;
    }
    say(){
        console.log("Меня зовут", this.name);
        console.log("Мне", this.age, "лет");
    }
}
// создаем объект класса Person
const tom = new Person("Tom", 39);
// обращаемся к методу say объекта
tom.say();

// создаем второй объект класса Person
const sam = new Person("Sam", 25);
// обращаемся к методу say объекта
sam.say();

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

В обоих случаях мы получим один и тот же консольный вывод.

Меня зовут Tom 
Мне 39 лет 
Меня зовут Sam 
Мне 25 лет 

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

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

const tom = new Person("Tom", 39);
tom.age = 12345;

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

В таких языках программирования, как Java, можно использовать специальные ключевые слова - модификаторы доступа, чтобы управлять доступом к полям класса извне. Например, ключевое слово private при применении к полям/методам класса делает из нелоступными извне:

class Person{ 
    private String name;    // приватное поле доступно только внутри класса
    private int age;       // приватное поле доступно только внутри класса

    public Person(String name, int age){
        this.name=name;
        this.age = age;
    }

    public void say(){
        System.out.printf("Меня зовут %s \n", name); 
        System.out.printf("Мне %d лет \n", age);
    }
}
public class Program{
    public static void main(String[] args) {
            
        Person tom = new Person("Tom", 39);
        tom.age = 12345;    // ! Ошибка  - поле age не доступно вне класса Person
    }
}

В других языках для скрытия состояния и поведения класса извне могут применяться другие инструменты. Так, JavaScript можно закрыть поля и методы внутри класса можно, поместив в начало названия полей/методов символ решетки #:

class Person{

    #name;   // имя
    #age;    // возраст
    constructor(name, age){
        this.#name = name;
        this.#age = age;
    }
    say(){
        console.log("Меня зовут", this.#name);
        console.log("Мне", this.#age, "лет");
    }
}

const tom = new Person("Tom", 39);
tom.#age = 12345; // ! Ошибка  - свойство #age не доступно вне класса Person

Тем не менее даже к инкапсулированному состоянию может потребоваться доступ. Например, мы хотим устанавливать для свойства age новые значения, если они представляют корректный возраст. Или мы хотим получать значение свойства name.

Для этого в классе обычно определяются специальные методы доступа, которые опосредуют доступ к приватным полям. Например, в Java:

public class Program{
    public static void main(String[] args) {
            
        Person tom = new Person("Tom", 39);
        System.out.println(tom.getName());  // Tom 
        System.out.println(tom.getAge());  // 39 
        tom.setAge(22);
        System.out.println(tom.getAge());  // 22
        tom.setAge(1222);
        System.out.println(tom.getAge());  // 22
    }
}
// класс человека
class Person{ 
    private String name;    // имя человека
    private int age;       // возраст человека

    public Person(String name, int age){
      
        this.name=name;
        this.age = age;
    }
    //  метод для получения имени
    public String getName(){ return name; }
    //  метод для получения возраста
    public int getAge(){return age;}
    // метод для установки возраста 
    public void setAge(int value){
        // если переданное значение представляет валидный возраст, изменяем age
        if(value > 0 && value < 110) age = value;
    }

    public void say(){
        System.out.printf("Меня зовут %s \n", name); 
        System.out.printf("Мне %d лет \n", age);
    }
}

Здесь для получения имени определен метод getName, для получения возраста - метод getAge, а для установки возраста - метод setAge. Причем метод setAge изменяет возраст, если он представляет валидное значение (от 1 до 109). Для поля name также можно было бы определить метод для установки значения, но в данном случае предположим, что свойство name будет доступно только для чтения (в реальной жизни не так уж часто меняют имя).

В данном случае методы getName/getAge/setAge еще называются методами доступа. Методы getName/getAge называются "геттерами" (getters), так как они получают значение, а метод setAge - сеттером (setter), так как он устанавливает значение.

В разных языках для определения геттеров и сеттеров могут быть специальные конструкции. Например, в JavaScript есть специальные конструкции get и set:

class Person{

    #name;   // имя
    #age;    // возраст
    constructor(name, age){
        this.#name = name;
        this.#age = age;
    }
    get name(){     // геттер для имени
        return this.#name;
    }
    set age(value){ // сеттер для возраста
        if(value > 0 && value < 110) this.#age = value;
    }
    get age(){      // геттер для возраста
        return this.#age;
    }
    say(){
        console.log("Меня зовут", this.#name);
        console.log("Мне", this.#age, "лет");
    }
}

const tom = new Person("Tom", 39);
console.log(tom.name);  // Tom 
console.log(tom.age);  // 39 
tom.age = 22;
console.log(tom.age);  // 22
tom.age =1222;
console.log(tom.age);  // 22

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

Третий принцип ООП представляет наследование, который предполагает, что один класс может наследовать от другого поля/свойства и методы. Например, выше мы определили класс человека. Теперь определим класс работника некоторой компании. Работник по сути имеет те же признаки, что и Person, - имя и возраст и то же поведение, добавляя ним свои дополнительные - например, название компании, где работает работник. В этом случае мы можем создать класс работника и унаследовать его от класса Person. В различных языках для установки наследования между классами могут применяться различные операторы. В Java для этого применяется оператор extends:

public class Program{
    public static void main(String[] args) {
            
        Employee sam = new Employee("Sam", 25, "Google");
        sam.say();
    }
}
// класс человека
class Person{ 
    private String name;    // имя человека
    private int age;       // возраст человека

    public Person(String name, int age){
      
        this.name=name;
        this.age = age;
    }

    public void say(){
        System.out.printf("Меня зовут %s \n", name); 
        System.out.printf("Мне %d лет \n", age);
    }
}
// Employee унаследован от класса Person
class Employee extends Person{

    private String company;     //  компания работника
    public Employee(String name, int age, String company){
        super(name, age);   //  вызываем конструктор класса Person
        this.company = company;
    }
    // переопределяем метод say
    @Override
    public void say(){
        super.say();    // вызываем метод say из класса Person
        System.out.printf("Я работаю в %s \n", company);
    }
} 

Здесь определен класс Employee, который наследуется от класса Person и который добавляет поле company для хранения названия компании. В этом отношении класс Person еще называется базовым классом, родительским классом, суперклассом. А класс Employee - классом-наследником, производным классом, подклассом.

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

super(name, age);

Кроме того, класс-наследник обычно может не просто унаследовать функционал, но и переопределить его при необходимости. В данном случае переопределяем метод say, для чего к нему в Java применяется аннотация @Override. Например, мы хотим кроме имени и возраста выводить также и компанию работника:

@Override
public void say(){
    super.say();    // вызываем метод say из класса Person
    System.out.printf("Я работаю в %s \n", company);
}

Здесь сначала обращаемся к методу say базового класса Person - super.say(). Это, во-первых, позволяет избежать ненужного дублирования кода (не надо писать строки для вывода на консоль имени и возраста). Во-вторых, поля name и age в Person - приватные и поэтому в классах-наследниках недоступны. Для обращения к функционалу базового класса в Java также используется слово super

Аналогичная программа на JavaScript:

class Person{

    #name;   // имя
    #age;    // возраст
    constructor(name, age){
        this.#name = name;
        this.#age = age;
    }
    say(){
        console.log("Меня зовут", this.#name);
        console.log("Мне", this.#age, "лет");
    }
}
// класс Employee унаследован от Person
class Employee extends Person{

    #company; // компания работника
    constructor(name, age, company){
        super(name, age);   // вызываем конструктор базового класса Person
        this.#company = company;
    }
    say(){
        super.say();    // вызов реализации метода say из класса Person
        console.log("Я работаю в", this.#company);
    }
}

const sam = new Employee("Sam", 25, "Google");
sam.say();

Консольный вывод программ на обоих языках будет идентичен:

Меня зовут Sam 
Мне 25 лет 
Я работаю в Google 

Полиморфизм

Полиморфизм представляет способность объектов принимать другой тип (форму) или представлять себя как другой тип в зависимости от контекста или их использования. Применение к классам наследования позволяет установить связь "является". То есть работник является человеком или объект Employee является Person. Соответственно, если, к примеру, функция ожидает объект типа Person, то в качестве аргументов могут быть переданы как объекты Person, так и объекты Employee:

Так, определим следующую программу на Java:

public class Program{
    public static void main(String[] args) {
            
        Person tom = new Person("Tom", 39);
        Employee sam = new Employee("Sam", 25, "Google");
        printPerson(tom);
        printPerson(sam);
       
    }
    public static void printPerson(Person person){
        person.say();
        System.out.println();
    }
}
// класс человека
class Person{ 
    private String name;    // имя человека
    private int age;       // возраст человека

    public Person(String name, int age){
      
        this.name=name;
        this.age = age;
    }

    public void say(){
        System.out.printf("Меня зовут %s \n", name); 
        System.out.printf("Мне %d лет \n", age);
    }
}
// Employee унаследован от класса Person
class Employee extends Person{

    private String company;     //  компания работника
    public Employee(String name, int age, String company){
        super(name, age);   //  вызываем конструктор класса Person
        this.company = company;
    }
    // переопределяем метод say
    @Override
    public void say(){
        super.say();    // вызываем метод say из класса Person
        System.out.printf("Я работаю в %s \n", company);
    }
} 

Здесь метод printPerson принимает объект Person:

public static void printPerson(Person person){
    person.say();
    System.out.println();
}

Но так как объект Employee - это тоже объект Person, то в эту функцию также можно передать объект Employee

Person tom = new Person("Tom", 39);
Employee sam = new Employee("Sam", 25, "Google");
printPerson(tom);
printPerson(sam);

Для JavaScript в силу слабой типизации принцип полиморфизма меньше характерен, так как параметру функции мы можем передать любой объект:

class Person{

    #name;   // имя
    #age;    // возраст
    constructor(name, age){
        this.#name = name;
        this.#age = age;
    }
    say(){
        console.log("Меня зовут", this.#name);
        console.log("Мне", this.#age, "лет");
    }
}
class Employee extends Person{

    #company; // компания работника
    constructor(name, age, company){
        super(name, age);   // вызываем конструктор базового класса Person
        this.#company = company;
    }
    say(){
        super.say();    // вызов реализации метода say из класса Person
        console.log("Я работаю в", this.#company);
    }
}

function printPerson(person){
    person.say();
    console.log("\n");
}
const tom = new Person("Tom", 39);
const sam = new Employee("Sam", 25, "Google");
printPerson(tom);
printPerson(sam);

Консольный вывод программ на обоих языках:

Меня зовут Tom 
Мне 39 лет 

Меня зовут Sam 
Мне 25 лет 
Я работаю в Google 
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850