SOLID

S: Single Responsibility Principle

O: Open-Closed Principle

L: Liskov Substitution Principle

I: Interface Segregation Principle

D: Dependency Inversion Principle

Принцип единственной ответственности

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

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

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

class News {
    constructor(title, text) {
        this.title = title
        this.text = text
        this.modified = false
    }

    update(text) {
        this.text = text
        this.modified = true
    }
}

class NewsPrinter {
    constructor(news) {
        this.news = news
    }

    html() {
        return `
            <div class="news">
                <h1>${this.news.title}</h1>
                <p>${this.news.text}</p>
            </div>
        `
    }

    json() {
        return JSON.stringify({
            title: this.news.title,
            text: this.news.text,
            modified: this.news.modified
        })
    }
}

const printer = new NewsPrinter(
    new News('News title', 'News text')
)

console.log(printer.html())
/*
    <div class="news">
        <h1>News title</h1>
        <p>News text</p>
    </div>
*/

console.log(printer.json())
/*
    {"title":"News title","text":"News text","modified":false}
*/

Принцип открытости/закрытости

Программные сущности (классы, модули, функции) должны быть открыты для расширения, но не для модификации.

class Shape {
    area() {
        throw new Error('Area method should be implemented!')
    }
}

class Square extends Shape {
    constructor(size) {
        super()
        this.size = size
    }

    area() {
        return this.size ** 2
    }
}

class Circle extends Shape {
    constructor(radius) {
        super()
        this.radius = radius
    }

    area() {
        return (this.radius ** 2) * Math.PI
    }
}

class AreaCalculator {
    constructor(shapes = []) {
        this.shapes = shapes
    }

    sum() {
        return this.shapes.reduce((acc, shape) => {
            acc += shape.area()

            return acc
        }, 0)
    }
}

const calc = new AreaCalculator([
    new Square(2),
    new Circle(5)
])

console.log(calc.sum())  // 82.53981633974483

Принцип подстановки Барбары Лисков

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

class Person {}

class Member extends Person {
    access() {
        console.log('You have an access.')
    }
}

class Guest extends Person {
    isGuest = true
}

class Frontend extends Member {
    canCreateFrontend() {}
}

class Backend extends Member {
    canCreateBackend() {}
}

class PersonFromDifferentCompany extends Guest {
    access() {
        throw new Error('You haven\'t access!')
    }
}

function openSecretDoor(member) {
    member.access()
}

openSecretDoor(new Frontend())  // You have an access.
openSecretDoor(new Backend())  // You have an access.
openSecretDoor(new PersonFromDifferentCompany())  // Uncaught Error: You haven't access!

Принцип разделения интерфейсов

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

class Animal {
    constructor(name) {
        this.name = name
    }
}

const walker = {
    walk() {
        console.log(`${this.name} can walk.`)
    }
}

const swimmer = {
    swim() {
        console.log(`${this.name} can swim.`)
    }
}

const flier = {
    fly() {
        console.log(`${this.name} can fly.`)
    }
}

class Dog extends Animal {}
class Eagle extends Animal {}
class Whale extends Animal {}

Object.assign(Dog.prototype, walker, swimmer)
Object.assign(Eagle.prototype, walker, flier)
Object.assign(Whale.prototype, swimmer)

const dog = new Dog('Leo')

dog.walk()  // Leo can walk.
dog.swim()  // Leo can swim.

const eagle = new Eagle('Vu')

eagle.walk()  // Vu can walk.
eagle.fly()  // Vu can fly.

const whale = new Whale('Bubba')

whale.swim()  // Bubba can swim.

Принцип инверсии зависимостей

Система должна конструироваться на основе абстракций “сверху вниз”: не абстракции должны формироваться на основе деталей, а детали должны формироваться на основе абстракций

class Fetch {
    request(url) {
        // return fetch(url).then(response => response.json())
        return Promise.resolve('Data from fetch.')
    }
}

class LocalStorage {
    get() {
        const dataFromLocalStorage = 'Data from LocalStorage.'
        return dataFromLocalStorage
    }
}

class FetchClient {
    constructor() {
        this.fetch = new Fetch()
    }

    clientGet() {
        return this.fetch.request()
    }
}

class LocalStorageClient {
    constructor() {
        this.localStorage = new LocalStorage()
    }

    clientGet() {
        return this.localStorage.get()
    }
}

class Database {
    constructor(client) {
        this.client = client
    }

    getData() {
        return this.client.clientGet()
    }
}

const db = new Database(new FetchClient())

console.log(db.getData())  // Promise {<fulfilled>: "Data from fetch."}

const localStorage = new Database(new LocalStorageClient())

console.log(localStorage.getData())  // Data from LocalStorage.