Урок 26: Модификаторы доступа public, private, protected

На 26-ом уроке мы разберем одну из фундаментальных тем, которая отличает профессиональный код от любительского: модификаторы доступа.

Этот урок посвящен правилам, которые определяют, кто, когда и к каким частям нашего класса имеет доступ. Понимание этих правил дает ключ к созданию безопасных, предсказуемых и легко поддерживаемых приложений. Мы детально разберем три кита, на которых стоит инкапсуляция в TypeScript: publicprivate и protected.

Объяснение областей видимости. Значение по умолчанию (public)

Представь, что ты проектируешь дом. В этом доме есть гостиная, спальня и сейф. Гостиная это место, куда ты приглашаешь гостей, всё там открыто и доступно. Спальня уже более приватное пространство, заходят туда только члены семьи. А сейф это нечто сугубо личное, доступ к чему имеешь только ты и ещё пара очень доверенных лиц.

В мире объектно-ориентированного программирования наш класс это и есть такой дом. А модификаторы доступа это правила, определяющие, какие «комнаты» (свойства и методы) нашего класса являются «гостиной», какие «спальней», а какие «сейфом».

Область видимости (или доступ) это контекст в коде, из которого можно обратиться к переменной, свойству, методу или классу. TypeScript, как строгий архитектор, позволяет нам очень четко определять эти области с помощью ключевых слов publicprivate и protected.

Теперь о самом важном и самом распространенном модификаторе public.

Значение по умолчанию public

Если ты до этого урока создавал свойства и методы в классе и не указывал никаких модификаторов, то все они по умолчанию были public. Это означает, что к такому свойству или методу можно получить доступ откуда угодно: изнутри класса, из экземпляра класса, из классов-наследников абсолютно из любого места программы.

Давай рассмотрим простой пример. Создадим класс Car.

typescript
class Car {
  // Не указываем модификатор, значит по умолчанию это public
  brand: string;
  model: string;

  constructor(brand: string, model: string) {
    this.brand = brand;
    this.model = model;
  }

  // Метод также public по умолчанию
  getInfo(): string {
    return `Это автомобиль ${this.brand} ${this.model}`;
  }
}

// Создаем экземпляр класса
const myCar = new Car('Tesla', 'Model S');

// Мы имеем полный доступ к public-свойствам и методам извне.
console.log(myCar.brand); // "Tesla"
console.log(myCar.model); // "Model S"
console.log(myCar.getInfo()); // "Это автомобиль Tesla Model S"

Как видишь, мы свободно обращаемся ко всему, что объявили в классе. Ключевое слово public можно указывать явно. Это не поменяет поведение, но сделает код более читаемым и явным.

typescript
class ExplicitCar {
  // Явно указываем public
  public brand: string;
  public model: string;

  constructor(brand: string, model: string) {
    this.brand = brand;
    this.model = model;
  }

  public getInfo(): string {
    return `Это автомобиль ${this.brand} ${this.model}`;
  }
}

const myExplicitCar = new ExplicitCar('Lada', 'Vesta');
console.log(myExplicitCar.brand); // Всё так же работает

Явное указание public это хороший тон. Оно сразу сообщает другому разработчику (или тебе самому через месяц), что ты сознательно сделал этот член класса публичным.

Итак, запомни: все, что помечено как public (явно или по умолчанию), является открытым API твоего класса. Это обещание внешнему миру, что эти свойства и методы стабильны и их можно использовать.

Модификатор private,  полная инкапсуляция

Теперь перейдем к «сейфу» в нашем доме-классе. Модификатор private это самый строгий ограничитель доступа.

Свойство или метод, помеченный как private, доступен только внутри того класса, где он был объявлен. Это значит, что к нему нельзя обратиться из экземпляра класса, из класса-наследника или из любого другого места outside класса.

Зачем это нужно? Для полной инкапсуляции. Мы можем скрыть внутреннюю реализацию класса, его «кишки», которые не должны быть никому видны. Мы прячем те данные и вспомогательные методы, которые нужны для внутренней работы класса, но не являются частью его публичного интерфейса.

Классический пример, свойство для хранения пароля или внутреннего состояния.

typescript
class User {
  public username: string;
  private _password: string; // Приватное свойство

