Выходя за рамки основ

Для программистов JavaScript, которые уже сделали решительный шаг и изучили основы TypeScript, эта статья покажет вам более сложные способы использования языка. Мы также кратко рассмотрим две важные концепции, о которых должен знать каждый объектно-ориентированный программист: SOLID и шаблоны проектирования.

Для новичков в TypeScript или тех, кто нуждается в обновлении, вы можете ознакомиться с другой моей статьей Переход от JavaScript к TypeScript.

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

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

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

Невозможно создать экземпляр абстрактного класса, т. Е. Вы не можете создать экземпляр из одного:

Давайте добавим к этому классу несколько свойств:

Мы обсудим, что означает protected позже. Наше первое свойство PI - обычное свойство. Он также отмечен как readonly, потому что мы не хотим, чтобы его значение когда-либо менялось.

Наше второе свойство calcType - абстрактное свойство. Отметка свойства как абстрактного означает, что дочерний класс нашего базового класса должен определять это свойство. Давайте создадим дочерний класс Calculator с именем SimpleCalculator.

У нас есть ошибка, которую мы можем исправить, нажав «Быстрое исправление», а затем «Реализовать унаследованный абстрактный класс».

Давайте также предоставим значение для calcType:

Поскольку calcType является абстрактным свойством, мы должны определить его в дочернем классе. Также обратите внимание, что мы удаляем ключевое слово abstract из calcType в дочернем классе. Давайте создадим еще два дочерних класса из Calculator:

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

SOLID и принцип открытого-закрытого

В мире объектно-ориентированного программирования вы рано или поздно столкнетесь с набором принципов проектирования, известных как SOLID. SOLID состоит из пяти частей, где буква O означает принцип открыт-закрыт. Наши классы открыты для расширения, но закрыты для модификации.

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

Защищенный модификатор и абстрактный метод

Создание дочернего класса из другого класса называется наследованием. В отличие от JavaScript, TypeScript предоставляет функцию, известную как модификаторы видимости, которые позволяют вам контролировать, какие свойства дочерний класс будет наследовать и который останется доступным только в родительском (базовом) классе.

Свойство или метод, помеченные как private, не наследуются, а помеченные как protected наследуются. Те, что помечены как public, также наследуются. Как правило, наши методы - это public, а наши свойства - protected. Давайте добавим два метода в наш Calculator класс: один обычный и один абстрактный.

Идите вперед и исправьте все новые ошибки, выбрав «Реализовать унаследованный абстрактный класс для каждого дочернего класса».

Все наши дочерние классы наследуют метод sum, который определен в нашем базовом классе Calculator, но каждый дочерний класс должен предоставлять свою собственную реализацию для метода mul (умножение).

Хотя этот пример надуман, давайте удалим класс GraphingCalculator и предоставим две разные реализации для mul:

SimpleCalculator - примитивное устройство, и его можно умножать только многократным сложением. ScientificCalculator, с другой стороны, имеет встроенную схему для выполнения умножения. Оба подкласса используют уже определенный sum метод из Calculator.

Коллекции калькуляторов и ограничивающих обобщений

Взгляните на следующий фрагмент кода:

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

Калькулятор корзины покупок

Представьте, что у нас есть интернет-магазин по продаже калькуляторов. Когда клиент посещает наш сайт, у него есть корзина для заказа калькуляторов. В нашей корзине покупок хранится каждый заказанный калькулятор в виде массива:

Аннотации какого типа мы даем calcOrder? Мы можем изменить класс, чтобы он принимал общий параметр типа T, но T может представлять любой тип:

Мы хотим ограничить наш общий параметр:

Фрагмент кода T extends SimpleCalculator | ScientificCalculator использует тип объединения, чтобы ограничить T одним из двух типов, но что, если у нас есть три, четыре или 100 различных типов калькуляторов? Перечислить все возможные типы было бы слишком трудоемко. Более того, нам придется постоянно изменять определение базового класса, что противоречит принципу открытого-закрытого.

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

Шаблоны проектирования и шаблон Singleton

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

Шаблон Singleton позволяет нам ограничить класс созданием из него только одного экземпляра. Предоставляется специальный метод для предоставления доступа к этому экземпляру.

Корзина покупок - хороший кандидат на то, чтобы стать синглтоном: нам нужен только один экземпляр корзины покупок в нашем (интерфейсном) приложении. Независимо от того, где мы находимся в коде нашего приложения, мы можем быть уверены, что получаем доступ и изменяем только одну корзину.

