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

Паттерны

DRY, KISS, YAGNI, SOLID

DRY — Don’t Repeat Yourself (Не повторяйся)

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

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

KISS — Keep It Simple, Stupid (будь проще)

  1. Методы должны быть маленькими и решать одну проблему.
  2. Код должен быть простым и понятным.
  3. Избегайте ненужной сложности.
  4. Не устанавливайте целую библиотеку ради одной функции.
  5. Не добавляйте функционал, который не требуется.

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

**YAGNI — You Aren’t Gonna Need It (Вам это не понадобится)

  1. Реализуйте только то, что нужно сейчас.
  2. Удаляйте ненужный код.
  3. Не добавляйте функционал без необходимости.

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

SOLID

  1. S - Принцип единственной ответственности (Single Responsibility Principle, SRP): Каждый кусок кода должен делать только одну вещь хорошо. Например, если у вас есть функция, которая читает данные из базы данных и выводит их на экран, лучше разделить её на две функции: одну для чтения данных и другую для вывода.

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

  1. O - Принцип открытости/закрытости (Open/Closed Principle, OCP): Код должен быть такой, чтобы можно было легко добавлять новые функции, не меняя старые. Например, если у вас есть программа для игры в шахматы, вы должны смочь добавить новые фигуры без изменения основного кода игры.

Представьте, что у вас есть класс Rectangle, который может вычислять площадь прямоугольника. Если вам понадобится добавить новую форму, например, треугольник, вам придется изменить класс Rectangle, что нарушает принцип открытости/закрытости. Лучше создать базовый класс Shape с методом area, который вызывает ошибку, и затем создать классы для каждой формы, которые наследуются от Shape и реализуют метод area.

  1. L - Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP): Если у вас есть класс "Машина" и класс "Автомобиль", который наследуется от "Машины", то "Автомобиль" должен вести себя так же, как и "Машина". То есть, если вы можете использовать "Машину" вместо "Автомобиля", то код должен работать правильно.

Представьте, что у вас есть класс Bird, который имеет метод fly. Если вы создаете подкласс Penguin, который не может летать, но наследуется от Bird, это нарушает принцип подстановки Барбары Лисков. Лучше создать отдельный класс FlyingBird с методом fly и класс Penguin, который не наследуется от FlyingBird.

  1. I - Принцип разделения интерфейса (Interface Segregation Principle, ISP): Лучше иметь много маленьких интерфейсов, чем один большой. Это значит, что если у вас есть класс, который должен работать с несколькими разными вещами, лучше разделить эти вещи на отдельные интерфейсы, а не объединять их в один большой интерфейс.

Представьте, что у вас есть интерфейс Worker, который требует методов work и eat. Если у вас есть класс Robot, который не ест, но должен реализовывать интерфейс Worker, это нарушает принцип разделения интерфейса. Лучше создать два интерфейса: Worker с методом work и Eater с методом eat, и класс Robot должен реализовывать только интерфейс Worker.

  1. D - Принцип инверсии зависимостей (Dependency Inversion Principle, DIP): Код должен зависеть от абстракций, а не от конкретных деталей. Например, если у вас есть класс, который использует конкретный класс для чтения данных, лучше передать эту зависимость через параметры или конструктор, чтобы класс мог работать с любым источником данных, а не только с одним конкретным.

Представьте, что у вас есть класс UserRepository, который напрямую зависит от класса Database для сохранения данных. Если вам понадобится изменить способ хранения данных, например, перейти с MySQL на PostgreSQL, вам придется изменить класс UserRepository. Это нарушает принцип инверсии зависимостей. Лучше сделать так, чтобы UserRepository зависел от абстракции Database, а не от конкретного класса, и передавать конкретную реализацию Database через конструктор или метод.

БЭМ

Методология именования элементов DOM дерева.

  • Блок (Block): Блок — это часть веб-страницы, которую можно повторно использовать. В HTML мы используем class для этого.
  • Элемент (Element): Элемент — это маленькая часть блока, которую нельзя использовать отдельно.
  • Модификатор (Modifier): Модификатор — это класс, который меняет внешний вид или поведение блока или элемента.
<div class="block block--big">
<div class="block__element block__element--red">
<!-- Здесь что-то интересное -->
</div>
</div>

Здесь block — это блок, block--big — делает его большим, block__element — это элемент внутри блока, а block__element--red — делает его красным.

ООП

Основные принципы объектно-ориентированного программирования (ООП) в JavaScript

