Я уже писал статью об абстрактных классах и интерфейсах в PHP Интерфейсы PHP и абстрактные классы. Когда использовать какой. объясняет, что каждый из них и когда использовать каждый в объектно-ориентированном программировании PHP. Поскольку сейчас наблюдается тенденция к JavaScript и, в частности, к TypeScript, я почувствовал, что должен написать еще одну статью, чтобы показать и объяснить те же функции в TypeScript из-за важности абстрактных классов и интерфейсов в TypeScript, чтобы подчеркнуть ООП. природа этого надмножества языка JavaScript. Итак, приступим 🙂.

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

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

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

Интерфейсы TypeScript

Цитата из определения TutorialsTeacher.com интерфейса TypeScript:

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

Что это значит? Давайте посмотрим на пример.

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

Я создам новую папку. Я назову это глупым именем, например «ts-if-ac», которое должно означать «TypeScript-InterFace-AbstractClasses». 😁

Затем мне нужно установить компилятор TypeScript, чтобы я мог запускать код TypeScript. Я работаю в среде Windows.

md ts-if-ac
cd ts-if-ac
npm init -y
npm install -g typescript

Проще говоря, я создал новую папку, перешел в новую папку, инициализировал npm package.json, а затем установил компилятор TypeScript.

Я не буду углубляться в настройку TypeScript в среде узла, так как это выходит за рамки этой статьи.

Предположим, у нас есть функция Squeeze(), которая ожидает объект Fruits. И представьте, что эта функция является общей в модуле, который будет импортирован несколькими разработчиками, у каждого из которых может быть своя реализация объекта Fruit, переданного в эту функцию. Функции может потребоваться доступ к определенным свойствам в переданном объекте, и если они недоступны, это может привести к тому, что система выдаст ошибку и, возможно, сбой. И, конечно же, вы не хотите, чтобы это произошло.

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

Итак, давайте определим наш интерфейс проекта, чтобы другие разработчики могли следовать им при определении своих классов Fruits. Мы назовем его IFruit, поскольку соглашение об именах заключается в добавлении «I» (заглавной или маленькой) перед предполагаемым классом или типом объекта. На данный момент интерфейс будет содержать только свойства фруктов, и, поскольку это интерфейс, у них будут только определения без значений.

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

interface IFruit {
  color: string,
  taste: string,
  count: number,
  juicy: boolean
}

function Squeeze(fruit: IFruit) {
  if (fruit.juicy) ... // do the squeeze steps
  else return 'This fruit is not squeezable';
}

Если есть свойства, которые являются необязательными, их можно пометить как «необязательные», используя вопросительный знак «?» как таковой:

interface IFruit {
    name: string,
    color: string,
    taste: string,
    weight?: number,
    count: number,
    juicy: boolean,    
}

Здесь вес является необязательным свойством, что означает, что его не обязательно включать в реализацию объектов, определенных как IFruit.

Теперь, если у нас есть объект, объявленный с использованием интерфейса IFruit, или он содержит как минимум свойства, определенные в интерфейсе IFruit, функция будет счастлива выполниться. В противном случае функция будет жаловаться и выдавать ошибку.

Я проиллюстрирую это в коде, используя Visual Studio Code.

Как видите, параметр apple подчеркнут красным, что указывает на ошибку. Ошибка TypeScript появляется при наведении указателя мыши на параметр apple.

Так что же здесь происходит? 🤔

Поскольку мы объявили, что параметр функции Squeeze должен иметь интерфейс IFruit, любой объект, передаваемый этой функции, должен содержать те же свойства, что и IFruit. Те же имена и те же типы. Он может парить больше, но не меньше. И у этого объекта-яблока есть название, цвет и свойства сочности, но отсутствуют свойства вкуса и количества! Итак, давайте добавим их и посмотрим, понравится ли это функции Squeese() 🙂.

Похоже, проблема решена, потому что переданный параметр имеет все необходимые параметры, объявленные интерфейсом. Давайте рассмотрим еще один более информативный способ объявления объекта apple с помощью интерфейса в объявлении. Мы можем объявить объект яблока типа IFruit. В этом случае TypeScript ожидает, что я включу все свойства и объявления интерфейса. Но что, если я не знаю, что включить в задание? При инициализации или присвоении свойств объекту с пустым объектом {} мы получим ошибку о том, что свойства отсутствуют. Как показано ниже. Это можно исправить, нажав Quick Fix… или Ctrl+. и выбрав опцию «Добавить отсутствующие свойства». Это добавит отсутствующие свойства интерфейса к нашему объекту.