  constructor(username: string, password: string) {
    this.username = username;
    this._password = password; // Сохраняем пароль внутри класса
  }

  // Публичный метод для проверки пароля.
  // Он имеет доступ к private _password.
  public validatePassword(inputPassword: string): boolean {
    return inputPassword === this._password;
  }

  // А вот так делать НЕЛЬЗЯ! Это нарушит безопасность.
  // public getPassword(): string {
  //   return this._password; // Ошибка компиляции: нельзя раскрывать приватное свойство
  // }
}

const user = new User('Max', 'superSecret123');

console.log(user.username); // "Max" - публичное свойство, доступ есть
console.log(user._password); // ОШИБКА! Свойство "_password" является приватным и доступно только внутри класса "User".
console.log(user.validatePassword('wrong')); // false
console.log(user.validatePassword('superSecret123')); // true

Обрати внимание на два момента:

  1. Попытка обратиться к user._password извне вызывает ошибку компиляции. TypeScript не даст нам скомпилировать такой код, защищая нашу же архитектуру.

  2. Мы создали публичный метод validatePassword, который изнутри класса имеет доступ к приватному свойству _password и может выполнить с ним нужные операции, не раскрывая его значение.

Соглашение по именованию

Часто приватные свойства предваряют символом подчеркивания _. Это не более чем договоренность между разработчиками. TypeScript не придает этому символу никакого особого значения, для него важен только модификатор private. Но это очень полезное соглашение, которое позволяет просто взглядом отличить приватные члены класса от публичных.

Приватные методы

Точно так же можно делать и приватные методы. Это вспомогательные методы, которые используются внутри класса для решения своих внутренних задач.

typescript
class DatabaseConnection {
  public connect(connectionString: string) {
    this._validateConnectionString(connectionString);
    this._establishPhysicalConnection();
    this._logSuccess();
  }

  // Приватный метод для внутренней валидации
  private _validateConnectionString(str: string) {
    if (!str || str.length === 0) {
      throw new Error('Connection string is invalid!');
    }
  }

  // Приватный метод для имитации установки соединения
  private _establishPhysicalConnection() {
    console.log('Establishing connection to database...');
    // Логика установки соединения...
  }

  // Ещё один приватный метод
  private _logSuccess() {
    console.log('Connection established successfully!');
  }
}

const db = new DatabaseConnection();
db.connect('my-db://localhost:5432'); // Всё работает

// А эти методы вызвать извне НЕЛЬЗЯ:
db._validateConnectionString('test'); // Ошибка компиляции!
db._establishPhysicalConnection(); // Ошибка компиляции!

Такая архитектура класса DatabaseConnection идеальна. Пользователю класса предоставлен один простой публичный метод connect(). Вся сложная, внутренняя логика (валидация, установка соединения, логирование) надежно спрятана внутри приватных методов. Мы можем как угодно менять реализацию этих методов, не боясь сломать код, который использует наш класс.

Модификатор protected

Мы разобрались с «гостиной» (public) и «сейфом» (private). Осталась «спальня», нечто среднее. Это модификатор protected.

Свойство или метод, помеченный как protected, доступен внутри класса, где объявлен, а также внутри всех классов-наследников (дочерних классов). При этом из экземпляра класса обратиться к protected-члену по-прежнему нельзя.

Это инструмент для построения иерархий классов, когда базовый класс содержит некоторую общую логику и данные, которые должны быть доступны его потомкам, но скрыты от внешнего мира.

Давай рассмотрим пример с базовым классом Vehicle (транспортное средство) и дочерним классом Car.

typescript
class Vehicle {
  // Protected свойство. Не public, но и не private.
  protected brand: string;

  constructor(brand: string) {
    this.brand = brand;
  }

  // Protected метод.
  protected startEngine(): void {
    console.log(`Двигатель ${this.brand} запущен!`);
  }

  // Public метод, который использует protected метод.
  public drive(): void {
    this.startEngine();
    console.log(`${this.brand} начал движение.`);
  }
}

class Car extends Vehicle {
  private model: string;

  constructor(brand: string, model: string) {
    super(brand); // Вызываем конструктор родительского класса
    this.model = model;
  }

  // Дочерний класс имеет доступ к protected свойству и методу родителя.
  public getFullName(): string {
    // Мы можем обратиться к protected brand здесь!
    return `${this.brand} ${this.model}`;
  }

