Разница между ранним и поздним связыванием

от автора

В этой публикации я «на пальцах» попытаюсь объяснить, чем отличается раннее и позднее связывание кода для обычного программиста. Не для компилятора или статического анализатора, а для человека, который пишет JavaScript/TypeScript-код.

КДПВ

КДПВ

Для начала пара определений от «Игорь Иваныча» (ИИ), просто в качестве отправной точки:

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

Позднее связывание (late binding) — это процесс, при котором конкретная реализация метода или функции определяется на этапе выполнения программы, а не на этапе компиляции.

Пример

А теперь — по-простому. Вот TypeScript-код, который использует раннее связывание:

class Cat {     speak(): void {         console.log("Meow");     } }  function animalSound(cat: Cat): void {     cat.speak(); }  const myCat = new Cat(); animalSound(myCat);

А это — аналогичный TypeScript-код, который использует позднее связывание:

interface Animal {     speak(): void; }  class Cat implements Animal {     speak(): void {         console.log("Meow");     } }  function animalSound(animal: Animal): void {     animal.speak(); }  const myCat = new Cat();  animalSound(myCat);   

Вот в этом interface Animal и заключается вся разница.

Видно, что кода стало больше, но что нам это дало? А дало возможность декомпозиции нашего кода на составные части:

// animal.ts export interface Animal {     speak(): void; }  export function animalSound(animal: Animal): void {     animal.speak(); }
// cat.ts import {Animal} from './animal';  export class Cat implements Animal {     speak(): void {         console.log("Meow");     } }
// main.ts import {animalSound} from './animal'; import {Cat} from './cat';  const myCat = new Cat(); animalSound(myCat);

У нас получилась такая цепочка зависимостей:

animal.ts => cat.ts => main.ts

Если же мы попытаемся разбить «ранне-связанный» код, то у нас получится немного другая цепочка зависимостей:

cat.ts => animal.ts => main.ts
// cat.ts export class Cat {     speak(): void {         console.log("Meow");     } }
// animal.ts import {Cat} from "./cat";  export function animalSound(animal: Cat): void {     animal.speak(); }
// main.ts import {animalSound} from './animal' import {Cat} from './cat'  const myCat = new Cat(); animalSound(myCat);

Если мы захотим добавить dog в приложение, то animal.ts в коде с ранним связыванием примет вот такой вид:

// animal.ts import {Cat} from "./cat"; import {Dog} from "./dog";  export function animalSound(animal: Cat | Dog): void {     animal.speak(); }

А вот в коде с поздним связыванием animal.ts не изменится.

Вообще.

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

Инверсия Контроля

То есть, при позднем связывании разработчик «думает» не в категориях классов, которые он поставляет «наружу«, а в категориях интерфейсов, которые он получает «извне» или отдаёт туда же. Он либо сам определяет интерфейсы (требования к будущим потребителям его кода), либо отталкивается от интерфейсов, уже определённых внешним потребителем его кода.

Это несколько контр-интуитивно для тех, кто начинает изучать ООП с «Hello World!» и продолжает двигаться вперёд применяя только инкапсуляцию и наследование. Но как только впервые появляется потребность в полиморфизме, появляется возможность посмотреть на свой код с точки зрения уже позднего связывания.

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

Контейнер Объектов

В случае раннего связывания наш код все зависимости тянет через статические импорты:

import {Cat} from './cat';

Тут всё понятно — и сами разработчики, и куча инструментов (IDE, транспиляторы, анализаторы, …) умеют в статические импорты.

А как же подтягиваются зависимости в случае позднего связывания? Ведь на момент написания кода мы знаем только интерфейсы зависимостей («ходит как утка» и вот этот вот всё)? Кто на момент выполнения кода определяет, какой исходный код, имплементирующий соответствующий интерфейс, должен быть загружен и выполнен, чтобы получить нужную зависимость?

В классическом «кровавом энтерпрайзе» (Java, C#) уже давно ответили на этот вопрос — в приложении должен быть объект, который знает как, когда и какие объекты создавать и когда и куда их внедрять. Обычно его называют «контейнер объектов«.

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

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

import {animalSound} from './animal'; import {Cat} from './cat';  const cat = new Cat();  export class CatSound {     makeSound() {         animalSound(cat);     } }

Вы даёте возможность контейнеру объектов предоставить в ваш код нужные зависимости. Например, через конструктор (пример ниже — это уже JavaScript):

export class CatSound {     /**      * @param {Cat} cat      * @param {function(animal: Cat): void} animalSound      */     constructor(cat, animalSound) {         this.makeSound = function () {             animalSound(cat);         };     } }

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

Я в TypeScript не силён, этот код, аналогичный предыдущему, мы писали с «Игорь Иванычем«:

import {Cat} from './cat';  export class CatSound {     constructor(private cat: Cat, private animalSound: (animal: Cat) => void) {     }      public makeSound(): void {         this.animalSound(this.cat);     } }

Так вот, после компиляции в JavaScript статические импорты исчезли, как ненужные:

export class CatSound {     constructor(cat, animalSound) {         this.cat = cat;         this.animalSound = animalSound;     }     makeSound() {         this.animalSound(this.cat);     } }

Что и ожидаемо — ведь наш код ориентирован на позднее связывание, на runtime.

Резюме

На мой взгляд, разницу между типами связывания для программиста, вульгарно, можно свести к следующему:

  • раннее: работаем с классами и создаём объекты сами.

  • позднее: работаем с контрактами (интерфейсами) и используем готовые объекты, которые предоставляет нам контейнер.

КДПВ как раз демонстрирует идею раннего связывания — вы строите своё приложение из исходников и сами создаёте нужные вам объекты.

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

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

  • InversifyJS — Мощный DI-контейнер для TypeScript и JavaScript с поддержкой декораторов и аннотаций типов.

  • Awilix — Гибкий и лёгкий DI-контейнер для Node.js, оптимизированный для Express и модульных приложений.

  • BottleJS — Минималистичный DI-контейнер для JavaScript, поддерживающий фабрики и сервисы.

Ну и по традиции — немного саморекламы. Подписывайтесь на мой телеграм-канал попробуйте мою библиотеку!!

  • teqfw/di — DI-контейнер для модульной разработки на JavaScript с минимальной конфигурцией, поддерживающий автозагрузку.

Если будут вопросы по использованию — с интересом отвечу.

Хэппи, как говорится, кодинг…

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

Какой тип связывания вы используете в своих проектах на JavaScript или TypeScript?

16.67% Использую раннее связывание2
16.67% Использую позднее связывание2
25% В зависимости от проекта3
41.67% Не задумываюсь об этом5

Проголосовали 12 пользователей. Воздержались 3 пользователя.

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

Я использую позднее связывание и я пишу код на:

33.33% TypeScript2
66.67% JavaScript4
0% на обоих языках0

Проголосовали 6 пользователей. Воздержались 5 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/856578/


Комментарии

Добавить комментарий

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