Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"responsive-preview": {
"Mobile": [320, 675],
"Tablet": [1024, 765],
"Desktop": [1400, 800],
"Desktop HD": [1920, 1080]
}
}
13 changes: 13 additions & 0 deletions lessons/lesson25/code/eventEmitter/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<html>
<head>
<title>Parcel Sandbox</title>
<meta charset="UTF-8" />
</head>

<body>
<h1>Event Emitter / Event Bus</h1>
<div id="app"></div>

<script src="src/index.ts"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions lessons/lesson25/code/eventEmitter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "event-emitter",
"version": "1.0.0",
"description": "",
"main": "index.html",
"scripts": {
"start": "parcel index.html --open",
"build": "parcel build index.html"
},
"dependencies": {
"@types/jest": "26.0.21",
"parcel-bundler": "^1.6.1"
},
"devDependencies": {
"typescript": "4.2.3"
},
"resolutions": {
"@babel/preset-env": "7.13.8"
},
"keywords": []
}
80 changes: 80 additions & 0 deletions lessons/lesson25/code/eventEmitter/src/EventEmitter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { EventEmitter } from "./EventEmitter";

describe("EventEmitter", () => {
describe("formal public interface", () => {
it("is a constructor", () => {
expect(typeof EventEmitter).toBe("function");
expect(new EventEmitter() instanceof EventEmitter).toBe(true);
});

it("has public methods", () => {
const eventEmitter = new EventEmitter();

expect(typeof eventEmitter.on).toBe("function");
expect(typeof eventEmitter.off).toBe("function");
expect(typeof eventEmitter.trigger).toBe("function");
});
});

describe("runtime logic", () => {
let eventEmitter: EventEmitter;
const eventName = "eventName";
const eventPayload = { name: "Bob" };

beforeEach(() => {
eventEmitter = new EventEmitter();
});
it("allows to listen to the events", () => {
const spy = jest.fn();
eventEmitter.on(eventName, spy);
expect(spy).not.toHaveBeenCalled();
eventEmitter.trigger(eventName, eventPayload);
expect(spy).toHaveBeenCalledWith(eventPayload);
});

it("supports multiple listeners for the event", () => {
const spy1 = jest.fn();
const spy2 = jest.fn();
eventEmitter.on(eventName, spy1);
eventEmitter.on(eventName, spy2);
expect(spy1).not.toHaveBeenCalled();
expect(spy2).not.toHaveBeenCalled();
eventEmitter.trigger(eventName, eventPayload);
expect(spy1).toHaveBeenCalledWith(eventPayload);
expect(spy2).toHaveBeenCalledWith(eventPayload);
});

it("allows to unsubscribe from the events", () => {
const spy1 = jest.fn();
const spy2 = jest.fn();
eventEmitter.on(eventName, spy1);
eventEmitter.on(eventName, spy2);
eventEmitter.off(eventName, spy1);
eventEmitter.trigger(eventName, eventPayload);
expect(spy1).not.toHaveBeenCalled();
expect(spy2).toHaveBeenCalledWith(eventPayload);
});

describe("edge cases", () => {
it("handles events with no listeners", () => {
expect(() => {
eventEmitter.trigger(eventName, eventPayload);
}).not.toThrowError();
});

it("handles invalid unsubscriptions", () => {
eventEmitter.on("x", jest.fn());
expect(() => {
eventEmitter.off("x", () => {});
eventEmitter.off(eventName, () => {});
}).not.toThrowError();
});

it("handles triggers with no subscriptions", () => {
expect(() => {
eventEmitter.trigger(eventName, eventPayload);
}).not.toThrowError();
});
});
});
});
3 changes: 3 additions & 0 deletions lessons/lesson25/code/eventEmitter/src/EventEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class EventEmitter {
// @todo: put your code here
}
21 changes: 21 additions & 0 deletions lessons/lesson25/code/eventEmitter/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// import { EventEmitter } from "./EventEmitter";

// (document.querySelector("#app") as HTMLElement).innerHTML = `
// <input name="input1" placeholder="Enter some text..." />
// <h1></h1>
// <input name="input2" placeholder="Enter some text..." />
// `;
// const input1 = document.querySelector("input[name=input1") as HTMLInputElement;
// const input2 = document.querySelector("input[name=input2") as HTMLInputElement;
// const header = document.querySelector("h1") as HTMLHeadingElement;

// const eventEmitter = new EventEmitter();

// eventEmitter.on("changeText", (text) => (header.innerHTML = text));

// input1.addEventListener("keypress", (ev) =>
// eventEmitter.trigger("changeText", (ev.target as HTMLInputElement).value)
// );
// input2.addEventListener("keypress", (ev) =>
// eventEmitter.trigger("changeText", (ev.target as HTMLInputElement).value)
// );
13 changes: 13 additions & 0 deletions lessons/lesson25/code/eventEmitter/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"strict": true,
"module": "commonjs",
"jsx": "preserve",
"esModuleInterop": true,
"sourceMap": true,
"allowJs": true,
"lib": ["es6", "dom"],
"rootDir": "src",
"moduleResolution": "node"
}
}
Binary file added lessons/lesson25/images/EventBus.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added lessons/lesson25/images/ObservableUML.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
226 changes: 225 additions & 1 deletion lessons/lesson25/lesson.md
Original file line number Diff line number Diff line change
@@ -1 +1,225 @@
# Lesson 25
---
title: Занятие 25
description: Связь модулей - от интерфейсов до EventBus
---

# OTUS

## Javascript Basic

<!-- v -->

## Вопросы?

<!-- s -->

## Связь модулей - от интерфейсов до EventBus

<!-- s -->

### Разберемся с задачей

<!-- v -->

Для начала два термина - **связность(_cohesion_)** и **связанность(_coupling_)**.

[Связность](<https://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D1%8F%D0%B7%D0%BD%D0%BE%D1%81%D1%82%D1%8C_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)>) - на сколько составные части направлены на решение одной задачи.

[Связанность](<https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D1%86%D0%B5%D0%BF%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)>) - на сколько одни модули зависят от других (и как много они знают друг о друге)

<!-- v -->

[Качественный дизайн обладает слабой связанностью (low coupling) и сильной связностью (high cohesion).](https://medium.com/german-gorelkin/low-coupling-high-cohesion-d36369fb1be9)

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

<!-- v -->

**Слабое зацепление (Low Coupling)** и **Высокая связность (High Cohesion)** это 2 из 9 [**шаблонов GRASP**](<https://ru.wikipedia.org/wiki/GRASP#4._%D0%A1%D0%BB%D0%B0%D0%B1%D0%BE%D0%B5_%D0%B7%D0%B0%D1%86%D0%B5%D0%BF%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5_(Low_Coupling)>)

<!-- v -->

Высокая связность говорит об эффективности программы (или ее отдельных модулей).

Низкая связанность означает легкость рефакторинга и переиспользуемость кода.

<!-- v -->

### Вопросы?

<!-- s -->

### Наблюдатель (Observer)

<!-- v -->

[Наблюдатель](https://refactoring.guru/ru/design-patterns/observer) - подход (паттерн), позволяющий одним объектам следить и реагировать на события, происходящие в других объектах.

<!-- v -->

<img src="./images/ObservableUML.png" title="Observable UML" />

<!-- v -->

На самом деле вы с ним уже работали - это [EventTarget](https://developer.mozilla.org/ru/docs/Web/API/EventTarget)

<!-- v -->

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

<!-- v -->

Может быть представлен как

<!-- eslint-skip -->

```ts
IObservable {
addObserver(event, handler)
removeObserver(event, handler)
notifyObserver(event, data)
}
```

<!-- v -->

или

<!-- eslint-skip -->

```ts
EventTarget {
addEventListener(event, handler)
removeEventListener(event, handler)
dispatchEvent(event)
}
```

<!-- v -->

или

<!-- eslint-skip -->

```ts
Backbone.Events {
on(event, handler)
off(event, handler)
trigger(event)
}
```

<!-- v -->

Иногда могут добавлять вспомогательные методы, например

<!-- eslint-skip -->

```ts
Backbone.Events {
// ...
once(event, handler)
}
```

<!-- v -->

```ts
document.querySelector(element).addEventListener("click", (ev) => {
alert("Boom!");
});
```

<!-- v -->

Оговорка: чаще всего обработчиком события является функция. Но это также может быть и объект ([EventListener](https://developer.mozilla.org/ru/docs/Web/API/EventListener)) - в зависимости от реализации.

<!-- v -->

### Вопросы?

<!-- s -->

### Посредник (Mediator)

<!-- v -->

[Посредник](https://refactoring.guru/ru/design-patterns/mediator) - это поведенческий паттерн проектирования, который позволяет уменьшить связанность множества классов между собой, благодаря перемещению этих связей в один класс-посредник.

<!-- v -->

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

**Решение:** Создать объект, инкапсулирующий способ взаимодействия множества объектов.

**Преимущества:** Устраняется связанность между "Коллегами", централизуется управление.

<!-- v -->

Самый распространенный (и простой) вариант реализации паттерна - с использованием **EventEmitter** интерфейса (**Event Bus** - Шина событий).

<!-- v -->

Разница, по сравнению с обычным использованием EventTarget:

- события в EventTarget генерирует сам объект, при работе с EventBus это делают сторонние объекты
- список событий при работе с EventTarget ограничен устройством объекта, при работе с EventBus он определяется участниками

<!-- v -->

<img src="./images/EventBus.jpeg" title="Event Bus" />

<!-- v -->

При этом, чтобы избежать коллизии имен событий, зачастую вводят `namespaces`, в формате **{NAMESPACE}:{EVENT NAME}**. Например `user:add`, `searchHistory:add`.

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

<!-- v -->

```ts
const eventBus = new EventBus();

// module 1
eventBus.on("city:changed", (cityName) => console.log(`New city: ${cityName}`));

// module 2
eventBus.trigger("city:changed", "Minsk");
```

<!-- v -->

Как мы могли бы применить это к уже сделанным домашним заданиям?

<!-- v -->

### Вопросы?

<!-- s -->

### Практика

<!-- v -->

[Реализовать Event Emitter](https://codesandbox.io/s/github/vvscode/otus--javascript-basic/tree/master/lessons/lesson33/code/eventEmitter)

<!-- v -->

Реализовать поверх существующего функционала метод **once** (для одноразового вызова обработчика).

<!-- v -->

### Вопросы?

<!-- s -->

Дополнительные материалы:

- [Backbone Events](https://backbonejs.org/#Events) и [исходники](https://backbonejs.org/docs/backbone.html#section-17)
- [EventTarget simple implementation](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)
- [Паттерны проектирования понятным языком](https://refactoring.guru/ru/design-patterns)
- [Design patterns for humans!](https://github.com/sohamkamani/javascript-design-patterns-for-humans)

<!-- v -->

### Опрос о занятии
3 changes: 3 additions & 0 deletions lessons/lesson28/code/routing-examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# vanilla-client-side-routing-examples

Created with CodeSandbox
Loading