JavaRush /Курси /C# SELF /Помилки з модифікаторами доступу та інкапсуляцією

Помилки з модифікаторами доступу та інкапсуляцією

C# SELF
Рівень 25 , Лекція 2
Відкрита

1. Вступ

Модифікатори доступу — це як паркани й замки у вашому домі: вони визначають, хто може зайти до тієї чи іншої кімнати, а хто — лише піддивитися крізь замкову щілину. Зверніть увагу: у C# вони бувають такими:

Модифікатор Хто бачить
public
Усі
private
Лише всередині поточного класу
protected
Поточний клас і нащадки
internal
Увесь проєкт (збірка)
protected internal
Увесь проєкт і нащадки
private protected
Лише нащадки в цьому ж проєкті

Завдання інкапсуляції — приховувати деталі реалізації класу, щоб ніхто не зламав ваш об’єкт злим виразом obj.Field = 99999;, якщо ви цього не хочете.

2. Класичні помилки з рівнем доступу

Відкриття всього через public

Найпоширеніша помилка — оголошувати всі поля й методи класу як public. Логіка така: «А раптом знадобиться? Хай інші звертаються до елементів класу де завгодно!». Це здається зручним, доки хтось не присвоїть вашому об’єкту неприпустимий стан або не почне використовувати внутрішні деталі так, що змінювати код стає страшно.

Приклад помилки:


public class BankAccount
{
    public string Owner;
    public decimal Balance;
}

Тепер будь-хто може зробити ось так:


var acc = new BankAccount();
acc.Owner = "";
acc.Balance = -1000000M; // Раптово, борг на мільйон!

Що не так?
Ви порушили всі принципи безпеки й здорового глузду. Навіть якщо ваш банк містить лише листочки з дерев або кольорові фантики з гри «Монополія», від’ємний баланс «за бажанням» — дивно.

Перетворення класу на «struct без інкапсуляції»

Друга типова помилка — робити публічними не лише поля, а й внутрішні властивості та методи, які взагалі не повинні бути доступні ззовні.


public class Vault
{
    public string DoorPIN = "1234";
    public void OpenDoor() 
    {
        // Якась магія
    }
}

Що тепер? Тепер будь-хто може дізнатися PIN і відкрити двері до вашого сейфа. Навіть якщо це «тестовий сейф», за місяць про цю дірку забудуть — і станеться халепа.

Правильний підхід:
Інтерфейс класу має бути мінімальним і захищеним. Те, що потрібно іншим, — робимо public. Те, що лише для самого класу, — private. Якщо щось потрібно нащадкам — protected.

3. Помилки інкапсуляції

У .NET є усталений підхід: жодні дані класу (стан) не повинні зберігатися у публічних полях. Усе має бути закрито або принаймні загорнуте у властивості. Прямий доступ до полів — класика поганого стилю та джерело важковловних помилок.

Чому поля мають бути private

Поля — основа внутрішнього стану об’єкта. Якщо надати до них публічний доступ, ви втрачаєте контроль над тим, коли і які значення туди записують. Ваш об’єкт стає вразливим до некоректних значень, а під час зміни їхнього формату всі публічні звернення поламаються.

Замість цього:
Оголошуйте поля як private, а назовні віддавайте лише потрібні властивості або методи:


public class BankAccount
{
    private decimal balance;

    public decimal Balance
    {
        get { return balance; }
        private set 
        {
            if (value < 0) 
                throw new ArgumentException("Баланс не може бути відʼємним");
            balance = value;
        }
    }

    public BankAccount(decimal initialBalance)
    {
        Balance = initialBalance;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0) throw new ArgumentException("Не можна поповнювати на нуль або менше!");
        Balance += amount;
    }
}

Груба помилка: public set для даних, які змінювати не можна

Іноді потрібен лише get, але розробники пишуть public { get; set; } за замовчуванням, бо так швидше. У результаті будь-хто може зробити:


account.Balance = 99999999; // Чому б і ні?

Правильніше так:
Якщо властивість має встановлюватися тільки всередині класу, робіть set із модифікатором:


public decimal Balance { get; private set; }

або, якщо значення має задаватися лише під час створення об’єкта (C# 14):


public decimal Balance { get; init; }

4. Коли protected теж пастка

Відкриті двері для всіх нащадків

protected — поширений спосіб передати функціональність нащадку, але деякі дані краще не робити доступними навіть нащадкам. Наприклад, якщо нащадки класу не повинні змінювати критичні поля напряму.

Приклад:


public class SecureVault
{
    protected string secretCode = "1234";
}

Тепер будь-який нащадок може ось так:


public class HackerVault : SecureVault
{
    public void Hack()
    {
        secretCode = "0000"; // Легко змінив секрет!
    }
}

Рекомендація:
Використовуйте protected тільки для тих речей, які справді мають бути доступні для нащадків. Усе інше залишайте приватним, а для потрібних операцій створюйте захищені методи.

5. Помилки з internal і плутанина зі збірками

Модифікатор internal здається спокусливим: «хай усе буде доступно всередині проєкту». Але щойно проєкт стає більшим за один файл або під’єднуються бібліотеки, з’являються конфлікти. Часто студенти випадково роблять щось публічним усередині збірки, хоча клас мав бути повністю прихований.

Приклад:


internal class Logger
{
    internal void Write(string msg) { /* ... */ }
}

Тепер будь-хто може викликати Logger.Write з будь-якого файлу проєкту. А якщо через рік хтось під’єднає вашу DLL до іншого проєкту? Уся «внутрішня кухня» буде видна, якщо ви лишили щось public.

Порада:
Робіть internal для технічних класів, але намагайтеся тримати чутливі деталі все ж private.

6. Практика: банківський застосунок

Продовжимо наш приклад із банківським застосунком, щоб на практиці побачити, як легко помилитися і що з цим робити.

Приклад помилки № 1: Публічні поля


public class BankAccount
{
    public decimal Balance;
    public string Owner;
}

Проблема: будь-хто може це зламати.

Виправляємо через властивості


public class BankAccount
{
    private decimal _balance;
    private string _owner;

    public string Owner => _owner; // Тільки для читання

    public decimal Balance 
    { 
        get => _balance; 
        private set 
        {
            if (value < 0) throw new ArgumentException("Баланс не може бути відʼємним");
            _balance = value;
        }
    }

    public BankAccount(string owner, decimal initialBalance)
    {
        if (string.IsNullOrWhiteSpace(owner))
            throw new ArgumentException("Імʼя власника обовʼязкове");

        _owner = owner;
        Balance = initialBalance;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Сума поповнення має бути додатною");
        Balance += amount;
    }
}

Тепер якщо ви спробуєте ось так:


var acc = new BankAccount("Олена", 1000m);
acc.Balance = -700m; // Помилка! set приватний.

Або навіть так:


acc.Owner = "Хакер"; // Помилка компіляції, немає set

— нічого не вийде. Лише через конструктор або методи.

Помилка: властивості «для краси»

Багато хто пише:


public int Age { get; set; }

Хоча вік має змінюватися тільки через метод IncreaseAge (наприклад, 1 січня), а безпосередньо — ні.

7. Візуальне нагадування: як не можна і як треба

graph TD
    A[Public Fields] -->|Any code| D[Uncontrolled state]
    B[Private Fields + Public Methods] -->|Well-defined| E[Controlled state]
    F[Public Setters] -->|Anyone can change| D
    G[Private Setters] -->|Only class| E
Підхід Стан захищено? Можна перевіряти? Легко розширювати?
Публічні поля
Властивості (get/set — public) Частково Так, але небезпечно
Приватні поля + властивість із private set

8. Поради щодо стилю роботи з інкапсуляцією

  • Відкривайте назовні лише те, що справді потрібне користувачеві класу.
  • Завжди перевіряйте дані, які потрапляють в об’єкт.
  • Не женіться за надмірністю: якщо властивість не має змінюватися ззовні — робіть set приватним.
  • Для нестандартної логіки завжди використовуйте методи, а не прямий доступ.
  • Навіть якщо дуже кортить — не робіть public-поля «для тесту».
  • Використовуйте властивості замість прямих полів навіть для публічного читання — їх легко потім доопрацювати.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