Классы и объекты: Классы представляют собой шаблоны, описывающие атрибуты (поля) и методы (функции) объектов. Объекты являются конкретными экземплярами классов.

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

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

Полиморфизм: Полиморфизм позволяет объектам с одинаковым интерфейсом вести себя по-разному в зависимости от их типа или контекста использования.

Абстракция: Абстракция позволяет скрыть сложность реализации объектов за простым интерфейсом. Это делает код более понятным и удобным для использования.

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

Пример на классах:

// Создаем абстрактный класс Animal
class Animal {
constructor(name) {
this._name = name; // Используем _name для инкапсуляции (Приватное свойство)
}

// Метод для получения имени (Геттер)
getName() {
return this._name;
}

// Сеттер для изменения имени
setName(newName) {
this._name = newName;
}

// Метод, который будет реализован в дочерних классах
makeSound() {
throw new Error('The makeSound method must be implemented');
}
}

// Создаем классы-наследники
class Dog extends Animal {
constructor(name, breed) {
super(name);
this._breed = breed;
}

// Полиморфизм - переопределяем метод makeSound
makeSound() {
return 'Woof!';
}

// Метод, специфичный для класса Dog
fetch() {
return this._name + ' fetches the ball.';
}
}

class Cat extends Animal {
constructor(name, color) {
super(name);
this._color = color;
}

// Полиморфизм - переопределяем метод makeSound
makeSound() {
return 'Meow!';
}

// Метод, специфичный для класса Cat
purr() {
return this._name + ' purrs.';
}
}

// Создаем объекты
let dog = new Dog('Buddy', 'Golden Retriever');
let cat = new Cat('Whiskers', 'Gray');

// Установим новое имя с помощью сеттера
// dog.setName("Max");

// Используем полиморфизм для вызова makeSound
console.log(dog.getName() + ': ' + dog.makeSound()); // Выведет: Buddy: Woof!
console.log(cat.getName() + ': ' + cat.makeSound()); // Выведет: Whiskers: Meow!

// Вызываем методы, специфичные для каждого класса
console.log(dog.fetch()); // Выведет: Buddy fetches the ball.
console.log(cat.purr()); // Выведет: Whiskers purrs.

Пример на прототипах:

// Создаем конструктор для абстрактного класса Animal
function Animal(name) {
this._name = name; // Приватное свойство для инкапсуляции
}

// Метод для получения имени (Геттер)
Animal.prototype.getName = function () {
return this._name;
};

// Метод, который будет реализован в дочерних классах
Animal.prototype.makeSound = function () {
throw new Error('The makeSound method must be implemented');
};

// Создаем конструкторы для классов-наследников
function Dog(name, breed) {
Animal.call(this, name);
this._breed = breed;
}

function Cat(name, color) {
Animal.call(this, name);
this._color = color;
}

// Наследуем прототипы класса Animal
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

// Полиморфизм - переопределяем метод makeSound для каждого класса
Dog.prototype.makeSound = function () {
return 'Woof!';
};

Cat.prototype.makeSound = function () {
return 'Meow!';
};

// Метод, специфичный для класса Dog
Dog.prototype.fetch = function () {
return this._name + ' fetches the ball.';
};

// Метод, специфичный для класса Cat
Cat.prototype.purr = function () {
return this._name + ' purrs.';
};

// Создаем объекты
let dog = new Dog('Buddy', 'Golden Retriever');
let cat = new Cat('Whiskers', 'Gray');

// Используем полиморфизм для вызова makeSound
console.log(dog.getName() + ': ' + dog.makeSound()); // Выведет: Buddy: Woof!
console.log(cat.getName() + ': ' + cat.makeSound()); // Выведет: Whiskers: Meow!

// Вызываем методы, специфичные для каждого класса
console.log(dog.fetch()); // Выведет: Buddy fetches the ball.
console.log(cat.purr()); // Выведет: Whiskers purrs.

Классы и функциональное разделение:

Когда речь идет о структурировании кода, часто выделяют два основных подхода: классы и функциональное разделение.

Классы:

Классы представляют собой основной инструмент объектно-ориентированного программирования (ООП). Они объединяют данные и методы, работающие с этими данными, внутри одной структуры.

class Car {
constructor(make, model) {
this.make = make;
this.model = model;
}

start() {
console.log(`Starting ${this.make} ${this.model}`);
}
}

const myCar = new Car('Toyota', 'Camry');
myCar.start();

