Объектно-ориентированное программирование основано на четырех основных принципах:
Абстракция: абстрактное поведение объектов обобщается в классах
Инкапсуляция данных: свойства и методы инкапсулируются в виде классов и скрыты от внешнего доступа.
Наследование: свойства и методы могут быть унаследованы одним классом от другого класса
Полиморфизм: множество форм - объекты могут принимать различные формы в зависимости от их использования
«В объектно-ориентированном программировании класс представляет своего рода образец объектов, шаблон, на основе которого могут быть созданы отдельные экземпляры (объекты) во время выполнения программы. Внутри класса разработчик определяет свойства и методы, которыми должны обладать отдельные экземпляры объекта. Свойства представляют состояние экземпляров объекта, методы и их поведение.
Можно еще провести следующую аналогию. У нас у всех есть некоторое представление о человеке, у которого есть имя, возраст, какие-то другие характеристики. Совокупность подобных характеристик можно назвать шаблоном человека или классом. Конкретное воплощение этого шаблона может отличаться, например, одни люди имеют одно имя, другие - другое имя. И реально существующий человек (фактически экземпляр данного класса) будет представлять объект этого класса.
Существуют классические объектно-ориентированные языки как 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