  public makeSound(): void {
    // Мы можем вызвать protected метод родительского класса!
    this.startEngine();
    console.log('Вжжжжжж!');
  }
}

const myVehicle = new Vehicle('SomeVehicle');
const myCar = new Car('Toyota', 'Camry');

// Public методы доступны отовсюду:
myVehicle.drive();
myCar.drive();
myCar.getFullName(); // "Toyota Camry"
myCar.makeSound();

// А вот protected члены извне - НЕДОСТУПНЫ:
console.log(myVehicle.brand); // Ошибка: protected, доступ только внутри иерархии
myVehicle.startEngine(); // Ошибка: protected
console.log(myCar.brand); // Ошибка: protected
myCar.startEngine(); // Ошибка: protected

Это идеальный пример использования protected. Класс Vehicle говорит: «Внутренний механизм запуска двигателя (startEngine) и название бренда (brand) это детали реализации моей семьи (моей и моих потомков). Внешний мир не должен о них знать или напрямую ими управлять. Внешний мир может использовать только открытый метод drive(), а я уже сам решу, как внутри него запустить двигатель».

Класс-наследник Car является частью этой «семьи», поэтому он имеет полный доступ ко всем protected-ресурсам родителя.

Сравнительная таблица модификаторов доступа

Давай соберем все знания в одну простую таблицу для наглядности.

Модификатор Доступ внутри класса Доступ в дочерних классах Доступ из экземпляра класса
public
protected
private

Эта таблица, твоя шпаргалка. Пользуйся ею, когда сомневаешься, какой модификатор выбрать.

Практические примеры и задачи

Давай закрепим знания на реальных кейсах.

Задача 1: Банковский счет

Создай класс BankAccount, который инкапсулирует логику работы с банковским счетом.

  • Свойство private _balance: number хранит текущий баланс. Нельзя позволять менять его напрямую.

  • Конструктор должен инициализировать счет с определенной суммой (по умолчанию 0).

  • Публичный метод public deposit(amount: number): void для пополнения счета.

  • Публичный метод public withdraw(amount: number): void для снятия денег. Должна быть проверка, что средств достаточно.

  • Публичный метод public getBalance(): number единственный безопасный способ узнать баланс извне.

Решение:

typescript
class BankAccount {
  private _balance: number;

  constructor(initialBalance: number = 0) {
    this._balance = initialBalance;
  }

  public deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error('Сумма для пополнения должна быть положительной');
    }
    this._balance += amount;
    console.log(`Счет пополнен на ${amount}. Текущий баланс: ${this._balance}`);
  }

  public withdraw(amount: number): void {
    if (amount <= 0) {
      throw new Error('Сумма для снятия должна быть положительной');
    }
    if (this._balance < amount) {
      throw new Error('Недостаточно средств на счете');
    }
    this._balance -= amount;
    console.log(`Со счета снято ${amount}. Текущий баланс: ${this._balance}`);
  }

  public getBalance(): number {
    return this._balance;
  }
}

const myAccount = new BankAccount(1000);
myAccount.deposit(500); // "Счет пополнен на 500. Текущий баланс: 1500"
myAccount.withdraw(200); // "Со счета снято 200. Текущий баланс: 1300"
// myAccount._balance = 1000000; // Ошибка компиляции! Нельзя изменить баланс в обход методов.
console.log(myAccount.getBalance()); // 1300

Задача 2: Иерархия сотрудников

Создай базовый класс Employee (Сотрудник).

  • У него будут protected свойства name (имя) и salary (зарплата).

  • public метод getInfo(), который возвращает информацию о сотруднике.

  • Создай дочерний класс Manager (Менеджер), который добавляет новое свойство private _department (отдел).

  • Переопредели в классе Manager метод getInfo(), чтобы он включал информацию об отделе. Используй protected свойства родителя.

Решение:

typescript
class Employee {
  protected name: string;
  protected salary: number;

  constructor(name: string, salary: number) {
    this.name = name;
    this.salary = salary;
  }

  public getInfo(): string {
    return `Сотрудник ${this.name}, зарплата: ${this.salary}`;
  }
}

class Manager extends Employee {
  private _department: string;

