DevGang
Авторизоваться

Использование using в TypeScript для управления ресурсами

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

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

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

В этой статье мы обсудим важность управления ресурсами и то, как новый оператор using TypeScript может помочь нам лучше управлять нашими ресурсами. В нашем блоге вы также можете изучить тему: Использование using в JavaScript.

Что такое управление ресурсами?

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

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

В программном обеспечении всякий раз, когда мы хотим что-то сделать, мы вызываем функцию. Иногда время жизни объекта, для которого мы вызываем функцию, статично — другими словами, он существует в памяти только один раз и может быть вызван в любое время и по любой причине.

Примером такой функции может быть вызов console.log('log output') в нашей консоли разработчика в Chrome или Edge. Для этого нам даже не нужно создавать новый console объект. Он уже доступен по всему миру в окне нашего браузера:

На самом деле нам не нужно управлять этим ресурсом по разным причинам:

  • Он создан вне нашего контроля
  • Он существует как глобально доступная функция
  • Он также завершается за наносекунды
  • Он не работает асинхронно

Но не все наши приложения такие простые. С помощью современных веб-приложений мы можем делать великие дела — вызывать веб-сервисы на другом конце света, отправлять изображения на анализ ИИ, записывать подробные ответы в базу данных — этот список можно продолжать.

Однако при этом мы интенсивно используем системные ресурсы. В результате мы должны должным образом управлять этими ресурсами с течением времени.

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

Давайте представим, что мы хотим подключиться к базе данных, записать несколько записей, а затем отключиться от базы данных. Вероятно, это будет примерно следующее:

let db = new DbConnection("server=localhost,token=<...token here...>");
await db.connect();
await db.execute("INSERT INTO testusers ('Rob')");
await db.close(); // clean up used resourceso

Мы создали экземпляр нашего DbConnection, подключились к нему, а также вставили некоторые данные. Поскольку эти объекты были созданы, они теперь занимают часть памяти в хост-системе.

После того, как это произойдет, нам необходимо очистить себя, что включает в себя отключение от базы данных и указание того, что система может удалить объект DbConnection. В зависимости от того, где вы используете этот код, вы можете использовать что-то вроде db.close() или db.dispose() в следующей строке.

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

Если что-то пойдет не так, мы можем просто использовать блок try...catch для устранения проблем и выполнения соответствующих действий по очистке, например:

let db = new DbConnection("server=localhost,token=<...token here...>");
try{
  await db.connect();
  await db.execute("INSERT INTO testusers ('Rob')");
}
finally
{
  await db.close();
}

Но даже в этом случае наш код всё равно технически некорректен.

Наш объект db будет в состоянии closed, поскольку код в блоке finally был выполнен. Далее в этом файле мы можем забыть, что уже закрыли этот объект, и попытаться снова вызвать db, но наше приложение выдаст исключение.

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

Оператор using как решение для лучшего управления ресурсами

Введите явное предложение по управлению ресурсами, которое описывает, среди прочего, новый оператор using, который был представлен в TypeScript 5.2 и проникает в JavaScript. В начале файла README показано, на что направлено это предложение:

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

Звучит хорошо, но что это значит? Что ж, это немного сложно, поэтому давайте воспользуемся общими понятиями, чтобы понять, что для нас значит это изменение.

Рассмотрим скромный конструктор в TypeScript. Всякий раз, когда мы создаем экземпляр объекта в TypeScript, конструктор запускается по мере «создания» объекта. Нам всегда гарантируется, что конструктор запустится при создании объекта.

После того, как объект построен и мы выполнили соответствующую операцию, нам, скорее всего, придется выполнить демонтаж — очистку ресурсов, закрытие соединений и т. д. Как это сделать? В TypeScript 5.2 мы можем использовать оператор using.

Представьте, что у нас есть таблица insert внутри функции. Давайте посмотрим, как это можно записать с помощью оператора using:

function insertTestUsers() {
  using db = DbConnection("server=localhost,token=<...token here...>");
  await db.connect();
  await db.execute("INSERT INTO testusers ('Rob')");
}

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

Если вы когда-либо использовали C#, вы, возможно, узнали об этом и уже понимаете, что это значительно упрощает управление ресурсами и их очистку. Поскольку C# и TypeScript довольно тесно связаны с точки зрения того, кто их изобрел, возможно, неудивительно, что схожие ключевые слова используются для аналогичных функций.

