JavaRush /Java блог /Random /Кофе-брейк #90. 4 столпа объектно-ориентированного програ...

Кофе-брейк #90. 4 столпа объектно-ориентированного программирования

Статья из группы Random
Источник: The Geek Asian Давайте взглянем на четыре основы объектно-ориентированного программирования и попытаемся понять принципы их работы. Объектно-ориентированное программирование (ООП) — одна из основных парадигм программирования. Оно может быть легким и простым или наоборот — очень сложным. Все зависит от того, как вы решили разработать свое приложение. Кофе-брейк #90. 4 столпа объектно-ориентированного программирования - 1Существует 4 столпа ООП:
  1. Инкапсуляция.
  2. Наследование.
  3. Абстракция.
  4. Полиморфизм.
Теперь мы обсудим каждый из них с кратким объяснением и реальным примером кода.

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

Все мы изучали инкапсуляцию как сокрытие элементов данных и предоставление пользователям доступа к данным с помощью общедоступных методов. Мы это называем геттерами и сеттерами. А сейчас забудем об этом и найдем более простое определение. Инкапсуляция — это метод ограничения пользователя от прямого изменения элементов данных или переменных класса для поддержания целостности данных. Как мы это делаем? Мы ограничиваем доступ к переменным, переключая модификатор доступа на private и открывая публичные методы, которые можно использовать для доступа к данным. Давайте посмотрим на конкретные примеры ниже. Это поможет нам понять, как мы можем использовать инкапсуляцию для поддержания целостности данных. Без инкапсуляции:

/**
 * @author thegeekyasian.com
 */
public class Account {

  public double balance;
  
  public static void main(String[] args) {
  	
  	Account theGeekyAsianAccount = new Account();
  	
  	theGeekyAsianAccount.balance = -54;
  }
}
В приведенном выше фрагменте кода метод main() обращается к переменной balance напрямую. Это позволяет пользователю установить любое двойное значение для переменной balance класса Account. Мы можем потерять целостность данных, позволив любому установить значение balance как любое недопустимое число, например -54 в данном случае. С инкапсуляцией:

/**
 * @author thegeekyasian.com
 */
public class Account {

  private double balance;

  public void setBalance(double balance) {

    if(balance >= 0) { // Validating input data in order to maintain data integrity
	  this.balance = balance;
    }

    throw new IllegalArgumentException("Balance cannot be less than zero (0)");
  }
  
  public static void main(String[] args) {
  	
  	Account theGeekyAsianAccount = new Account();
  	
  	theGeekyAsianAccount.setBalance(1); // Valid input - Allowed
  	theGeekyAsianAccount.setBalance(-55); // Stops user and throws exception
  }
}
В этом коде мы ограничили доступ к переменной balance и добавили метод setBalance(), который дает возможность пользователям устанавливать значение balance для Account. Установщик проверяет предоставленное значение перед присвоением его переменной. Если значение меньше нуля, генерируется исключение. Это гарантирует, что целостность данных не будет нарушена. После объяснения приведенных выше примеров я надеюсь, что ценность инкапсуляции как одного из четырех столпов ООП очевидна.

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

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

/**
 * @author thegeekyasian
 */
public class Rectangle {

  private int width;
  private int height;

  public Rectangle(int width, int height) {
	this.width = width;
	this.height = height;
  }

  public int getArea() {
	return width * height;
  }
}

public class Square {

  private int width; // Duplicate property, also used in class Rectangle

  public Square(int width) { 
	this.width = width;
  }

  public int getArea() { // Duplicate method, similar to the class Rectangle
	return this.width * this.width;
  }
}
Два схожих класса имеют общие свойства width и метод getArea(). Мы можем увеличить возможность повторного использования кода, выполнив небольшой рефакторинг, когда класс Square в конечном итоге наследует класс Rectangle. С наследованием:

/**
 * @author thegeekyasian
 */
public class Rectangle {

  private int width;
  private int height;

  public Rectangle(int width, int height) {
	this.width = width;
	this.height = height;
  }

  public int getArea() {
	return width * height;
  }
}

public class Square extends Rectangle {

  public Square(int width) {
	super(width, width); // A rectangle with the same height as width is a square
  }
}
Просто расширив класс Rectangle, мы получим класс Square в виде типа Rectangle. Это означает, что он унаследовал все свойства, общие для Square и Rectangle. В приведенных выше примерах мы видим, как наследование играет важную роль в обеспечении возможности повторного использования кода. Оно также позволяет классу наследовать поведение родительского класса.

3. Абстракция

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

/**
* @author thegeekyasian.com
*/
public class Car {

  public void lock() {}
  public void unlock() {}

  public void startCar() {

	checkFuel();
	checkBattery();
	whatHappensWhenTheCarStarts();
  }

  private void checkFuel() {
	// Check fuel level
  }

  private void checkBattery() {
	// Check car battery
  }

  private void whatHappensWhenTheCarStarts() {
	// Magic happens here
  }
}
В вышеприведенном коде методы lock(), unlock() и startCar() являются публичными, а остальные являются частными по отношению к классу. Мы упростили пользователю “управление автомобилем”. Конечно, он может вручную проверить checkFuel() и checkBattery() перед запуском автомобиля startCar(), но это лишь усложнит процесс. С приведенным выше кодом все, что нужно сделать пользователю, это использовать startCar(), а об остальном позаботится класс. Это то, что мы называем абстракцией.