  constructor(name: string, salary: number, department: string) {
    super(name, salary);
    this._department = department;
  }

  // Имеем доступ к protected name и salary из родительского класса!
  public getInfo(): string {
    return `Менеджер ${this.name}, отдел: ${this._department}, зарплата: ${this.salary}`;
  }
}

const employee = new Employee('Иван', 50000);
console.log(employee.getInfo()); // "Сотрудник Иван, зарплата: 50000"

const manager = new Manager('Мария', 80000, 'Маркетинг');
console.log(manager.getInfo()); // "Менеджер Мария, отдел: Маркетинг, зарплата: 80000"

// employee.name; // Ошибка: protected, нельзя обратиться извне
// manager.name; // Ошибка: protected, нельзя обратиться извне

Важные нюансы и особенности

Модификаторы доступа это фича TypeScript. Важно понимать, что она существует только на этапе компиляции. JavaScript, в который компилируется наш TypeScript-код, не имеет такой концепции на уровне языка (по крайней мере, до недавних версий, но об этом ниже).

Когда TypeScript компилирует код с private или protected, он просто убирает эти модификаторы и в выходном JS-файле все свойства становятся обычными публичными полями. Вся проверка типов и доступов происходит до момента запуска кода, в твоем редакторе и в терминале при запуске tsc.

Это означает, что обойти защиту TypeScript’а можно, если очень захотеть (например, используя any и грубое приведение типов). Но цель этих модификаторов, не создать неприступную крепость, а помочь тебе самому architect правильные и надежные взаимодействия между частями твоего кода, поймать ошибки на самом раннем этапе, этапе написания кода.

Private Fields в современном JavaScript

Современный стандарт JavaScript (ECMAScript) ввел поддержку приватных полей на самом языке. Синтаксис отличается: вместо слова private используется символ # перед именем поля.

TypeScript позволяет использовать этот синтаксис, если тыtarget’ишь современные версии JavaScript.

typescript
class ModernClass {
  #truePrivateSecret: string; // Приватное поле на уровне JavaScript

  constructor(secret: string) {
    this.#truePrivateSecret = secret;
  }

  getSecret(): string {
    return this.#truePrivateSecret; // Доступ внутри класса есть
  }
}

const instance = new ModernClass('my secret');
console.log(instance.getSecret()); // "my secret"
console.log(instance.#truePrivateSecret); // Ошибка на этапе компиляции TypeScript И на этапе выполнения JavaScript!

Эти поля являются приватными не только на этапе компиляции, но и в скомпилированном JavaScript-коде. Это настоящая, «жесткая» инкапсуляция.

Для большинства проектов на TypeScript хватает классических private/protected модификаторов. Но если ты хочешь максимальной защиты и пишешь под современные браузеры или Node.js последних версий, можно использовать и #-синтаксис.

Итоги урока 26

Сегодня мы прошли один из важнейших рубежей в освоении объектно-ориентированного программирования в TypeScript. Давай резюмируем:

  1. public: публичный доступ отовсюду. Является модификатором по умолчанию. Используется для определения публичного API класса.

  2. private: приватный доступ только внутри объявленного класса. Используется для полной инкапсуляции внутреннего состояния и вспомогательных методов.

  3. protected: защищенный доступ внутри класса и его потомков. Используется для построения иерархий классов, предоставляя потомкам доступ к общим ресурсам, скрытым от внешнего мира.

Правильное применение этих модификаторов является признаком зрелого разработчика. Это позволяет:

  • Создавать надежные и безопасные классы.

  • Четко определять контракты взаимодействия (что можно использовать снаружи, а что нет).

  • Легко вносить изменения во внутреннюю реализацию классов, не боясь сломать код, который их использует.

  • Строить сложные и гибкие иерархии наследования.

Обязательно потренируйся использовать их в своем коде. Сначала будет непривычно постоянно думать о том, какой модификатор выбрать, но очень скоро это войдет в привычку и твой код станет на порядок лучше.

Не забывай, что все уроки курса ты можешь найти по ссылке ниже. Увидимся на следующем уроке, где мы продолжим углубляться в возможности TypeScript!

Полный курс с уроками по TypeScript для начинающих: https://max-gabov.ru/typescript-dlya-nachinaushih

Поделиться статьей:
Поддержать автора блога

Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий