Введение
Месяц назад вышел новый стандарт ECMAScript 2022, который нам дал всё больше и больше ООП. На Хабре уже была статья про то, что нам представили в новом стандарте, но сегодня я хочу остановиться на том, что мы не получили. Приватные конструкторы в студию.
Разберём, что такое приватные конструкторы, зачем они нужны и попробуем создать полифил.
Кто такой приватный конструктор?
Приватный конструктор - это специальная функция, которая вызывается при создании экземпляра класса. Только отличие от стандартного конструктора в том, что данная функция может быть вызвана, собственно, только внутри класса. Думаю, люди, которые хорошо знают ООП, уже знакомы с таким понятием.
А зачем он собственно нужен?
Я, собственно, как всегда буду высасывать проблемы из пальца. Первая из таких проблем - сокрытие реализации конструктора:
class Fahrenheit {
	constructor(value) {
    this.#value = value;
  }
	#value;
}Данный конструктор предполагает, что при создании класса мы будем передавать значение градусов в Фаренгейтах. Хмм... А что если мы хотим перевести Цильсии в Фаренгейты и создавать Фаренгейты только из Цельсий? Кому это нужно - не знаю, но тем не менее :D
Мы можем сделать так:
class Fahrenheit {
  constructor(value) {
    this.#value = value;
  }
  
  #value;
  
  static fromCelsius(value) {
		return new Fahrenheit(value * 9/5 + 32);
  }
}Вот тут-то мы и попались. В данном случае мы можем как конструктор использовать, так и статический метод.
Другой вариант, когда у нас есть необходимость в использовании приватного конструктора, - это асинхронное создание экземпляра класса. Пруфов нет, но вы мне верьте.
Как найти выход?
Давайте попробуем реализовать приватный конструктор самостоятельно? В качестве инструмента мы будем использовать прокси-объект:
function privateConstructor(cls) {
  // Флаг, который отвечает за то, что конструктор был вызван через метод,
  // а не через оператор new
  let viaMethod = false;
  
  return new Proxy(cls, {
    // Вешаем обработчик на конструктор, чтобы в случае вызова через new
    // выдавать ошибку
    construct: (target, args) => {
      if (!viaMethod) {
        throw new Error('Cannot use "new" for private constructor');
      }
      
      return Reflect.construct(target, args);
    },
    
    // Вешаем обработчик на каждое свойство и каждый метод
    get: (target, key) => {
      let maybeMethod = Reflect.get(target, key);
      
      // Проверяем, метод это или свойство
      if (maybeMethod instanceof Function) {
        // Переключаем флаг вызова через метод
        viaMethod = true;
      }
      
      return maybeMethod;
    }
  });
}В итоге у нас получилось что-то наподобие декоратора. Однако при такой реализации у нас есть проблема. Давайте рассмотрим её.
Пусть имеется класс A со статическим методом, который создаёт экземпляр класса:
class A {
  static create() {
   	return new A();
  }
}Теперь применим к классу наш декоратор:
const decorA = privateConstructor(A);Если мы попытаемся вызвать конструктор класса сразу, то получим ошибку. Однако, если мы попробуем вызвать сначала вызвать статический метод, а потом конструктор, то всё будет супер:
// Так нельзя
new decorA();
decorA.create();
// А так можно
decorA.create();
new decorA();Вот это я молодец - расписал очевидную проблему.
Проблема кроется во флаге viaMethod. Нужно сделать так, чтобы он, после вызова метода, обратно возвращался в значение false. А как? Ответ: используем декоратор. Это последний за сегодня декоратор, обещаю.
Сделаем декоратор, который вызывает переданную функцию после того, как метод был вызван и вычислен:
function callFunctionAfterMethod(method, callback) {
  // Да, снова прокси, он тоже последний
	return new Proxy(method, {
    apply: (target, thisArg, args) => {
      // Вызываем метод вместе с this
      const result = Reflect.apply(target, thisArg, args);
      // Вызываем колбэк
      callback();
      
      return result;
    }
  });
}А теперь поправим немного наш обработчик get:
function privateConstructor(cls) {
  // Флаг, который отвечает за то, что конструктор был вызван через метод,
  // а не через оператор new
  let viaMethod = false;
  
  return new Proxy(cls, {
    // Вешаем обработчик на конструктор, чтобы в случае вызова через new
    // выдавать ошибку
    construct: (target, args) => {
      if (!viaMethod) {
        throw new Error('Cannot use "new" for private constructor');
      }
      
      return Reflect.construct(target, args);
    },
    
    // Вешаем обработчик на каждое свойство и каждый метод
    get: (target, key) => {
      let maybeMethod = Reflect.get(target, key);
      
      // Проверяем, метод это или свойство
      if (maybeMethod instanceof Function) {
        // Переключаем флаг вызова через метод
        viaMethod = true;
        // Декорируем метод так, чтобы после его вызова флаг менялся на false
        maybeMethod = callFunctionAfterMethod(
          maybeMethod.bind(target),
          () => (viaMethod = false)
        );
      }
      
      return maybeMethod;
    }
  });
}Выводов не будет
Не знаю, что тут можно написать