4. Полиморфизм

Последний и самый важный из четырех столпов ООП — это полиморфизм. Полиморфизм означает “множество форм”. Судя по названию, это функция, которая позволяет выполнять действие несколькими или разными способами. Когда мы говорим о полиморфизме, обсуждать особо нечего, если мы не говорим о его типах. Существует два типа полиморфизма:
  1. Перегрузка метода — статический полиморфизм (Static Binding).
  2. Переопределение метода — динамический полиморфизм (Dynamic Binding).
Давайте обсудим каждый из этих типов и посмотрим, в чем между ними разница.

Перегрузка метода — статический полиморфизм:

Перегрузка метода или статический полиморфизм, также известный как статическое связывание (Static Binding) или связывание во время компиляции, — это тип, в котором вызовы методов определяются во время компиляции. Перегрузка методов позволяет нам иметь несколько методов с одним и тем же именем, имеющих разные типы данных параметра, или разное количество параметров, или то и другое. Но вопрос в том, чем полезна перегрузка метода (или статический полиморфизм)? Давайте посмотрим на приведенные ниже примеры, чтобы лучше понять перегрузку методов. Без перегрузки метода:

/**
* @author thegeekyasian.com
*/
public class Number {

  public void sumInt(int a, int b) {
	System.out.println("Sum: " + (a + b));
  }

  public void sumDouble(double a, double b) {
	System.out.println("Sum: " + (a + b));
  }

  public static void main(String[] args) {

	Number number = new Number();

	number.sumInt(1, 2);
	number.sumDouble(1.8, 2.5);
  }
}
В приведенном выше примере мы создали два метода с разными именами, просто чтобы добавить два разных типа чисел. Если мы продолжим аналогичную реализацию, у нас будет несколько методов с разными именами. Это снизит качество и доступность кода. Чтобы улучшить это, мы можем использовать перегрузку методов, применяя одно и то же название для разных методов. Это позволит пользователю иметь одну опцию в качестве точки входа для суммирования различных типов чисел. Перегрузка метода работает, когда два или более метода имеют одинаковое имя, но разные параметры. Тип возвращаемого значения может быть как одинаковым, так и различным. Но если два метода имеют одинаковое имя, одинаковые параметры, но разные типы возвращаемых значений, то это вызовет перегрузку и ошибку компиляции! С перегрузкой метода:

/**
* @author thegeekyasian.com
*/
public class Number {

  public void sum(int a, int b) {
	System.out.println("Sum: " + (a + b));
  }

  public void sum(double a, double b) {
	System.out.println("Sum: " + (a + b));
  }

  public static void main(String[] args) {

	Number number = new Number();

	number.sum(1, 2);
	number.sum(1.8, 2.5);
  }
}
В том же коде с небольшими изменениями мы смогли перегрузить оба метода, сделав имена одинаковыми для обоих. Теперь пользователь может указать свои конкретные типы данных в качестве параметров метода. Затем он будет выполнять действие на основе предоставленного им типа данных. Эта привязка методов выполняется во время компиляции, поскольку компилятор знает, какой метод будет вызван с указанным типом параметра. Вот почему мы называем это привязкой во время компиляции.

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

В отличие от перегрузки метода, переопределение метода позволяет вам иметь точно такую ​​же сигнатуру, что и несколько методов, но они должны быть в нескольких разных классах. Вопрос в том, что в этом особенного? Эти классы имеют отношение IS-A, то есть должны иметь наследование между собой. Иными словами, при переопределении метода или динамическом полиморфизме методы динамически обрабатываются во время выполнения при вызове метода. Это делается на основе ссылки на объект, которым он инициализируется. Вот небольшой пример переопределения метода:

/**
* @author thegeekyasian.com
*/
public class Animal {

  public void walk() {
	System.out.println("Animal walks");
  }
}

public class Cat extends Animal {

  @Override
  public void walk() {
	System.out.println("Cat walks");
  }
}

public class Dog extends Animal {

  @Override
  public void walk() {
	System.out.println("Dog walks");
  }
}

public class Main {

  public static void main(String[] args) {

	Animal animal = new Animal();
	animal.walk(); // Animal walks

	Cat cat = new Cat();
	cat.walk(); // Cat walks

	Dog dog = new Dog();
	dog.walk(); // Dog walks

	Animal animalCat = new Cat(); // Dynamic Polymorphism
	animalCat.walk(); // Cat walks
	
	Animal animalDog = new Dog(); // Dynamic Polymorphism
	animalDog.walk(); //Dog walks
  }
}
В этом примере переопределения мы динамически присвоили объекты типа “Dog” и “Cat” типу “Animal”. Это позволяет нам вызывать метод walk() с ссылочными экземплярами динамически во время выполнения. Мы можем сделать это с помощью переопределения метода (или динамического полиморфизма). На этом мы завершили краткое обсуждение четырех столпов ООП, и я надеюсь, что оно окажется для вас полезным.
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Artur Galimov Уровень 41
6 сентября 2021
Отличная статья для быстрого повторения материала, автору спасибо!
Herr Mauser Уровень 16
2 сентября 2021
Многое не понял, но очень интересно.