После исправления:

Давайте расширим интерфейс, чтобы включить некоторые методы. Я добавлю метод slice(), который будет принимать число и возвращать строку. Как и свойства, методы в интерфейсах не включают реализации, а только сигнатуры. Я также реализую функцию в объекте apple.

interface IFruit {
    name: string,
    color: string,
    taste: string,
    count: number,
    juicy: boolean,

    slice: (n: number) => string;
}

function Squeeze(fruit: IFruit) {
  if (fruit.juicy) return `Here is you ${fruit.name} juice.` 
  else return 'This fruit is not squeezable';
}

let apple: IFruit = {
    name: "Apple",
    color: "red",
    taste: "sweet",
    count: 10,
    juicy: true,
    slice: function (n: number): string {
        return `Here are ${n} slices of ${this.name}`
    }
}

Squeeze(apple);

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

Вернувшись на шаг назад, мы увидели пример объявления Apple без использования IFruit в определении. Итак, в чем разница между обоими подходами? Давайте взглянем. Я дам определение яблоку и апельсину так, как они приемлемы для функции Squeeze(). Один с интерфейсом и один без. Я не буду повторять определения интерфейса и функций, так как они останутся прежними.

let apple = {
    name: "",
    color: "",
    taste: "",
    count: 10,
    juicy: true,
    slice: function (n: number): string {
        return `Here are ${n} slices of ${this.name}`
    }
}

let orange: IFruit = {
    name: 'Orange',
    color: "orange",
    taste: "sour",
    count: 5,
    juicy: true,
    slice: function (n: number): string {
        return `Here are ${n} slices of ${this.name}`
    }
}

Squeeze(apple);
Squeeze(orange);

Как видите, оба плода имеют одинаковое количество свойств и методов. Но Orange использует IFruit для определения типа объекта, в то время как у Apple просто оказался правильный набор свойств и методов. Что делать, если мне нужно добавить к объектам больше свойств или методов? Что случится? Взгляните на это.

Хотя Squeeze() не жаловалась на то, что оранжевый получил дополнительный параметр, TypeScript выдал ошибку, потому что этот дополнительный параметр не является частью определения IFruit! Видите ли, в случае с яблоком мы не указали какой-либо другой тип, кроме объекта, поэтому TypeScript счел нормальным иметь любые реквизиты и методы, но в случае с апельсином вы вводили его как IFruit, и someProperty не является частью этого определения типа.

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

Еще одна вещь, прежде чем переходить к интерфейсам с классами, заключается в том, что вы можете получить новые интерфейсы из других интерфейсов из других интерфейсов. Скажем, вы хотите иметь интерфейс IGroceryItem, который будет расширять два интерфейса, IFruit и IVegetable, чтобы он мог иметь свойства и методы обоих и иметь больше собственных свойств. Во-первых, вам нужно убедиться, что все свойства или методы с похожими именами в расширенных интерфейсах имеют одинаковые типы, иначе вы получите ошибку.

Интерфейс IGroceryItem может иметь свои собственные свойства и методы, если они не имеют тех же имен, что и свойства и методы расширенных интерфейсов, или если они имеют такие же штампы типа.

interface IFruit {
    name: string,
    color: string,
    taste: string,
    count: number,
    juicy: boolean,

    slice: (n: number) => string,
}

interface IVeg {
    name: string,
    leafy: () => boolean,
    green: boolean

}

interface IGrocerryItem extends IFruit, IVeg {
    weight?: number,
    price: number,
    name: string
}

let myGroceryItem: IGrocerryItem ={
    price: 0,
    name: "",
    color: "",
    taste: "",
    count: 0,
    juicy: false,
    slice: function (n: number): string {
        throw new Error("Function not implemented.");
    },
    leafy: function (): boolean {
        throw new Error("Function not implemented.");
    },
    green: false
};

Теперь давайте посмотрим на интерфейсы с классами! 😃

Классы, использующие интерфейсы

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

interface IFruit {
    name: string,
    color: string,
    taste: string,
    count: number,
    juicy: boolean,

    slice: (n: number) => string,
}

