Skip to content
Nasimi Mamedov edited this page Aug 7, 2023 · 1 revision

🎓 SOLID

Принципы про­ек­ти­ро­ва­ния в ООП

Далее речь будет идти о таких сущностях в ООП, как классы, модули или объекты

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

Мы не можем написать такое приложение одним огромным и запутанным файлом с кодом

Мы выполняем декомпозицию - разделяем приложение на более простые части

Давайте вместо слова "части" использовать более подходящее для программирования слово модули

Модули должны взаимодействовать друг с другом

При этом они не должны ничего знать о внутреннем содержании друг друга

Код модуля, его внутренние переменные и функции должны быть недоступны извне

Значит, каждому модулю нужен интерфейс

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

Мы должны обеспечить закрытость модуля для внешнего вмешательства в его "внутренний мир", при этом обеспечить его открытость для гибкого взаимодействия с другими модулями

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

И так далее...

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

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

Иначе пришлось бы обращаться из одного модуля к внутреннему содержимому другого, что недопустимо

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

Теперь встает вопрос о функциональности модулей

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

Например, что было бы, если бы вместо вызова статического метода Object.assign нам пришлось бы тащить всю кучу статических методов Object, а потом использовать только один из них?

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

Итак, проектирование архитектуры приложения - важнейший этам разработки


🎓 Single responsibility

Этот принцип обеспечивает четкое разграничение функций ( обязанностей ) модулей

Один модуль отвечает только за что-то одно

Что нам дает соблюдение этого принципа?

Первое - код каждого модуля достаточно прост

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

Этот принцип иллюстрируют те же статические методы Object, которые добавляются в каждой новой спецификации языка

Рассмотрим пример объекта user, который изначально имел два метода - read и write

Затем мы решаем добавить объекту user еще один метод - изменить свою аватарку

Нам не приходится изменять код методов read и write

Мы просто добавляем новый метод с конкретной функциональностью

Позже мы сможем еще больше расширить возможности объекта user, добавляя новые и новые методы


🎓 Open-closed

Пример выше отлично иллюстрирует также и этот приницип

Объект user открыт для расширения, но при этом исходный код его модулей закрыт для изменения

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

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

User.updateMethods = function ( methodName, func ) {
    this.prototype [ methodName ] = func
}

User.updateMethods ( "write", function ( message ) {
    console.log ( `${this.name}: ${message}` )
})

let user = new User ( "Иван" )
user.write ( "Hello!" )

В консоли мы увидим

Иван: Hello!

В этом примере у класса User предусмотрен интерфейс updateMethods, расширяющий функциональность экземпляров класса

Давайте используем этот интерфейс для создания нового метода:

User.updateMethods ( "voyage", function ( city ) {
    console.log ( `${this.name}: I visit ${city}` )
})

Теперь вызовем новый метод

user.voyage ( "London" )

В консоли мы увидим

Иван: I visit London

⚠️ Обратите внимание, что для расширения функционала класса нам не потребовалось изменять код существующих модулей


🎓 Liskov substitution

Прин­цип под­ста­новки Бар­бары Лис­ков заключается в следующем:

работоспособность кода не должна быть нарушена в результате замены экземпляра класса на экземпляр наследующего класса

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

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

В общем-то, это всего лишь соблюдение принципов наследования...

Если мы расширим ранее объявленный класс User следующим образом:

class RegisteredUser extends User {
    constructor ( name, token ) {
        super ( name )
        let auth = token
        this.setIdentity ( token )
        this.testIdentity = () => this.getIdentity () === auth
    }
    getIdentity () {
        let token = document.cookie.split("; ")
            .filter ( item => item.indexOf ( "auth" ) === 0 )[0]
        return token ? token.split( "=" )[1] : null
    }
    setIdentity ( auth ) {
        document.cookie = `auth=${auth}`
    }
}

и заменим ранее созданный экземпляр user на экземпляр нового класса:

user = new RegisteredUser ( "Иван", "xJgb-809/**1Bh" )

то это не приведет к нарушению нормальной работы модулей, оперировавших старым экземпляром user


🎓 Interface segregation

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

Под интерфейсом мы понимаем публичные методы экземпляра

Дробление интефесов - нормальная практика: если вы написали метод, который делает слишком много разного, вы уже нарушили первый принцип SOLID

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

Достаточно добавить еще один узкоспециализированный интерфейсный метод

Например, вернемся к нашему классу User с его методом write

Предположим, что этот метод может:

  • выводить сообщение в консоль
  • выводить сообщение на страницу
  • выводить сообщение во всплывающем окне

это плохой интерфейс

Нужно три отдельных метода, каждый из которых реализует одну из вышеперечисленных функций

Очевидно, что принцип специализации интерфейсов непосредственно вытекает из первого принципа SOLID

const card = (
    function ( pin ) {
        let cash = 0
        const addCash = sum => cash += sum
        const changePin = newPin => pin = newPin
        const testPin = pincode => pin === pincode
        const showCash = () => console.log ( `Cash: ${cash}` )
        const getMoney = sum => {
            cash -= sum
            console.log ( `Get money: ${sum}` )
        }
        
        return function ( operation, sum ) {
            operation === 0 ? addCash ( sum ) : 
                testPin ( prompt ( "Enter pincode" ) ) ?
                    operation === 1 ? showCash() : 
                        operation === 2 ? 
                            sum <= cash ? getMoney ( sum ) : console.warn ( "Insufficient cash" )
                        : changePin ( prompt ( "Set your pincode" ) )
                : console.error ( "Invalide pincode" )
        }
    }
)( prompt ( "Set your pincode" ) )

Мы получили модуль с навороченным интерфейсом

Раздробим интерфейс модуля:

const card = (
    function ( pin ) {
        let cash = 0
        const changePin = () => pin = prompt ( "Set your pincode" )
        const testPin = pincode => pin === prompt ( "Enter pincode" )
        const pinError = () => console.error ( "Invalide pincode" )
        const showMoney = () => console.log ( `Cash: ${cash}` )
        const getMoney = sum => {
            cash -= sum
            console.log ( `Get your money: ${sum}` )
        }
        
        return {
            uppendCash: sum => cash += sum,
            showCash: () => testPin() ? showMoney() : pinError (),
            getCash: sum => testPin() ? sum <= cash ? getMoney ( sum ) 
                                : console.warn ( "Insufficient cash" ) 
                            : pinError (),
            changePincode: () => testPin() ? changePin () : pinError ()
        }
    }
)( prompt ( "Set your pincode" ) )

🎓 Dependency Invertion

Зависимости...

Что от чего зависит в вашем приложении?

С самого начала вы должны это хорошо продумать

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

Пример грубого нарушения этого принципа:

User.updateMethods ( "sedition", function ( prop, val ) {
    this.__proto__[prop] = val
})

user.sedition ( "badExample", "You should not do this" )

В этом примере мы предоставили доступ экземплярам к прототипу

⚠️ Но прототип не должен зависеть от экземпляров

Каждый класс является абстрацией

Каждый производный класс является детализацией абстрации верхнего уровня

В примере, приведенном ранее, класс RegisteredUser является детализией класса User

Другими словами, класс User является абстрацией более высокого уровня, чем класс RegisteredUser

Поэтому нормально, что класс RegisteredUser зависит от класса User, но никак не наоборот

⚠️ Обратная зависимость недопустима

© Nasimi Mamedov 2018

Курсы были созданы для студентов A-Level Ukraine.

Использование данных материалов или любой их части коммерческими школами ( курсами ) является нарушением авторских прав.

A-Level Ukraine


1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19

Занятие 1

⤵️

Занятие 2

⤴️ ⤵️

Занятие 3

⤴️ ⤵️

Занятие 4

⤴️ ⤵️

Занятие 5

⤴️ ⤵️

Занятие 6

⤴️ ⤵️

Занятие 7

⤴️ ⤵️

Занятие 8

⤴️ ⤵️

Занятие 9

⤴️ ⤵️

Занятие 10

⤴️ ⤵️

Занятие 11

⤴️ ⤵️

Занятие 12

⤴️ ⤵️

Занятие 13

⤴️ ⤵️

Занятие 14

⤴️ ⤵️

Занятие 15

⤴️ ⤵️

Занятие 16

⤴️ ⤵️

Занятие 17

⤴️ ⤵️

Занятие 18

⤴️ ⤵️

Занятие 19

⤴️ ⤵️

⤴️

ico20 Дополнительно
dir-20 Справочная инфо

Clone this wiki locally