Но это на C# — как нам использовать это в TypeScript? Что ж, по состоянию на 22 августа 2023 года он доступен в новом TypeScript 5.2, поэтому вы сможете использовать его сегодня, если обновите установленную версию TypeScript!

Использование оператора using в TypeScript

К счастью, настроить использование оператора using довольно просто. Начните с создания нового каталога, добавления в него нового файла tsconfig.json и вставки в него следующего кода:

{
    "compilerOptions": {
        "target": "es2022",
        "lib": ["es2022", "esnext.disposable", "dom"],
        "module": "es2022"
    },
}

Если вы похожи на меня, вы можете увидеть волнистую линию под записью ESNext.Disposable. Это нормально игнорировать.

Затем в каталоге выполните следующую команду:

npm -i typescript --dev

Если вы запустите tsc --version, он должен вернуть TypeScript 5.2. Это значит, что все готово.

Теперь давайте создадим пару классов:

  • Один класс будет отвечать за запись в базу данных.
  • Другой класс будет отвечать за запись в журнал, который находится в базе данных. Регистратор будет зависеть от базы данных

Мы также добавим импорт в core-js, чтобы иметь возможность использовать функцию dispose до того, как она станет широко доступной:

import 'core-js/actual/index.js'

class FakeDatabaseWriter {
    connected = false;
    executeSQLStatement(statement: string) {
        console.log(`Pretending to execute statement ${statement}`)
    }
    connect(){
        this.connected = true;
    }
}
class FakeLogger {
    constructor(private db: FakeDatabaseWriter){
    }
    writeLogMessage(message: string){
        console.log(`Pretending to log ${message}`);
    }
}

Давайте реализуем оба этих класса Disposable, а затем реализуем требования этого интерфейса, который представляет собой новую функцию под названием Symbol.dispose

В рамках этих функций давайте выведем логи, описывающие, когда эти объекты создаются и когда они удаляются для примера, который мы используем. Наш код теперь выглядит так:

// Because dispose and asyncDispose are so new, we need to manually 'polyfill' the existence of these functions in order for TypeScript to use them
// See: https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#using-declarations-and-explicit-resource-management
(Symbol as any).dispose ??= Symbol("Symbol.dispose");
(Symbol as any).asyncDispose ??= Symbol("Symbol.asyncDispose");

class FakeDatabaseWriter implements Disposable {
    constructor(){
        console.log("The FakeDatabaseWriter is being constructed");
    }
    [Symbol.dispose](): void {
        console.log("The FakeDatabaseWriter is disposing! Setting this.connected to false.");
        this.connected = false;
        console.log(`Connected property is now ${this.connected}`);
    }
    connected = false;
    executeSQLStatement(statement: string) {
        console.log(`Pretending to execute statement ${statement}`)
    }
    connect(){
        this.connected = true;
    }
}
class FakeLogger implements Disposable {

    [Symbol.dispose](): void {
        console.log("The FakeLogger is disposing!");
    }
    constructor(private db: FakeDatabaseWriter){
        console.log("The FakeLogger is being constructed");
    }

    writeLogMessage(message: string){
        console.log(`Pretending to log ${message}`);
    }
}

Теперь, когда все это настроено, давайте создадим наши классы и посмотрим, что произойдет.

Мы можем использовать ключевое слово using для создания экземпляра нашего FakeDatabaseWriter, а затем сразу же создать экземпляр нового FakeLogger с помощью уже созданного средства записи базы данных. Затем мы можем подключиться и выполнить оператор по мере необходимости:

{
    using db = new FakeDatabaseWriter(), logger = new FakeLogger(db);
    db.connect();
    db.executeSQLStatement("INSERT INTO fakeTable VALUES ('value one', 'value two')");
}

Теперь запустите npm run exec. Вывод следующий:

The FakeDatabaseWriter is being constructed
The FakeLogger is being constructed
Pretending to execute statement INSERT INTO fakeTable VALUES ('value one', 'value two')
The FakeLogger is disposing!
The FakeDatabaseWriter is disposing! Setting this.connected to false.
Connected property is now false