interface IVeg {
    name: string,
    leafy: () => boolean,
    green: boolean

}

class GroceryItem implements IFruit, IVeg {
    leafy: () => boolean;
    green: boolean;
    // leafy: boolean;
    name: string;
    color: string;
    taste: string;
    weight?: number | undefined;
    count: number;
    juicy: boolean;
    slice (n: number) {
        return `Here are ${n} slices of ${this.name}`;
    }
    
    moreOptions: string;
}

let myGroceryItem = new GroceryItem();

Обзор интерфейсов:

  1. Интерфейсы — это чертежи. Они не могут быть созданы. Они предоставляют минимально необходимые методы для определения классов и типизации аргументов функций.
  2. Они содержат только типы свойств и сигнатуры методов. Реализации методов оставлены для классов и объектов.
  3. Все их методы общедоступны и не могут содержать свойств.
  4. Классы и объекты должны включать все свойства и методы, определенные интерфейсами, которые они реализуют.

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

Этот раздел не будет таким длинным, как интерфейсы. Обещаю😁.

Цитата из определения абстрактных классов TypeScript www.typescriptlang.org:

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

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

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

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

Итак, если мы хотим иметь абстрактный класс AFruit, который будет содержать абстрактный метод slice(), аналогичный примеру с интерфейсом IFruit ранее, он будет таким:

abstract class AFruit {
    name: string;
    abstract slice(n: number) : string;
}

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

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

abstract class AFruit {
    name: string;
    abstract slice(n: number) : string;
}

class myFruit extends AFruit {
    // although the property name is not seen here, 
    // it is still a property of the new myFruit class
    slice(n: number): string {
        throw new Error("Method not implemented.");
    }
}

Пожалуйста, обратите внимание на следующее:

  1. Любой класс, содержащий абстрактные методы, должен быть объявлен как абстрактный класс.
  2. Абстрактныеметоды определяют сигнатуры методов и не могут определять реализацию.
  3. Абстрактныеклассы могут расширять другие абстрактныеклассы, что приводит к расширению классов, наследующих определенные методы и свойства, и, как и в интерфейсах, к необходимости реализации всех абстрактных методов всех расширенных абстрактных классов.
  4. Классы могут расширять только один класс (т. е. могут наследоваться только от одного класса с помощью ключевого слова extends). Это отличается от разрешенных множественных реализаций интерфейсов.

Вот и все, что касается абстрактныхклассов. 😃

Сводка абстрактных классов:

  1. Абстрактныеклассы действуют как шаблоны для новых определений классов, где вы можете предопределять методы, свойства и константы, позволяя переопределять методы и принудительно реализовывать некоторые методы. пользователями.
  2. Классы, определяющие абстрактныеметоды, должны быть определены как абстрактныеклассы.
  3. Все абстрактныеметоды в расширенном (унаследованном) классе должны быть реализованы расширяющим (наследующим) классом.
  4. Абстрактныеклассы могут содержать элементы с различными модификаторами (public, private и protected), в отличие от интерфейсов, в которых разрешены только общедоступные члены.
  5. Абстрактные классы могут иметь статические члены, если они завершены (т. е. константы или реализованные методы).

Вот пример, иллюстрирующий вышесказанное.

abstract class AFruit {
    name: string;
    abstract slice(n: number) : string;
    private weight: number
}

abstract class AGrocery extends AFruit {
    abstract price(w: number, r: number) : number;
    private unitPrice: number = 3.4;

    static calcPrice(w: number, uPrice: number) {
        return w * uPrice;
    }
}

class Grocery extends AGrocery {
    // from AGrocery abstract class
    price(w: number, r: number): number {
        throw new Error("Method not implemented.");
    }

    // from AFruit abstract class
    slice(n: number): string {
        throw new Error("Method not implemented.");
    }
}

Итак, когда что использовать?

Теперь, когда мы рассмотрели и интерфейсы, и абстрактные классы, какой из них когда используется? или когда использовать какой? 😄

Интерфейсы:

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

Они не подходят, когда вам нужен класс для наследования методов и свойств, поскольку интерфейсы не могут иметь ни свойств, ни реализаций методов.

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

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

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

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

Также не забывайте хлопать в ладоши и подписываться на меня, если вы еще не подписчик.

Спасибо, что прочитали мою статью 😃.