Комментарии (32)
 - k12th09.08.2022 10:00+2- А как все-таки связаны приватные и асинхронные конструкторы?  - Crinax Автор09.08.2022 10:19- Если я где-то упоминал про асинхронные конструкторы, то процитируйте, пожалуйста, а-то, вроде, всю статью перечитал - не нашёл :D - Под "асинхронным созданием экземпляра класса" я имел ввиду, что может случится примерно следующий кейс: - class Payment { constructor(props) { this.#props = props; } #props; static async fromServer(link) { const data = await fetch(link).then(res => res.json()); return new Payment(data); } }- Я думал привести подобный пример, но чувство, что меня за это архитекторы ПО в подворотне испинают :D  - mayorovp09.08.2022 10:37+3- Ага, значит тут мы получаем данные платежа с сервера и создаём на их основе объект. - А что если мы получили данные платежа из другого источника? Прочитали из файла? Забили константами, потому что у нас модульный тест? В чём проблема обратиться к конструктору когда у нас есть данные платежа?  - nin-jin09.08.2022 12:03-2- Приведу вам пример из жизни. Вот есть у вас Uint8Array с данными криптографического ключа. Вам нужно создать экземпляр обёртки: - const key = new Key( data )- И.. это не компилируется, так как функция импорта ключа - асинхронная. И приходится менять конструктор на фабрику: - const key = await Key.from( data ) - mayorovp09.08.2022 12:12- Функция импорта ключа возвращает CryptoKey. И, скорее всего, именно этот CryptoKey и будет передан в конструктор. - Внимание, вопрос: есть ли какая-то действительно важная причина запрещать прямой вызов конструктора, если у вызывающей стороны уже каким-то чудом есть корректный CryptoKey?  - nin-jin09.08.2022 13:06-3- Она принимает 100500 параметров. Собственно для того обёртка и нужна, чтобы не возиться с этими функциями напрямую. И подобное костылеварение ещё долго продолжаться будет, пока все методы высшего порядка и операторы не будут продублированы в асинхронном варианте.  - mayorovp09.08.2022 13:18- И что дальше? Я всё ещё не понимаю как из 100500 параметров некоторой функции следует необходимость сделать конструктор класса приватным.  - nin-jin09.08.2022 13:22-2- Ну а я не вижу причин что либо вообще приватным делать. При чём тут это вооще?  - mayorovp09.08.2022 13:25+1- Смотрите, я написал что конструктор не обязательно делать приватным. Вы начали спорить. - Но если вы не видите причин делать что-либо вообще приватным, то о чём вообще спор-то? Какую мысль вы пытаетесь донести? 
 
 
 
 
 
 
 
 
 - mayorovp09.08.2022 10:28+5- Слишком сложно. - Основная проблема приватного конструктора — в том, что класс и его конструктор в js — это одно и то же, и нельзя выставить наружу первое не выставив второго. - Однако, зачем в принципе может понадобиться скрывать конструктор? Рассмотрим всё тот же класс Fahrenheit. Если вызвать его конструктор, то случится … что? Мы успешно преобразуем "фаренгейты" в "фаренгейты"? Звучит как совершенно корректная операция. - С асинхронными "конструкторами" та же самая история. Я ещё не видел ситуаций, когда бы действительно необходимо было скрывать конструктор. - Однако, если скрыть конструктор всё же нужно — почему бы не скрыть весь класс? - class Fahrenheit { constructor(value) { this.#value = value; } #value; } Fahrenheit.prototype.constructor = null; export function fromCelsius(value) { return new Fahrenheit(value * 9/5 + 32); } - Crinax Автор09.08.2022 10:40- Звучит на самом деле логично. Я, если честно, сам не встречался с тем, что где-то нужен был приватный конструктор. Может быть я не в правильном сообществе нахожусь (сейчас не про Хабр, не подумайте), но набирает популярность factory method pattern. - С асинхронными конструкторами тоже не встречался 
 
 - Cryvage09.08.2022 10:43+5- Зачем так сложно? Можно безо всяких декораторов. - class Fahrenheit { constructor(value, key) { if (Fahrenheit.#key !== key) throw new Error("Using of raw constroctor is not allowed. Use fromCelsius method instead."); this.#value = value; } static #key = new Object(); #value; static fromCelsius(value) { return new Fahrenheit(value * 9/5 + 32, Fahrenheit.#key); } }
 А вообще, пример, конечно, притянут за уши. Обычно, приватные конструкторы нужны для реализации синглтонов. Но в JS можно реализовать синглтон и без этого.Например, вот так- class Singleton { constructor(val) { if (Singleton.#instance != null) return Singleton.#instance; Singleton.#instance = this; this.Val = val; } Val; static #instance; }
 По вкусу, можно добавить выбрасывание ошибки при повторном явном вызове конструктора, статический метод getInstance, ну и т.д.
 
 На вскидку, сложно сказать, насколько реально нужен приватный конструктор в JS, но он уж точно не является приоритетом. Вот чего не хватает, так это модификатора protected. Уж если начали модификаторы доступа добавлять, не стоило останавливаться на private.
 - Mecitan09.08.2022 11:53+1- Испытал диссонас. JS сообщество идёт по пути функционального программирования, а новый стандарт дал угла в сторону ООП или я чего-то не понимаю?! Разъясните для новичка пожалуйста.  - mayorovp09.08.2022 12:14+1- Во-первых, когда стандарт принимали, так сильно сообщество в сторону ФП ещё не шло. 
 Во-вторых, что плохого в мультипарадигменности языка программирования?
 В-третьих, куча функций — ещё не ФП, и сообщество зашло на путь ФП не так далеко как многие думают. - Mecitan10.08.2022 10:45- Это только плюс, когда есть такие возможности. Я с этим солидарен. Однако бытует мнение, что на классах писать не солидно. Особенно это касается React-a. Тип, мол так сообщество не пишет. Хотя моё видение на этот счёт, что надо использовать все возможности языка. Пусть даже код будет чуть длиннее, но зато более читаемый. (речь о классах). 
- Много читал про это. По этому просто выражу свою благодарность за ваш ответ. 
  - Crinax Автор10.08.2022 11:01- По поводу классовых компонентов. У них есть одна проблема, которая описана в этой статье. Сейчас проверил - она до сих пор актуальна.  - mayorovp10.08.2022 11:41+1- Не уверен что это именно проблема. Отслеживать устаревающие данные нужно в любом случае, и функциональные компоненты хорошо справляются лишь в простейших случаях. Подробности — в комментариях по вашей же ссылке. 
  - Alexandroppolus10.08.2022 12:21- Проблема классовых компонентов только одна - они хуже декомпозируются и имеют склонность превращаться в год-обжекты того или иного размера, с довольно низким cohesion. Да и реактовская центральная идея "ui как значение от стейта" более естественно вписывается в функции. - Но, как уже говорил, это относится только к компонентам. Писать логику может быть удобнее в духе ООП (хотя тут субъективно). 
  - nin-jin10.08.2022 12:34- Потрясающе. Выдавать проблему работы с устаревшим состоянием за достоинство - это надо уметь. 
 
 
 
 
 
           
 


nin-jin
Вывод от меня: когда вкручивали async/await, как обычно, не нашлось знающего JS человека, чтобы подсказать горе-стандартизаторам, что функции могут вызываться и через `new`, и неплохо бы предусмотреть асинхронность и в этом месте.
Crinax Автор
Если я не ошибаюсь, то асинхронных конструкторов нет и в других языках. По крайней мере поиск не дал результатов на язык C#.
nin-jin
Да и в остальные языки async/await вкручивали люди с тем же уровнем развития. Иначе бы не вкручивали эту чушь вовсе.
Nosferatudima
Есть.
Kolyaj
await же просто ждёт резолва промиса. Возвращайте из конструктора промис, будет у вас асинхронный конструктор.
mayorovp
А теперь попробуйте унаследоваться от этого класса и посмотрите какой this придёт в конструктор наследника...
Впрочем, в других реализациях наследование тоже не особо работает.
nin-jin
mayorovp
Да, это будет работать. Но выглядит ужасно. И IDE почти наверняка соврёт в подсказках.