Функциональное разделение:

Функциональное разделение означает разделение кода на функции, которые выполняют конкретные задачи. Это структура кода, основанная на функциях, а не на классах. Обычно используется в функциональном программировании, но также может быть применено в JavaScript.

function createCar(make, model) {
return {
make,
model,
start() {
console.log(`Starting ${make} ${model}`);
}
};
}

const myCar = createCar('Toyota', 'Camry');
myCar.start();

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

Рекомендации:

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

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

Чем отличается микросервисная архитектура от монолита?

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

Монолитная архитектура:

  • В монолитной архитектуре все компоненты приложения (функциональность, пользовательский интерфейс, база данных и т.д.) объединены в одном кодовом репозитории и работают вместе как единое целое.
  • Это означает, что изменения в одной части приложения могут затронуть другие части, и для внесения изменений необходимо перекомпилировать и перезапустить всё приложение.
  • Монолиты обычно легче в разработке и тестировании, так как все компоненты находятся в одном месте и могут взаимодействовать напрямую.
  • Однако монолиты могут стать сложными и трудными для масштабирования, поскольку все компоненты тесно связаны и изменения в одной части могут повлиять на другие.

Микросервисная архитектура:

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

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

Декоратор

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

Пирамида тестирования

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

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

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

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

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

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

Алгоритмы

  • Определение: Последовательность шагов для решения задачи или достижения цели.
  • Сложность: Измеряет, насколько эффективно алгоритм использует ресурсы (время, память) в зависимости от размера входных данных.

Сложность алгоритмов:

  • O(1): Константная сложность. Время выполнения не зависит от размера входных данных.
  • O(n): Линейная сложность. Время выполнения прямо пропорционально размеру входных данных.
  • O(log n): Логарифмическая сложность. Время выполнения пропорционально логарифму размера входных данных.
  • O(n log n): Сложность, промежуточная между линейной и квадратичной.
  • O(n^2): Квадратичная сложность. Время выполнения пропорционально квадрату размера входных данных.
  • O(2^n): Экспоненциальная сложность. Время выполнения растет экспоненциально с увеличением размера входных данных.
  • O(n!): Факториальная сложность. Время выполнения растет очень быстро с увеличением размера входных данных.

Типы алгоритмов:

  • Поиск: Найти элемент в структуре данных.
  • Сортировка: Упорядочить элементы в определенном порядке.
  • Обход: Пройти по всем элементам структуры данных.
  • Динамическое программирование: Решить задачу, разбив её на подзадачи и сохраняя промежуточные результаты.
  • Графические алгоритмы: Решать задачи на графах, такие как поиск кратчайшего пути.

Алгоритмы сортировки:

  • Bubble Sort: Сравнивает соседние элементы и меняет их местами, если они в неправильном порядке.
  • Merge Sort: Разделяет массив на две половины, сортирует их и затем сливает в один отсортированный массив.
  • Quick Sort: Выбирает "опорный" элемент и разделяет массив на две части, так что элементы меньше опорного идут в одну часть, а больше — в другую.

Алгоритмы поиска:

  • Linear Search: Проходит по массиву и сравнивает каждый элемент с искомым значением.
  • Binary Search: Использует бинарный поиск для поиска элемента в отсортированном массиве.

Алгоритмы динамического программирования:

  • Fibonacci Sequence: Вычисляет числа Фибоначчи с помощью рекурсии и мемоизации.
  • Knapsack Problem: Решает задачу о рюкзаке, выбирая наиболее ценные предметы для упаковки в рюкзак с ограниченным пространством.

Алгоритмы на графах:

  • Dijkstra's Algorithm: Находит кратчайший путь от одной вершины графа до всех остальных.
  • Breadth-First Search (BFS): Обходит граф в ширину, начиная с заданной вершины.
  • Depth-First Search (DFS): Обходит граф в глубину, начиная с заданной вершины.

Алгоритмы оптимизации:

  • Greedy Algorithms: Принимают локально оптимальное решение на каждом шаге, чтобы найти глобально оптимальное решение.
  • Dynamic Programming: Решает задачу, разбивая её на подзадачи и сохраняя промежуточные результаты.

Алгоритмы машинного обучения:

  • Linear Regression: Предсказывает числовые значения на основе линейной зависимости от признаков.
  • Decision Trees: Создает дерево решений для классификации или регрессии.
  • Neural Networks: Использует искусственные нейронные сети для обучения на больших наборах данных.