Поламана капсула
Щоб повністю зрозуміти контекст, давайте спершу розберемося, що таке Інкапсуляція!
Інкапсуляція означає практику приховування внутрішнього стану об'єкта та вимогу, щоб всі взаємодії з ним виконувалися через контрольовані методи.
Ми робимо це для захисту внутрішнього стану об'єктів та забезпечення контрольованого доступу.
Деяким з вас це може бути не зовсім зрозуміло, тому давайте подивимося на цей клас Employee.
Примітка: Клас java.util.Date
, що використовується в цьому прикладі, вважається застарілим і подано тут лише для демонстраційних цілей, щоб показати концепцію змінюваності та її вплив на інкапсуляцію.
import java.util.Date;
public class Employee {
private String name;
private double salary;
private Date joinedDate; // Змінюваний об'єкт
public Employee(String name, double salary, Date joinedDate) {
this.name = name;
this.salary = salary;
this.joinedDate = joinedDate;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public Date getJoinedDate() {
return joinedDate;
}
public void setJoinedDate(Date joinedDate) {
this.joinedDate = joinedDate;
}
}
1. Приватні поля:
- Поля
name
,salary
іjoinedDate
позначені якprivate
. Це означає, що ці поля не можна безпосередньо отримати чи змінити ззовні класуEmployee
.
Employee emp = new Employee("Alice", 50000, new Date());
// Прямий доступ, наприклад, emp.name = "Bob"; не дозволяється.
2. Публічні методи отримання та встановлення значень:
- Публічні методи (
getName
,setName
,getSalary
,setSalary
,getJoinedDate
,setJoinedDate
) надаються для читання або зміни приватних полів. Ці методи виступають як контрольовані точки доступу.
emp.setName("Bob"); // Дозволено
System.out.println(emp.getName()); // Дозволено
3. Контрольований доступ
- Використовуючи методи отримання та встановлення значень, можна накладати обмеження або додавати перевірки при доступі або зміні полів.
- Це є перевагою інкапсуляції. Вона дозволяє змінювати внутрішню реалізацію без впливу на будь-який інший код, крім конкретного методу.
- Наприклад, можна змінити метод
setSalary
, щоб заборонити встановлення негативних зарплат.
public void setSalary(double salary) {
if (salary >= 0) {
this.salary = salary;
} else {
throw new IllegalArgumentException("Зарплата повинна бути позитивною");
}
}
Проблема: Змінювані об'єкти руйнують інкапсуляцію
Змінюваний означає здатний змінювати свій стан.
Змінюваний об'єкт може змінювати свій стан після створення. Клас Date
в Java є яскравим прикладом.
Давайте зрозуміємо, як реалізація класу Employee вищезазначена була порушена.
public class Main {
public static void main(String[] args) {
// Створюємо об'єкт Employee
Date date = new Date();
Employee employee = new Employee("Alice", 50000, date);
// Отримуємо joinedDate
Date culpritDateObject = employee.getJoinedDate();
culpritDateObject.setTime(0); // Модифікує дату на "Thu Jan 01 00:00:00 GMT 1970"
// Перевіряємо joinedDate Employee
System.out.println("Дата приєднання працівника: " + employee.getJoinedDate());
}
}
Вивід: Дата приєднання працівника: Thu Jan 01 00:00:00 GMT 1970
Зверніть увагу, що ви змогли змінити приватну властивість joinedDate без використання методу setJoinedDate(“..дата..”), просто використовуючи метод setTime() на змінюваному об'єкті culpritDateObject.
Хтось може запитати: "Як зміна culpritDateObject може вплинути на значення приватного joinedDate?"
Коли ви працюєте з об'єктами, ви працюєте з посиланнями на ці об'єкти. Ці посилання вказують на місце в пам'яті, де зберігається сам об'єкт.
Дозвольте пояснити вам,
1.
Прості типи: Для змінних простих типів (як-от int
, float
тощо) змінна безпосередньо містить значення.
int a = 10; // `a` містить значення 10
2. Типи за посиланням (Об'єкти): Для змінних об'єктних типів змінна містить посилання на місце в пам'яті, де зберігається об'єкт.
Date date = new Date(); // `date` містить посилання на місце в пам'яті об'єкта Date
Отже, в прикладі з класом Employee, коли ви використовуєте метод отримання значення, ви фактично повертаєте посилання на об'єкт дати, а не саме значення дати.
Внаслідок цього culpritDateObject отримує доступ до того ж місця в пам'яті, що й joinedDate, дозволяючи його змінювати.
Що сталося?
- Метод отримання
getJoinedDate()
повернув посилання на внутрішній об'єктjoinedDate
. - Зміна отриманого об'єкта
Date
безпосередньо вплинула на внутрішній стан об'єктаEmployee
. - Інкапсуляція порушена, оскільки зовнішній код змінив внутрішній стан.
Рішення: Оборонне копіювання
1. Що таке оборонне копіювання?
Оборонне копіювання гарантує, що повертається або приймається лише копія змінюваного об'єкта, зберігаючи цілісність оригінального об'єкта.
Змініть клас Employee
наступним чином:
public class Employee {
private String name;
private double salary;
private Date joinedDate;
public Employee(String name, double salary, Date joinedDate) {
this.name = name;
this.salary = salary;
this.joinedDate = new Date(joinedDate.getTime()); // Копія при призначенні
}
public Date getJoinedDate() {
return new Date(joinedDate.getTime()); // Повертаємо оборонну копію
}
public void setJoinedDate(Date joinedDate) {
this.joinedDate = new Date(joinedDate.getTime()); // Копія при встановленні
}
}
2. Використовуйте незмінювані об'єкти
Замість змінюваних класів, таких як Date
, використовуйте незмінювані альтернативи, наприклад, LocalDate
(з java.time
)
private LocalDate joinedDate;
Висновок
- Інкапсуляція є основою надійного об'єктно-орієнтованого дизайну.
- Змінювані об'єкти, якщо з ними неправильно працювати, можуть непомітно порушити інкапсуляцію, що призведе до помилок і непередбачуваної поведінки.
- Використовуючи оборонне копіювання та незмінювані об'єкти, ви можете захистити внутрішній стан вашого класу і забезпечити надійність коду на довгий час.
Перекладено з: Breaking Encapsulation: How Mutable Objects Can Compromise Your Code