Порядок этих операций важен. Обобщим:

  1. Объекты в операторе using создаются
  2. Код выполняется на последующих строках
  3. Когда оператор using выходит за пределы области действия, функция dispose вызывается для объектов, созданных с помощью оператора using

Обратите внимание, что операции удаления вызываются в обратном порядке. Это ключ! При вызове операций удаления в обратном порядке сначала удаляются зависимые объекты, а затем другие базовые объекты.

В результате все создается и удаляется аккуратно, без необходимости помнить об этом в блоках try...catch...finally.

А как насчет управления ресурсами для асинхронных операций?

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

Однако реальность такова, что мы, скорее всего, столкнемся с операциями, которые потребуют асинхронной очистки, пока мы ожидаем освобождения ресурсов. В этих случаях мы можем реализовать AsyncDisposable следующим образом:

class LongRunningCleanup implements AsyncDisposable{
    async [Symbol.asyncDispose]()  {
        console.log(`LongRunningCleanup is disposing! Began at ${new Date()}`)
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log(`LongRunningCleanup is finished disposing. Finished at ${new Date()}`)
    }
    async longRunningOperation() {
        console.log("Executing long running operation...");
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log("Long running operation has finished");
    }
}

Выполнение этого кода приводит к следующим результатам:

The FakeDatabaseWriter is being constructed
The FakeLogger is being constructed
Pretending to execute statement INSERT INTO fakeTable VALUES ('value one', 'value two')
Executing long running operation...
LongRunningCleanup is disposing! Began at Mon Jul 31 2023 23:09:34 GMT+1000 (Australian Eastern Standard Time)
Long running operation has finished
LongRunningCleanup is finished disposing. Finished at Mon Jul 31 2023 23:09:35 GMT+1000 (Australian Eastern Standard Time)
The FakeLogger is disposing!
The FakeDatabaseWriter is disposing! Setting this.connected to false.
Connected property is now false

И снова мы видим, что зависимости освобождаются в обратном порядке. Разница здесь в том, что вызывается вызов асинхронного метода удаления и наблюдается await.

Полный пример кода таков:

// Because dispose and asyncDispose are so new, we need to manually 'polyfill' the existence of these functions in order for TypeScript to use them
// See: https://devblogs.microsoft.com/typescript/announcing-typescript-5-2/#using-declarations-and-explicit-resource-management
(Symbol as any).dispose ??= Symbol("Symbol.dispose");
(Symbol as any).asyncDispose ??= Symbol("Symbol.asyncDispose");

class FakeDatabaseWriter implements Disposable {
    constructor() {
        console.log("The FakeDatabaseWriter is being constructed");
    }
    connected = false;
    executeSQLStatement(statement: string) {
        console.log(`Pretending to execute statement ${statement}`)
    }
    connect() {
        this.connected = true;
    }
    [Symbol.dispose]() {
        console.log("The FakeDatabaseWriter is disposing! Setting this.connected to false.");
        this.connected = false;
        console.log(`Connected property is now ${this.connected}`);
    }
}
class FakeLogger implements Disposable {
    [Symbol.dispose]()  {
        console.log("The FakeLogger is disposing!");
    }
    constructor(private db: FakeDatabaseWriter) {
        console.log("The FakeLogger is being constructed");
    }

    writeLogMessage(message: string) {
        console.log(`Pretending to log ${message}`);
    }
}
class LongRunningCleanup implements AsyncDisposable{
    async [Symbol.asyncDispose]()  {
        console.log(`LongRunningCleanup is disposing! Began at ${new Date()}`)
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log(`LongRunningCleanup is finished disposing. Finished at ${new Date()}`)
    }
    async longRunningOperation() {
        console.log("Executing long running operation...");
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log("Long running operation has finished");
    }
}

using db = new FakeDatabaseWriter(), logger = new FakeLogger(db);
await using longrunning = new LongRunningCleanup();
db.connect();
db.executeSQLStatement("INSERT INTO fakeTable VALUES ('value one', 'value two')");
longrunning.longRunningOperation();

export {}

Заключительные мысли: TypeScript и будущее управления ресурсами

Использование нового оператора using должно значительно упростить управление ресурсами в TypeScript, и вам не придется постоянно помнить о вызове dispose вручную.

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

Источник:

#TypeScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу