Learning Book

abstract-классы и static-члены

abstract-классы и static-члены

Абстрактные классы

Абстрактный класс — это класс, который нельзя создать напрямую через new. Он служит базой для наследников и может содержать как реализованные, так и абстрактные (без реализации) методы.

abstract class Shape {
  // Абстрактные члены — наследник ОБЯЗАН реализовать
  abstract area(): number;
  abstract perimeter(): number;

  // Обычные методы — наследуются как есть
  describe(): string {
    return `Площадь: ${this.area()}, периметр: ${this.perimeter()}`;
  }
}

// Ошибка! Нельзя создать экземпляр абстрактного класса
// const shape = new Shape();

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  // Обязательная реализация абстрактных методов
  area(): number {
    return Math.PI * this.radius ** 2;
  }

  perimeter(): number {
    return 2 * Math.PI * this.radius;
  }
}

const circle = new Circle(5);
console.log(circle.describe()); // "Площадь: 78.54, периметр: 31.42"

Абстрактные поля

Абстрактными могут быть не только методы, но и поля:

abstract class Vehicle {
  abstract readonly type: string;
  abstract maxSpeed: number;

  info(): string {
    return `${this.type} — макс. скорость ${this.maxSpeed} км/ч`;
  }
}

class Car extends Vehicle {
  readonly type = "Автомобиль";
  maxSpeed = 200;
}

class Bicycle extends Vehicle {
  readonly type = "Велосипед";
  maxSpeed = 40;
}

Абстрактные конструкторные сигнатуры

Иногда нужно принять класс как параметр и создать его экземпляр. Для абстрактных классов обычная конструкторная сигнатура не подойдёт:

abstract class Filter {
  abstract apply(data: string): string;
}

class UpperCaseFilter extends Filter {
  apply(data: string): string {
    return data.toUpperCase();
  }
}

// Ошибка! Нельзя использовать abstract-класс как конструкторный тип
// function createFilter(ctor: typeof Filter): Filter {
//   return new ctor();
// }

// Правильно — используем abstract construct signature
function createFilter(
  ctor: new () => Filter
): Filter {
  return new ctor();
}

const filter = createFilter(UpperCaseFilter);
console.log(filter.apply("привет")); // "ПРИВЕТ"

Абстрактный класс vs интерфейс

Характеристикаabstract classinterface
Рантайм-кодДа (генерирует JS-класс)Нет (стирается при компиляции)
Реализация методовМожет содержатьНе может
Множественное наследованиеНет (только extends одного)Да (implements нескольких)
КонструкторМожет иметьНе может
Модификаторы доступаДаНет

Правило: используйте интерфейс, когда нужен только контракт. Используйте абстрактный класс, когда нужно разделить общую реализацию между наследниками.

Статические члены

Статические поля и методы принадлежат самому классу, а не его экземплярам:

class Counter {
  // Статическое поле — общее для всех экземпляров
  static instanceCount = 0;

  // Статический метод
  static getCount(): number {
    return Counter.instanceCount;
  }

  readonly id: number;

  constructor() {
    Counter.instanceCount++;
    this.id = Counter.instanceCount;
  }
}

const a = new Counter(); // id: 1
const b = new Counter(); // id: 2
console.log(Counter.getCount()); // 2

Статические члены и модификаторы доступа

Статические члены поддерживают те же модификаторы видимости:

class Config {
  private static instance: Config | null = null;

  // Приватный конструктор — паттерн Singleton
  private constructor(
    public readonly apiUrl: string,
    public readonly timeout: number
  ) {}

  static getInstance(): Config {
    if (!Config.instance) {
      Config.instance = new Config("https://api.example.com", 5000);
    }
    return Config.instance;
  }
}

// const config = new Config(...); // Ошибка! Конструктор приватный
const config = Config.getInstance();

Статические члены и наследование

Статические члены наследуются:

class Base {
  static greeting = "Привет";

  static createGreeting(name: string): string {
    return `${this.greeting}, ${name}!`;
  }
}

class Derived extends Base {
  static greeting = "Здравствуйте";
}

console.log(Base.createGreeting("мир"));     // "Привет, мир!"
console.log(Derived.createGreeting("мир"));  // "Здравствуйте, мир!"

Запрещённые имена статических членов

Нельзя переопределять свойства Function.prototype: name, length, call, apply, bind:

class StringUtils {
  // Ошибка! 'name' конфликтует с Function.prototype.name
  // static name = "StringUtils";

  // Используйте другое имя
  static utilName = "StringUtils";
}

Статические блоки инициализации

static {} блоки (ES2022+) позволяют выполнять сложную инициализацию статических полей:

class Database {
  static connection: string;
  static isReady: boolean;

  // Статический блок — выполняется один раз при загрузке класса
  static {
    try {
      Database.connection = Database.initConnection();
      Database.isReady = true;
    } catch {
      Database.connection = "";
      Database.isReady = false;
    }
  }

  private static initConnection(): string {
    // Сложная логика инициализации
    return "postgres://localhost:5432/mydb";
  }
}

Статические блоки имеют полный доступ ко всем членам класса, включая private:

class Secret {
  private static key: string;

  static {
    // Доступ к private-членам внутри static-блока
    Secret.key = crypto.randomUUID();
  }

  static getKeyHint(): string {
    return Secret.key.slice(0, 4) + "...";
  }
}

В JavaScript/TypeScript модульные функции часто предпочтительнее статических методов:

// Вариант 1: Статический метод
class MathUtils {
  static clamp(value: number, min: number, max: number): number {
    return Math.max(min, Math.min(max, value));
  }
}

// Вариант 2: Модульная функция
export function clamp(value: number, min: number, max: number): number {
  return Math.max(min, Math.min(max, value));
}

Статические члены оправданы, когда: 1) логика тесно связана с классом (фабричные методы, singleton), 2) нужен доступ к private членам класса, 3) нужно наследование статических методов. В остальных случаях модульные функции проще и лучше поддаются tree-shaking.