Skip to content
IRC-CodingIRC-Coding
ООПИнкапсуляцияEncapsulationInformation HidingМодификаторы видимостиGetterSetterImmutability

ООП Инкапсуляция: Information Hiding и видимость

ООП инкапсуляция с Information Hiding, модификаторы видимости, Getter/Setter и Immutability. Примеры на Java.

S

schutzgeist

9 min read
ООП Инкапсуляция: Information Hiding и видимость

Основы инкапсуляции в OOP: скрытие информации и видимость

Инкапсуляция (Encapsulation) - одно из четырёх фундаментальных принципов объектно-ориентированного программирования. Она скрывает внутреннее состояние и детали реализации объекта, предоставляя наружу только чётко определённые интерфейсы.

Что такое инкапсуляция?

Инкапсуляция объединяет данные и поведение в одну сущность, защищая внутреннее представление от несанкционированного доступа. Цель - создание надёжного, поддерживаемого и безопасного кода через ясное разграничение ответственности.

Ключевые принципы инкапсуляции

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

Модификаторы видимости

Уровни видимости в Java

public class VisibilityDemo {
    
    // public: доступно отовсюду
    public String publicField = "öffentlich";
    
    // protected: внутри класса и подклассов
    protected String protectedField = "geschützt";
    
    // package-private: только внутри пакета
    String packageField = "paket-privat";
    
    // private: только внутри класса
    private String privateField = "privat";
    
    // Private Methode - interne Logik
    private void validateInput(String input) {
        if (input == null || input.trim().isEmpty()) {
            throw new IllegalArgumentException("Input darf nicht leer sein");
        }
    }
    
    // Öffentliche Methode mit Validierung
    public void processData(String data) {
        validateInput(data); // Private Methode nutzen
        // Verarbeitung...
    }
}

Модификаторы доступа в C#

public class BankAccount
{
    // public: доступно отовсюду
    public string AccountNumber { get; }
    
    // private: только внутри класса
    private decimal balance;
    
    // protected: внутри класса и производных классов
    protected string AccountType { get; set; }
    
    // internal: только внутри сборки
    internal string BankCode { get; set; }
    
    // protected internal: внутри сборки или производные классы
    protected internal string BranchCode { get; set; }
    
    // Public Property с приватным setter
    public decimal Balance 
    { 
        get { return balance; }
        private set { balance = value; }
    }
}

Управление доступом в Python

class BankAccount:
    def __init__(self, account_number: str):
        # Public attribute
        self.account_number = account_number
        
        # Protected attribute (Konvention)
        self._balance = 0.0
        
        # Private attribute (Name Mangling)
        self.__transaction_history = []
    
    def deposit(self, amount: float):
        """Public method with validation"""
        if amount <= 0:
            raise ValueError("Amount must be positive")
        self._balance += amount
        self.__add_transaction("deposit", amount)
    
    def _validate_amount(self, amount: float):
        """Protected method for subclasses"""
        return amount > 0
    
    def __add_transaction(self, transaction_type: str, amount: float):
        """Private method - internal use only"""
        self.__transaction_history.append({
            'type': transaction_type,
            'amount': amount,
            'timestamp': datetime.now()
        })

Геттеры и сеттеры

Правильная реализация геттеров и сеттеров

public class BankAccount {
    private String iban;
    private int balanceInCents;
    private boolean isActive = true;
    
    // Konstruktor mit Validierung
    public BankAccount(String iban) {
        if (iban == null || !isValidIban(iban)) {
            throw new IllegalArgumentException("Ungültige IBAN");
        }
        this.iban = iban;
        this.balanceInCents = 0;
    }
    
    // Getter für lesenden Zugriff
    public int getBalanceInCents() {
        return balanceInCents;
    }
    
    // Getter mit Formatierung
    public String getFormattedBalance() {
        return String.format("€%.2f", balanceInCents / 100.0);
    }
    
    // Setter mit Validierung und Business Logik
    public void setBalanceInCents(int balanceInCents) {
        if (!isActive) {
            throw new IllegalStateException("Konto ist deaktiviert");
        }
        if (balanceInCents < 0) {
            throw new IllegalArgumentException("Negativer Kontostand nicht erlaubt");
        }
        this.balanceInCents = balanceInCents;
    }
    
    // Business Methode statt einfachem Setter
    public void deposit(int cents) {
        if (cents <= 0) {
            throw new IllegalArgumentException("Betrag muss positiv sein");
        }
        this.balanceInCents += cents;
    }
    
    public boolean withdraw(int cents) {
        if (cents <= 0) {
            throw new IllegalArgumentException("Betrag muss positiv sein");
        }
        if (cents > balanceInCents) {
            return false; // Nicht genug Guthaben
        }
        this.balanceInCents -= cents;
        return true;
    }
    
    // Private Validierungsmethode
    private boolean isValidIban(String iban) {
        // IBAN Validierungslogik
        return iban != null && iban.matches("[A-Z]{2}[0-9]{20}");
    }
}

Properties в Python

class BankAccount:
    def __init__(self, iban: str):
        self._iban = iban
        self._balance = 0.0
    
    @property
    def balance(self) -> float:
        """Getter für den Kontostand"""
        return self._balance
    
    @property
    def iban(self) -> str:
        """Read-only property für IBAN"""
        return self._iban
    
    @balance.setter
    def balance(self, value: float):
        """Setter mit Validierung"""
        if value < 0:
            raise ValueError("Kontostand darf nicht negativ sein")
        self._balance = value
    
    @property
    def formatted_balance(self) -> str:
        """Computed property"""
        return f"€{self._balance:.2f}"
    
    def deposit(self, amount: float):
        """Business Methode statt direktem Setter"""
        if amount <= 0:
            raise ValueError("Betrag muss positiv sein")
        self._balance += amount

Неизменяемость

Неизменяемые объекты в Java

// Immutable Klasse mit final Feldern
public final class ImmutablePerson {
    private final String name;
    private final int age;
    private final List<String> hobbies; // Defensive Copy!
    
    public ImmutablePerson(String name, int age, List<String> hobbies) {
        this.name = Objects.requireNonNull(name, "Name darf nicht null sein");
        this.age = age;
        // Defensive Copy für veränderliche Parameter
        this.hobbies = List.copyOf(Objects.requireNonNull(hobbies));
    }
    
    // Getter - keine Setter!
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    // Defensive Copy für veränderliche Rückgaben
    public List<String> getHobbies() {
        return new ArrayList<>(hobbies);
    }
    
    // Methoden erstellen neue Instanzen
    public ImmutablePerson withAge(int newAge) {
        return new ImmutablePerson(this.name, newAge, this.hobbies);
    }
    
    public ImmutablePerson addHobby(String hobby) {
        List<String> newHobbies = new ArrayList<>(this.hobbies);
        newHobbies.add(hobby);
        return new ImmutablePerson(this.name, this.age, newHobbies);
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ImmutablePerson that = (ImmutablePerson) o;
        return age == that.age && 
               Objects.equals(name, that.name) && 
               Objects.equals(hobbies, that.hobbies);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age, hobbies);
    }
    
    @Override
    public String toString() {
        return "ImmutablePerson{name='" + name + "', age=" + age + ", hobbies=" + hobbies + "}";
    }
}

Python Dataclasses для неизменяемости

from dataclasses import dataclass
from typing import List
import copy

@dataclass(frozen=True)
class ImmutablePerson:
    name: str
    age: int
    hobbies: List[str]  # Внимание: список остаётся изменяемым!
    
    def __post_init__(self):
        # Валидация после инициализации
        if self.age < 0:
            raise ValueError("Возраст не может быть отрицательным")
        if not self.name:
            raise ValueError("Имя не может быть пустым")
        
        # Защитная копия для изменяемых полей
        object.__setattr__(self, 'hobbies', tuple(self.hobbies))
    
    def with_age(self, new_age: int) -> 'ImmutablePerson':
        """Создаёт новый экземпляр с изменённым возрастом"""
        return ImmutablePerson(self.name, new_age, list(self.hobbies))
    
    def add_hobby(self, hobby: str) -> 'ImmutablePerson':
        """Создаёт новый экземпляр с добавленным хобби"""
        new_hobbies = list(self.hobbies) + [hobby]
        return ImmutablePerson(self.name, self.age, new_hobbies)

Защитные копии

Защита от внешних изменений

public class ShoppingCart {
    private final List<Item> items = new ArrayList<>();
    private final Customer customer;
    
    public ShoppingCart(Customer customer) {
        this.customer = Objects.requireNonNull(customer);
    }
    
    // Защитная копия при возврате
    public List<Item> getItems() {
        return new ArrayList<>(items); // Возвращаем копию
    }
    
    // Защитная копия при приёме параметра
    public void addItems(List<Item> newItems) {
        if (newItems != null) {
            this.items.addAll(new ArrayList<>(newItems)); // Сохраняем копию
        }
    }
    
    // Неизменяемое представление
    public List<Item> getItemsUnmodifiable() {
        return Collections.unmodifiableList(items);
    }
    
    // Stream API для безопасного доступа
    public Stream<Item> itemStream() {
        return items.stream();
    }
}

Защитные копии в Python

class ShoppingCart:
    def __init__(self, customer):
        self._customer = customer
        self._items = []
    
    def get_items(self):
        """Возвращает защитную копию"""
        return self._items.copy()
    
    def add_items(self, items):
        """Сохраняет защитную копию"""
        if items:
            self._items.extend(items.copy())
    
    def get_items_immutable(self):
        """Возвращает неизменяемое представление"""
        return tuple(self._items)
    
    def items_iterator(self):
        """Итератор для безопасного доступа"""
        return iter(self._items)

Закон Деметры

Избегаем цепочек вызовов

// Плохо - нарушает закон Деметры
public void processOrder(Order order) {
    // Слишком много точек - высокая связанность
    String city = order.getCustomer().getAddress().getCity();
    double tax = order.getCustomer().getAddress().getTaxRate();
    // ...
}

// Хорошо - слабая связанность
public void processOrder(Order order) {
    String city = order.getCustomerCity();
    double tax = order.getCustomerTaxRate();
    // ...
}

// Лучшая реализация
public class Order {
    private Customer customer;
    
    public String getCustomerCity() {
        return customer.getAddress().getCity();
    }
    
    public double getCustomerTaxRate() {
        return customer.getAddress().getTaxRate();
    }
}

Валидация и инварианты

Надёжная стратегия валидации

public class EmailAddress {
    private final String value;
    private static final Pattern EMAIL_PATTERN = 
        Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$");
    
    public EmailAddress(String email) {
        String normalized = normalize(email);
        validate(normalized);
        this.value = normalized;
    }
    
    private String normalize(String email) {
        if (email == null) {
            throw new IllegalArgumentException("Email не может быть null");
        }
        return email.trim().toLowerCase();
    }
    
    private void validate(String email) {
        if (email.isEmpty()) {
            throw new IllegalArgumentException("Email не может быть пустым");
        }
        if (!EMAIL_PATTERN.matcher(email).matches()) {
            throw new IllegalArgumentException("Неверный формат email");
        }
        if (email.length() > 254) {
            throw new IllegalArgumentException("Email слишком длинный");
        }
    }
    
    public String getValue() {
        return value;
    }
    
    @Override
    public String toString() {
        return value;
    }
}

Design by Contract

Предусловия и постусловия

public class BankTransfer {
    private final BankAccount fromAccount;
    private final BankAccount toAccount;
    
    public BankTransfer(BankAccount fromAccount, BankAccount toAccount) {
        this.fromAccount = Objects.requireNonNull(fromAccount);
        this.toAccount = Objects.requireNonNull(toAccount);
    }
    
    /**
     * Переводит деньги с одного счёта на другой
     * 
     * @param amount сумма перевода в центах
     * @throws IllegalArgumentException если amount <= 0
     * @throws IllegalStateException если исходный счёт не покрывает сумму
     * @throws IllegalStateException если один из счётов неактивен
     * @post fromAccount.getBalance() == old(fromAccount.getBalance()) - amount
     * @post toAccount.getBalance() == old(toAccount.getBalance()) + amount
     */
    public void transfer(int amount) {
        // Проверяем предусловия
        if (amount <= 0) {
            throw new IllegalArgumentException("Сумма должна быть положительной");
        }
        if (!fromAccount.isActive() || !toAccount.isActive()) {
            throw new IllegalStateException("Оба счёта должны быть активны");
        }
        if (fromAccount.getBalanceInCents() < amount) {
            throw new IllegalStateException("Недостаточно средств на исходном счёте");
        }
        
        // Сохраняем старые значения для проверки постусловий
        int oldFromBalance = fromAccount.getBalanceInCents();
        int oldToBalance = toAccount.getBalanceInCents();
        
        try {
            // Атомарная операция
            fromAccount.withdraw(amount);
            toAccount.deposit(amount);
            
            // Проверяем постусловия
            assert fromAccount.getBalanceInCents() == oldFromBalance - amount;
            assert toAccount.getBalanceInCents() == oldToBalance + amount;
            
        } catch (Exception e) {
            // Откатываем при ошибках
            throw new RuntimeException("Перевод не выполнен", e);
        }
    }
}

Инкапсуляция на практике

Пример: система заказов в электронной коммерции

public class Order {
    private final String orderId;
    private final Customer customer;
    private final List<OrderItem> items;
    private OrderStatus status;
    private final LocalDateTime createdAt;
    private LocalDateTime shippedAt;
    
    // Приватный конструктор - используем Factory Method
    private Order(String orderId, Customer customer) {
        this.orderId = Objects.requireNonNull(orderId);
        this.customer = Objects.requireNonNull(customer);
        this.items = new ArrayList<>();
        this.status = OrderStatus.PENDING;
        this.createdAt = LocalDateTime.now();
    }
    
    // Factory Method для создания объектов
    public static Order create(Customer customer) {
        if (customer == null || !customer.isActive()) {
            throw new IllegalArgumentException("Неверный клиент");
        }
        String orderId = generateOrderId();
        return new Order(orderId, customer);
    }
    
    // Бизнес-метод с переходами между состояниями
    public void addItem(Product product, int quantity) {
        if (product == null) {
            throw new IllegalArgumentException("Продукт не может быть null");
        }
        if (quantity <= 0) {
            throw new IllegalArgumentException("Количество должно быть положительным");
        }
        if (status != OrderStatus.PENDING) {
            throw new IllegalStateException("Заказ больше нельзя изменять");
        }
        
        OrderItem item = new OrderItem(product, quantity);
        items.add(item);
    }
    
    // Переход между состояниями с валидацией
    public void ship() {
        if (status != OrderStatus.CONFIRMED) {
            throw new IllegalStateException("Заказ должен быть подтверждён");
        }
        if (items.isEmpty()) {
            throw new IllegalStateException("Заказ пуст");
        }
        
        this.status = OrderStatus.SHIPPED;
        this.shippedAt = LocalDateTime.now();
        
        // Публикуем событие
        EventPublisher.publish(new OrderShippedEvent(orderId));
    }
    
    // Безопасные геттеры с защитными копиями
    public List<OrderItem> getItems() {
        return new ArrayList<>(items);
    }
    
    public Customer getCustomer() {
        return customer; // Неизменяемо, копия не нужна
    }
    
    // Вычисляемое свойство
    public BigDecimal getTotalAmount() {
        return items.stream()
            .map(OrderItem::getTotalPrice)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    // Геттер статуса - неизменяемо
    public OrderStatus getStatus() {
        return status;
    }
    
    // Приватный вспомогательный метод
    private static String generateOrderId() {
        return "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
    }
}

Преимущества инкапсуляции

1. Слабая связанность

  • Компоненты менее зависимы друг от друга
  • Изменения имеют ограниченное влияние
  • Рефакторинг становится проще

2. Высокая связность

  • Связанная функциональность собрана в одном месте
  • Ясное распределение ответственности
  • Лучшая понятность кода

3. Улучшенная безопасность

  • Контролируемый доступ к данным
  • Валидация на границах
  • Защита от несогласованных состояний

4. Лучшая тестируемость

  • Чёткие интерфейсы для Unit Tests
  • Проще создавать Mock-объекты
  • Фокус на поведении вместо реализации

Недостатки и трудности

1. Дополнительная работа

  • Больше кода для getters/setters
  • Boilerplate для простых классов данных
  • Выше затраты на реализацию

2. Возможная чрезмерная инкапсуляция

  • Слишком много мелких методов
  • Лишние слои абстракции
  • Сложность без пользы

3. Кривая обучения

  • Нужно понимать, как правильно инкапсулировать
  • Баланс между открытостью и закрытостью
  • Опыт для выбора нужной детализации

Вопросы к экзамену

Типичные вопросы IHK

  1. В чём разница между инкапсуляцией и абстракцией?

    • Инкапсуляция скрывает детали реализации, абстракция снижает сложность
  2. Когда имеют смысл getters и setters?

    • Только при деловой необходимости, не для каждого приватного поля
  3. Почему публичные поля проблематичны?

    • Они обходят валидацию и нарушают инварианты
  4. Как неизменяемость поддерживает инкапсуляцию?

    • Гарантирует стабильные инварианты и упрощает многопоточное использование
  5. Что говорит Law of Demeter?

    • Общайся только с прямыми соседями, избегай каскадных вызовов

Итоги

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

  • Продуманного планирования видимости
  • Валидации на границах
  • Оборонительного программирования
  • Намеренной неизменяемости
  • Чётких границ ответственности

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

Продолжение в обучающем пути OOP

Все статьи OOP теперь завершены. Вернитесь к первой статье: Objektorientierte Programmierung OOP Grundlagen.

Назад к блогу
Share:

Похожие статьи