Давайте превратим наш ShoppingCart класс в синглтон. Есть три шага.

Создайте свойство, ссылающееся на себя

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

(Статические свойства - также известные как статические поля - в настоящее время являются экспериментальными в JavaScript. Статические методы доступны с ES6. Нажмите здесь для получения дополнительной информации.)

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

Предоставьте статическую точку доступа

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

getCart проверяет, не создали ли мы уже корзину покупок. Сначала ссылка на нашу корзину пуста. getCart затем вызывает конструктор и устанавливает статическое свойство cart для этого нового экземпляра. Оператор if заблокирует возможность кому-либо создать еще один экземпляр нашей корзины покупок. Каждый последующий вызов getCart всегда возвращает один и тот же экземпляр корзины.

Заблокируйте конструктор

Последний шаг - запретить кому-либо использовать оператор new с нашим классом ShoppingCart. Сделаем это, сделав конструктор private:

Теперь конструктор доступен только внутри самого класса. Ключевое слово new нельзя использовать вне класса для создания нового ShoppingCart экземпляра:

Итак, как нам получить доступ к нашей корзине покупок? Мы используем статический метод доступа getCart:

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

Небольшое отступление о модификаторах видимости

Модификаторы видимости - public, private и protected - не только управляют тем, как работает наследование от родительского (базового) класса к дочернему классу, они также определяют, можно ли получить доступ к свойствам и методам извне класса. public свойства и методы открыты для внешнего мира, а private и protected заблокированы.

Изменение спецификатора видимости конструктора с public на private ограничивает доступ к конструктору исключительно внутри класса. Ключевое слово new больше не может использоваться извне для создания экземпляра ShoppingCart.

Перегрузка функций (и методов)

Другие объектно-ориентированные языки, такие как C ++ и Java, позволяют писать несколько функций с одним и тем же именем, но с другим набором параметров:

Код выше - это Java. Мы напишем версию TypeScript, но концепция перегрузки функций более ясна в Java, чем в TypeScript.

У нас есть четыре версии функции greet. Они различаются друг от друга количеством и типами параметров, то есть сигнатурами.

Функция main, с которой наше приложение начинает выполнение кода, вызывает greet четыре раза. Java сопоставит количество аргументов и их типы, чтобы определить, какую greet функцию использовать.

Мы также можем повторно использовать код из одной версии функции внутри другой, составляя их вместе в цепочку: функция greet, которая принимает две строки, вызывает версию, которая принимает одну строку, которая, в свою очередь, вызывает версию без аргументов, который, в свою очередь, печатает слово Hello. Сами перегруженные функции не обязательно должны содержать код для печати Hello.

К сожалению, в TypeScript нет этой элегантной и простой версии перегрузки функций:

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

Мы определяем тело только для одной версии нашей функции. Эта версия является наиболее общей формой с точки зрения допустимых параметров. См. Функцию в строке 155 в приведенном выше примере.

param1 - обязательный параметр, поэтому все перегруженные версии должны содержать хотя бы один аргумент. Возможные типы аргументов для param1 для любой данной перегрузки должны быть одним из типов, указанных в общей форме: number, string или undefined.

Второй аргумент, param2, помечен как необязательный, поэтому мы можем создавать перегрузки с одним или двумя аргументами. Если мы создадим перегрузку со вторым параметром, она может быть только типа string; см. строку 151.

Хотя у нас нет версии, которая могла бы принимать нулевые аргументы, мы можем создать функциональный эквивалент, разрешив param1 быть неопределенным.

Логика внутри наиболее общей версии (строка 155) предназначена для учета всех комбинаций количества аргументов и типов, описываемых каждой перегрузкой.

Небольшое отступление о функции и перегрузке методов

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

Дженерики и ключи объектов

Последняя тема, которую мы рассмотрим в этой статье, касается универсальных типов и объектных ключей. Давайте создадим функцию, которая принимает массив объектов, а также некоторый ключ, а затем распечатывает значение данного ключа для каждого объекта. Мы также предоставим массив объектов:

Теперь вызовем printValues один раз с действительным ключом и один раз с недопустимым:

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

Обратите внимание, что мы также изменили тип key с string на K. Теперь мы не можем вызвать нашу функцию с недопустимым ключом lastName:

Мы можем и должны также ограничивать T:

Заключение

Я надеюсь, что вы нашли информацию в этой статье информативной и полезной. Удачи вам в путешествии в мир TypeScript.