Useful packages, links and materials
- PHP
- Common useful links
- Полезные инструменты
- Symfony
- Архитектуры (DDD, Clean Architecture, Hexagonal)
- OOP
- СУБД
- REST, SOAP ...
- EventStorming
- Bash
- https://phptherightway.com/
- https://riptutorial.com/php/example/5441/reading-a-large-file-with-a-generator
- https://habr.com/ru/post/437972/ - базовое (про сессии и куки)
Пакеты, с которыми я столкнулся в ходе разработки
- markrogoyski/math-php (Powerful Modern Math Library for PHP)
- zircote/swagger-php
- php-di/php-di
- guzzlehttp/guzzle
- monolog/monolog
- twig/twig
- webmozart/assert
- swiftmailer/swiftmailer
- ramsey/uuid (uuid generator)
- https://github.com/php-pm/php-pm
queryBuilder:
security
filesystems
- https://github.com/thephpleague/flysystem a filesystem abstraction which allows you to easily swap out a local filesystem for a remote one.
For math (обертки над gmp и BcMath)
- brick/math A PHP library to work with arbitrary precision numbers.
- https://github.com/moneyphp/money
- phpunit/phpunit
- fzaninotto/faker
-
https://habr.com/ru/company/badoo/blog/426605/ как баду юзать сразу phpstan, phan и psalm
-
https://github.com/squizlabs/PHP_CodeSniffer detects violations of a defined set of coding standards.
- https://github.com/slevomat/coding-standard Slevomat Coding Standard for PHP_CodeSniffer provides many useful sniffs
- https://github.com/PHPCompatibility/PHPCompatibility PHP Compatibility check for PHP_CodeSniffer
-
sensiolabs-de/deptrac ( static code analysis tool that helps to enforce rules for dependencies between software layers)
-
psalm - static analysis tool for PHP that helps you identify both obvious and hard-to-spot bugs in your code.
-
phpstan - PHPStan finds bugs in your code without writing tests
-
first, collect app metrics for monitoring important buisness hotspots
-
pinba - (for all prod req) сервис для получения realtime -статистики от работающих приложений без накладных расходов на её сбор
-
xhprof (for small 0.1 % of requests) (, xshprof, lifeprof)
- https://github.com/badoo/liveprof-ui - профилирования всех запросов с интерфейсом для анализа изменения производительности приложения.
https://zinvapel.github.io/it/prog/lang/2019/01/10/xhprof/ https://habr.com/ru/post/145895/
- https://github.com/tideways/php-xhprof-extension - xhprof optimized for PHP7
- https://github.com/phacility/xhprof for ui
-
https://github.com/NoiseByNorthwest/php-spx (require centos 7+)
- php-fig/fig-standards
- psr/log
Статьи, зачем вообще этот psr:
- https://habr.com/ru/post/458484/ PSR Стандарты - краткое описание
- https://elisdn.ru/blog/80/some-reasons-to-learn-phpdoc
- https://mwop.net/blog/2015-01-26-psr-7-by-example.html psr7 на примерах
- cebe/markdown
- twig
- ezyang/htmlpurifier
- bandwidth-throttle/token-bucket - Implementation of the Token Bucket algorithm in PHP.
- brendt/phpstorm-photon-theme
- zualex/devmap - Карта развития веб-разработчика
- tlbootcamp/tlroadmap - Карта навыков и модель развития тимлидов
- https://habr.com/ru/search/?target_type=posts&q=тимлид&order_by=relevance - статьи на хабре по тимлидству
тесты по php:
- https://www.w3schools.com/quiztest/quiztest.asp?qtest=PHP (для джуна)
- https://corp.mamba.ru/ru/test/ (последний: 232)
- https://github.com/centrifugal/centrifugo - Scalable real-time messaging server in language-agnostic way.
Можно юзать для чатов, как микросервис доставки сообщений на клиент.
- https://github.com/walkor/Workerman - An asynchronous event driven PHP socket framework. Supports HTTP, Websocket, SSL and other custom protocols
- https://github.com/php-enqueue/enqueue-dev (если не юзать messanger symfony) - Предоставляет общий способ для программ создавать, отправлять, читать сообщения.
- https://habr.com/ru/company/oleg-bunin/blog/481092/ хорошее описание
- https://habr.com/ru/company/mailru/blog/341912/
- https://habr.com/ru/company/otus/blog/509598/
- https://thephp.website/en/issue/how-does-php-engine-actually-work/ (пару слов о том, как php работает)
исходный код -> (компилируется в) байт код (opcodes) > интерпретатор -> машинный (native)
Упрощенная схема
- сервер принмает запрос
- компилирует его в байт код (ru.wikipedia.org/wiki/Байт-код: исходный код -> байт код > интерпретатор -> машинный (native) код)
- тот поступает на исполнение VM (виртуальной машине)
Виртуальная машина, исполняя байт-код, может вызывать и другие PHP-файлы, которые опять перекомпилируются в байт-код и опять исполняются.
По завершению выполнения запроса вся информация, которая к нему относится, включая байт-код, удаляется из памяти. То есть каждый PHP-скрипт должен быть скомпилирован на каждом запросе заново.
Подобнее
-
Первая фаза управляется лексическим анализатором PHP . Он отвечает за сопоставление ключевых слов языка вроде function, return и static с отдельными частями, которые обычно называются токенами. Каждый токен зачастую дополняется метаданными, необходимыми для следующей фазы.
-
Вторая фаза управляется парсером PHP. Он отвечает за анализ одного или нескольких токенов, а также за сопоставление их с шаблонами языковых структур. Например, $foo + 5 распознаётся как двоичная операция «сложения», а переменная $foo и число 5 распознаются как операнды. Парсер рекурсивно строит абстрактное дерево синтаксиса (AST). Обычно работа лексического анализатора и парсера считается одной задачей.
-
Третья фаза — компилирование. AST преобразуется в упорядоченную последовательность инструкций-опкодов (байт код). Каждый опкод можно считать низкоуровневой операцией виртуальной машины Zend. Полный список поддерживаемых опкодов можно посмотреть здесь.
-
последняя фаза — исполнение. ВМ Zend выполняет каждую задачу, описанную в опкодах, и генерирует результат.
Первые три фазы (лексический анализатор, парсер и компилятор) объединены в «конвейер» (pipeline). Причём третья фаза занимает гораздо больше времени и потребляет больше ресурсов (памяти и процессора). Чтобы снизить вес фазы компилирования, в PHP 5.5 ввели расширение Zend OPCache.
Главная задача OPcache — избавиться от перекомпиляции скриптов на каждом запросе. Он встраивается в специально предназначенную для него точку, перехватывает все запросы на компиляцию и кэширует скомпилированный байт-код в shared memory (кеширует выходные данные фазы компилирования (байт код) в общую память (shm).
При этом экономится не только время компиляции, но и память, потому что раньше память под байт-код выделялася в адресном пространстве каждого процесса, а теперь он существует в единственном экземпляре.
opcache.preload = "сцценарий загрузки php файлов". Выполняется единожды при запуске fpm, apach...
технология уже включена в PHP 7.4 и дает возможность задать набор файлов, загрузить их при старте сервера и сделать все функции из этих файлов постоянными.
Одна из проблем, которую решает preloading-технология — это проблема связывания классов. Дело в том, что когда мы просто компилируем файлы в PHP, каждый файл компилируется отдельно от других. Делается это потому, что каждый из них может изменяться отдельно.
Соответствено, при каждом вызове include/require копируются сигнатуры классов и функций из общей памяти в память процесса-воркера, а также делается различная вспомогательная работа. И эта работа должна быть сделана для каждого запроса, так как по его окончании память процесса-воркера очищается.
Preloading позволяет связать классы изначально (не в runtime), а сделать это единожды.И, соответственно, генерировать код более оптимально. Как минимум, для фреймворков, которые будут загружаться с помощью preloading’а. Кроме того связанные классы теперь хранятся не в адресном пространстве каждого процесса, а в shared memory , и следовательно общее потребление памяти падает.
Нюансы:
-
Так как все preload-файлы компилируются только при запуске, помечаются как immutable и не перекомпилируются в дальнейшем, единственный способ применить изменения в этих файлах — перезапустить (reload или restart) PHP-FPM/Apache/и т. п.
-
глобальные константы, в отличие от констант/полей класса, принудительно очищаются после окончания фазы preload, в то время как константы/поля класса — резолвятся и сохраняются. Это приводит к тому, что во время выполнения запроса нам приходится определять глобальную константу заново, в результате чего она может получить другое значение.
В целом, opcach (и jit далее), имеет наилучший эффект для CPU-bound приложений. Существует два типа возможности занять поток (процесс): https://stackoverflow.com/questions/868568/what-do-the-terms-cpu-bound-and-i-o-bound-mean
- CPU Bound— блокировка, когда поток занят непосредственно вычислениями. Здесь необходимо позаботиться о том, чтобы длинная операция не блокировала потоки пула потоков .NET (ThreadPool), а работала отдельно и синхронизировала возврат результата.
- IO Bound— блокировка, ожидание результата от устройств ввода-вывода — тут асинхронный подход имеет максимальный эффект, так как, по сути, мы занимаемся ожиданием, и наши потоки могут выполнять пустую работу.
IO Bound хорошо решается асинронным подходом работы потоков (ReactPHP и тд). CPU Bound - наращиванием мощностей, использованием RoadRannera либо демонов на Go
JIT реализуется как часть OPcache.
После того, как байт-код скомпилирован и оптимизирован, для него запускается JIT-компилятор, который уже не работает с исходными текстами. Из PHP байт-кода JIT-компилятор генерирует нативный код, после чего в байт-коде изменяется адрес первой инструкции (по сути дела функции).
После этого нативный, уже сгенерированный код начинает вызываться из существующего интерпретатора без каких-либо изменений.
Упрощая вещи: когда JIT работает должным образом, ваш код не будет выполняться через Zend VM, вместо этого он будет выполняться непосредственно как набор инструкций уровня процессора.
- https://thephp.website/en/issue/how-does-php-engine-actually-work/ (пару слов о том, как php работает)
- http://xandeadx.ru/blog/php/866
- https://lectureswww.readthedocs.io/5.web.server/cgi.html (общее про cgi и его недостатки)
- https://hostiq.ua/wiki/php-modes/
PHP в режиме FastCGI в памяти висит сам php интерпретатор, а не какой-то конкретный php-скрипт.
- http://ocramius.github.io/doctrine-best-practices/#/10
- https://medium.com/phpyh/правильная-регистрация-консольных-команд-в-symfony-di-f7536c254926 правильная регистрация консольных команд (lazy)
- https://github.com/andreia/symfony-cheat-sheets (полезная шпаргалка по основным компонентам)
-
рейтинг в гитхабе по языку php (можно много интересного найти) https://github.com/search?q=stars%3A%3E1000+language%3APHP+state%3Aopen+language%3APHP&type=Repositories
-
Шаблон апи на симфони 4 (регистрация/авторизация/профиль юзера): https://github.com/tarlepp/symfony-flex-backend
-
Простенький сайтик для шаринга фото на симфони 5 пхп 7.4 с попыткой отказаться от геттеров и сеттеров: https://github.com/svbackend/cropofil
-
Апи на симфе 4, позволяет получить список фиььмов и рекомендации к понравившимся: https://github.com/svbackend/my-art-lib
Полезные курсы:
- от елисеева (менеджер проектов) https://rutracker.org/forum/viewtopic.php?t=5775309
- https://symfonycasts.com/ (или Knpuniversity) (https://coursehunter.net/course/symfony-5-glubokoe-pogruzhenie -httpkernel-request-response-flow)
Топ компоненты, бандлы и пакеты:
- symfony/notifier
- symfony/messenger
- Symfony/mailer
- https://github.com/lexik/LexikJWTAuthenticationBundle
- https://gist.github.com/fesor/fb1d53e8e4e427c59b930559da83d9a3 - RequestObjectResolver, для валидации DTO из реквеста (до попадания в контроллер)
- https://github.com/nelmio/NelmioCorsBundle - Adds CORS (Cross-Origin Resource Sharing) headers support (статья про CORS в symfony https://blog.liplex.de/symfony-cors-listener/)
Админки:
- EasyAdminBundle
Bundles for dev:
- dmaicher/doctrine-test-bundle - bundle to isolate your app's doctrine database tests and improve the test performance
- https://github.com/paratestphp/paratest - Parallel testing for PHPUnit
- https://github.com/lchrusciel/ApiTestCase - решение по интеграционному тестированию внешних апи
Предметно-ориентированное проектирование (Domain-driven design, DDD ) — это набор принципов и схем, направленных на создание оптимальных систем объектов. Сводится к созданию программных абстракций, которые называются моделями предметных областей. В эти модели входит бизнес-логика, устанавливающая связь между реальными условиями области применения продукта и кодом.
Статьи по Hexagonal
Статьи по DDD
- https://matthiasnoback.nl/2018/06/doctrine-orm-and-ddd-aggregates/
- https://matthiasnoback.nl/2018/03/ormless-a-memento-like-pattern-for-object-persistence / интересная статья о сохранении моделей без орм (через снепшоты внутреннего состояния)
- https://habr.com/ru/post/427739/ (todo почитать)
Выжимки из синей и красной книги
Видео:
- https://www.youtube.com/watch?v=fWU8ZK0Dmxs - Уди рассказывает про race condition довольно часто важные бизнес правила будут в перемешку с правилами, нарушения которых не приводят к проблемам (типа лайкнуть архивные баннер). Нужно быть аккуратным с защитными условиями бизнес правил в сущностях, для которой может быть гонка. Есть важные правила которые нельзя нарушать. Которые например на тебя налагает не менеджер или там продукт оунер а законодательство страны в которой твой продукт работает Если "а что будет, если я выключил промокод, а в это время его применили" - это не критично.
Проектирование без учета ORM Когда вы узнаете, как проектировать доменные объекты с использованием шаблонов, управляемых доменом, вам сначала нужно избавиться от идеи, что проектируемые объекты когда-либо будут сохраняться
When to use - if you have complicated and everchanging business rules
BUT - if you have simple not-null checks and a couple of sums to calculate, a Transaction script is better bet
Модель – это всеохватывающее определение для всего доменного кода. Как "модель солнечной системы" и "модель киоска с шаурмой". С кучей сущностей, расчётов и правил внутри. А не какой-то один класс Model. "Доменная модель" подразумевает, что ты разбираешься с доменом, а не просто объектики лепишь. Основная суть в том что бы понять какие у тебя есть важные правила и как реагировать на них.
Во многих фреймворках в отличие от этого класс Model определяет "модель данных БД", "модель ввода" или что-то ещё мелкое. В Eloquent это "модель данных БД". В Yii это "модель ввода с валидацией".
Cодержат бизнес-правила, независимые от приложения. И они не просто объекты с данными. Entities могут содержать ссылки на объекты с данными, но основное их назначение в том, чтобы реализовать методы бизнес-логики, которые могут использоваться в различных приложениях.
An object model of the domain that incorporates both behavior and data. You’ll find objects that mimic the data in the business and objects that capture the rules the businessuses.
Анемик модели это хорошо и норм, просто это не модели а структуры если вам надо сохранить и считать данные (CRUD) а логики не надо то структура самое то
О анемик модель vs reach models
- https://thevaluable.dev/anemic-domain-model/ Anemic Domain Model vs Rich Domain Model with Examples
- http://ocramius.github.io/doctrine-best-practices/#/26
- https://habr.com/ru/post/470021/ Анемичная и «Богатая» модель в контексте GRASP шаблонов
- https://habr.com/ru/company/mobileup/blog/335382/ - статья по clean architecture
Конструктор можно делать приватным + именнованые для разных кейсов создания сущности
Вопрос по VO
- почему на слайде свойство "иммутабельность" перечеркнуто?
- почему репозиторий выделен в отдельный слой на ровне с остальными?
- почему в моделе у тебя свойства публичные?
- что по поводу айди до создания сущности uuid?
- сущности не должны знать, как они используются
- по поводу зависимости от внешних клиентов
- не имеет идентификатора (в отличии от агрегата)
- иммутабельный
- валидирует состсяние при создании
- все состояние VO можно легко "серриализовать" и передавать между процессами, т.к. не имеет идентификатора
Например, есть class UrlValueObject = при своении в конструкторе можем чекать все что надо, дальше мы уже можем преедавать урл не как строку, а как VO. И он immutable. Для изменения нужно создать новый инстанс объекта с нужным значением
Тесирование В сущности не нужны добавлять getter-ы только ради тестов (чтобы проверить "правильность" созданной сущности).
Конструктор не тестят в принципе. Сущность либо создалась с корректным состояние, либо упала с DomainException.
Методы смены состояний можно проверить через Domain Events, которые хранит в себе агрегат.
Агрегаты строишь вокруг инвариантов, а не вокруг "групп" данных. На агрегатах у тебя только "командные" методы с :void В самом простом случае на чтение ты данные забираешь при помощи DBAL (ReadModel) из таблицы напрямую, минуя сам агрегат. В сложном — EventSourcing и проекции, тогда можно +- любые данные на чтение собирать на базе событий. Но EventSourcing лучше пробовать не в перую очередь и только когда поймёшь, что он реально нужен проекту и что ты понимаешь, что ты делаешь.
в dto вместо геттеров и сеттеров публичные свойства с @psalm-immutable.
в модель тоже геттеры/сеттеры не нужны никогда.
в value object-ах лучше делать toString/to*. ну и всё)
доменные события — это внешний контракт агрегата. его API. вместо геттеров ты получаегшь куда более мощную информацию об изменениях
http://gorodinski.com/blog/2012/05/19/validation-in-domain-driven-design-ddd/
Dto нужны только для того, чтобы передавать данные между уровнями. Например:
- передача данных с уровня инфраструктуры в уровень сервиса (или домена).
- возврат данных с read model. Для того, чтобы отобразить данные, не нужно подымать сложные domain модели с поведением.
Чтобы данные не передавать массивом, создается объект с public полями (ide подскажет поля объекта при использовании и тип, при наличии phpdoc).
-
это грубо говоря массив, который мы перекидываем из внешнего слоя инфраструктуры в слой приложения (или домена).
-
объект с заранее известными полями без какого-либо поведения
-
в dto вместо геттеров и сеттеров публичные свойства с @psalm-immutable.
-
без валидации и контроля типов (кроме psalm). Необходимые проверки на типы и корректность данных должна выполнять использующая сторона - сервисы, репозитории, ентити.
-
как правило, для dto справедливо, что он должен инстанцироваться с заполнением определенных полей. Нужно добавить конструктор и принимать эти обязательные поля, а остальные поля заполнять из реквеста (формы в контроллере) путем присваивания публичных полей (или автоматически из валидатора форм). Но в общем, dto после создания не надо модифицировать, это что-то вроде сообщения
Best way to write DTO - constructor + public properties + Psalm @immutable to control for access. then you do an optimal class and check immutability in ci instead of runtime just write once from constructor, that's it all modifications with "with" methods + cloned return to keep immutability but DTO generally should not be modified, it's like a message
Иммутабельность Это св-во VO. Но т.к. DTO-ки как правило не модефицируют, это и к нему тоже косвенно можно отнести
Материал:
- https://martinfowler.com/bliki/CQRS.html
- https://www.youtube.com/watch?v=RfnySciLUhc&feature=youtu.be
Read and write models
-
Write models - Domain models (ak DDD) domain models (агрегаты, сущности, vo) держим логику. Данные группируем по сущностям так, как удобно для логики
-
Read models. Используются для фронта например (списков, форм и тп) Для чтения юзаем отдельные простые структуры данных. Заполняем их напрямую из результатов запросов (без использования write models)
-
https://github.com/prooph/event-sourcing - Provides basic functionality for event sourced aggregates (чтобы самому не реализовывать. Однако, добавляет зависимость в доменный слой)
Пример
- https://github.com/dmitrymendelson/cqrs_es_example (dirty example of cqrs)
- https://github.com/ShittySoft/lyska-2020-cqrs-es-works
About communication Use case adn presenter?
-
An entity/model class for each layer https://habr.com/ru/company/mobileup/blog/335382 (пункт Заблуждение: Обязательность маппинга между слоями). Если у вас сложное приложение с логикой бизнеса и логикой приложения, и/или разные люди работают над разными слоями, то лучше разделять данные между слоями (и маппить их). Также это стоит делать, если серверное API корявое. Но если вы работаете над проектом один, и это простое приложение, то не усложняйте лишним маппингом.
Статьи:
- https://martinfowler.com/bliki/TellDontAsk.html
- https://elisdn.ru/blog/142/structs-or-objects?utm_source=subscribe&utm_medium=email&utm_campaign=blog
облако/PHP/книги/прочел/Патерны%20проектирования%20refactoringguru.pdf ст 14 -18
-
ООП — парадигма разработки, в которой приложения описываются взаимодействием различных объектов.
-
Объекты являются сущностями, у которых есть состояние и поведение. Объект взаимодействую между собой путем обмена сообщений (вызовов методов других объектов).
-
Обычно объекты являются экземплярами какого-нибудь класса. Класс определяет, что объект этого класса умеет делать (каким поведением обладает объект). Классы могут образовывать иерархию наследования.
ООП парадигма отличается от процедурной и функциональной тем, что подразумевает создание объектов, хранящих свои данные и код для операций по работе с этими данными внутри себя, а не раздельно. ООП для меня это сообщения, локальное удержание и защита, скрытие состояния и позднее связывание всего.
Инвариант - выражение, которое определяет непротиворечивое (консистентное) внутренее состояние объекта Это также означает, что при создании и последующиех вызовах любых методов объекта в любой последовательности, его инвариант неизменяется (внутренее состояние остается непротиворичивым).
Инкапсуляция позволяет сохранить инвариант. Для этого, следует ограничить использование геттеорв, и тем более сеттеров, которые нарушают инкапсуляцию класса и позволяют изменять состояние объекта, не позволяя объекту обеспечивать собстевенный инвариант.
ООП определяется через 4 принципа: инкапсуляция, наследование, абстракция, полиморфизм
Инкапсуляция (способ логической группировки (упаковка) данных и функций в единый компонент (с) Википедия)
Инкапсуляция - главный механизм обеспечения инвариант класса
"Любая программная сущность, обладающая нетривиальным состоянием, должна быть превращена в замкнутую систему, которую можно только перевести из одного корректного состояния в другое". Чтобы этого добиться, необходимо выполнять:
- четкое разделение интерфейса и реализации класса, и строгое соблюдение этих границ
- для взаимодействия с объектом наружу выставляется только спецификация (интерфейс) объекта
- при этом детали реализации должны быть скрыты от клиентской стороны внутри самого объекта (
- для соблюдения границ используется принцип "сокрытие информации (состояния)". Он обеспечивает ограничение доступа одних компонентов к деталям реализаций других. Реализуется использованием различных модификаторов доступа. Яявляется самым надежным способом сохранения инкапсуляции
- для обеспечения полной защиты состояния объекта, в конструктор и его методы необходимо добавить различные проверки для сохранения инварианта класса.
Таким образом, для взаимодействия с объектом наружу выставляется только спецификация (интерфейс) объекта. При этом сам объект хранит своё внутреннее состояние и полностью его контролирует и защищает, скрывая детали реализации от клиентской стороны с помощью "сокрытия информации (состояния)" (используя модификаторы доступа). Объект не позволяет создать себя в некорректном состоянии и после создания не даёт себя сломать, т.е. сохраняет инфариант.
Сохранение инварианта как раз легко реализуется с помощью инкапсуляции через помещение кода рядом с состоянием в сам объект и с помощью сокрытия состояния от прямого внешнего доступа к полям.
наследование - позволяет описать новый класс на основе уже существующего и дополнить его, или частично изменить его поведение.
- позволяет объединить переиспользование кода и силу полиморфизма
- классы могут образовывать иерархию
- Класс, от которого производится наследование, называется базовым или родительским. Новый класс – потомком, наследником или производным классом.
- наследник полностью удовлетворяет спецификации родительского, однако может иметь дополнительную функциональность. С точки зрения интерфейсов, каждый производный класс полностью реализует интерфейс родительского класса. Обратное не верно.
Плюсы, минусы:
- (+) повторное использование существующего кода
- (+) полиморфность родителя и дочерних класов
Полиморфность наследования же в том, что мы можем там, где клиентский код ожидает от нас родительский класс, дать объект типа дочернего класса, и вызвать нужный метод базового класса. Компилятор поймёт, что нам подсунули наследника, и сначала начнёт искать реализацию этого метода у наследников.
- (-) подклассы всегда следуют интерфейсу родительского класса. Вы не можете исключить из подкласса метод, объявленный в его родителе
- (-) при возрастании иерархии, нужно помнить о проблеме хрупких базовых классов. Внесение изменений в базовый класс может сломать поведение дочерних.
- (-) Поскольку подклассу доступны детали реализации родительского класса, то часто говорят, что наследование нарушает принцип "сокрытие информации" (не инкапсуляции)
Предок должен быть закрыт от наследников настолько, насколько это возможно. Например, для реализации патерна "шаблонный метод)". Абстрактный класс описывает шаблон алгоритма и предоставляет возможность дочерним классам конкретезировать некоторые шаги. В соответствии с этим паттерном, поведение абстрактного родительского класса разделяют на две части:
- Поведение в неабстрактных методах. Это общий код, формирующий шаблон (скелет) алгоритма. Дочерний класс наследует реализацию этих методов. Неабстрактные методы рекомендуется объявлять с модификатором final. Это позволяет избавиться от одной из проблем с сокрытием – исключить возможность переопределения поведения в дочерних классах.
- Поведение в абстрактных методах. Конкретная реализация этого поведения выполняется дочерними классами. В теле метода размещается код, который описывают специфичную для дочернего класса реализацию некоторых шагов алгоритма. Дочерний класс наследует только интерфейс (сигнатуру) абстрактного метода.
Этот паттерн снижает силу зацепления по реализации в рамках иерархии, за счет разделения кода на abstract методы, реализованные в дочерних классах, и final методы, реализованные в абстрактном родительском классе. За счет ограничивающих ключевых слов вы, в принципе, запрещаете переопределять реализацию в процессе наследования. Пределы изменения реализации в дочерних классах четко ограничены абстрактными методами, т.к. остальные методы помечены ключевым словом final.
Сформированное эмпирически правило гласит, что если можно переиспользовать код без наследования, то лучше так и сделать (юзать композицию).
- Во-первых, мы полностью убрали зацепление классов по реализации. В этом случае классы разделяют только сигнатуры методов и полностью отсутствует какое-либо унаследованное поведение. Классы объявлены как final, а значит исключаются все связанные с наследованием реализации проблемы: хрупкость базового класса, запутанность потока выполнения при использовании открытой рекурсии и т.д.
- Детали реализации поведения надежно скрыты за интерфейсом и теперь не являются частью контракта, что существенно повышает качество архитектуры приложения.
Классический пример: квадрат является фигурой, как и круг. И их можно нарисовать. Но по разному.
- https://www.thoughtworks.com/insights/blog/composition-vs-inheritance-how-choose
- http://programming-lang.com/ru/comp_programming/satter/0/j84.html композиция против наследования
-Абстрагирование – отделение существенного от несущественного. (как данных, так и поведения)
- Абстракция — это модель объекта реального мира, в которой рассматриваются отдельные значимые характеристики (данные, поведение) для данного контекста, исключая незначемые.
- Абстракция - правильное разделение программы на объекты
Абстракция не подразумевает наличие наследования. Абстракция не может существовать без инкапсуляции!
В контексте ООП, абстракция проявляется в 2х видах :
- в абстрагировании от незначительных в контексте программы характеристик объекта. Когда мы моделируем некий объект, мы можем отказаться от его частей (характеристик), не важных в контексте программы. Представьте, что мы описываем яблоко. Оно имеет: цвет, вкус, калорийность, кол-во зерен и т.д. Исчерпывающее ли это описание? Нет. Об объекте можно думать по разному: и как о наборе клеток, и как о чем-то, что можно съесть и тп.
Как тогда описать яблоко, при написании проги для сортировки яблок? Так как нужно программе: если программа сортирует яблоки по весу и цвету, то ей нужна инфа только об этом и не больше. Необходимо использование только тех характеристик объекта, которые с достаточной точностью представляют его в данной системе.
- в абстрагировании от деталей реализации. когда мы имеем больше описания об объекте, чем нужно остальной части программы (клиенской стороне).
Пример: объект файл. Остальной программе нужны только методы записи и чтения (остольное поведение отбросили из абстракции, т.к. оно для нас не нужно). Хотя понятно, что объект файл будет еще содержать системный указать, информацию о текущей позиции в файле и прочие внутренние данные.
В таком случае мы тоже применяем абстракцию, чтобы скрыть ненужные детали реализации от остальной части кода (с помощью механизма инкапсуляции). А всю работу с объектом производить через известный интерфейс, при этом не вдаваясь в подробности внутренней реализации.
Выводы. Абстракция: основа ООП, позволяет работать с объектами, не вдаваясь в особенности их реализации. Проявляется в 2х видах:
- в абстрагировании от незначительных в контексте программы характеристик объекта
- в абстрагировании от деталей реализации, путем сокрытия ненужных деталей реализации от клиента, используя инкапсуляцию. Вся работа с объектом ведется только через интерфейс. Главное, чтобы инициализированный объект выполнял возложенную работу, а как он это делает внутри - не важно (реализация скрывается от внешнего мира с помощью инкапсуляции).
Про Уровни абстрагирования - приложения разделяется на уровни абстрагирования (от верхнего к нижним). Каждый уровень абстрагирует реализацию объектов нижних уровней. Ему не важно, как они устроены внутри, главное, чтобы они выполняли свою работу (объекты эти находятся на более низком уровне абстракций). Самые нижние уровни выполняют самую черную работу. Похожим образом работает ОС, различиные сетевые протоколы...
Про абстракции. Практически все компьютеры работают в двоичной системе 0 и 1 все что "выше" это уже абстракция. Все наши эти if, else, ArrayList все это абстракции по сути своей. И даже 0 и 1 это абстракция, а изначально это состояние транзистора - включен/выключен. И транзистор по сути абстракция, через которую получается управлять потоком электронов, то есть током. И понятие электрический ток - тоже абстракция. А дальше уже квантовая физика :-) Хотя там абстракций еще больше...
полиморфизм - один интерфейс - множество реализаций
- возможность объектов с одинаковой спецификацией (интерфейсом) иметь различную реализацию.
- использование объектов с одинаковым интерфейсом без информации о типе и внутренней структуре объекта.
- способность программы выбирать различные реализации при вызове операций с одним и тем же названием.
Отделение интерфейса от реализации позволяет коду, использующему интерфейс не зависить от деталей реализации. Или от того факта, что она поменялась целиком и полностью.
- статический полиморфизм (адресс метода определяется во время компиляции) - раннее (статическое) связывание / связывание во время компиляции / перегрузка метода(оператора) (В том же классе)
Механизм реализации: - перегрузка методов (несколько различных определений для одного имени метода в одной области видимости) - перегрузка операторов
class Calculation {
void sum(int a,int b){System.out.println(a+b);}
void sum(int a,int b,int c){System.out.println(a+b+c);}
public static void main(String args[]) {
Calculation obj=new Calculation();
obj.sum(10,10,10); // 30
obj.sum(20,20); //40
}
}
- динамический полиморфизм (адресс метода определяется во время выполнения) - позднее (динамическое) связывание / Связывание во время выполнения / Переопределение метода. (В разных классах)
Механизм реализации: - переопределение абстрактных методов в наследнике (родитель не имеет базовой реализации) - переопределение виртуальных методов в наследнике (переопределение базовой реализации родителя) - реализация методов интерфейса
class Animal {
public void move(){
System.out.println("Animals can move");
}
}
class Dog extends Animal {
public void move() {
System.out.println("Dogs can walk and run");
}
}
public class TestDog {
public static void main(String args[]) {
Animal a = new Animal(); // Animal reference and object
Animal b = new Dog(); // Animal reference but Dog object
a.move();//output: Animals can move
b.move();//output:Dogs can walk and run
}
}
- https://medium.com/swlh/final-classes-in-php-9174e3e2747e
- https://habr.com/ru/post/482154/ чуть более длинная статья с большими разъяснениями
- http://programming-lang.com/ru/comp_programming/satter/0/j84.html тут пару неплохих аргументов
- "является" - отноешение между базовым классом и его наследником
KISS -keep it simple stupod - большинство систем работают лучше всего, если они остаются простыми, а не усложняются
YAGNI (You aren't gonna need it)— игнорируй изменения, которых скорее всего не будет. Затык тут в «скорее всего», потому что будущее мы предсказывать не умеем.
TellDon'tAsc (похоже на Information Expert из GRASP) - https://martinfowler.com/bliki/TellDontAsk.html
DRY - don't repead yourself - Каждая часть знания должна иметь единственное, непротиворечивое и авторитетное представление в рамках системы
Single Choice Principle - всякий раз, когда система программного обеспечения должна поддерживать множество альтернатив, их полный список должен быть известен только одному модулю системы
SOlID
- https://habr.com/ru/post/446816/
- https://habr.com/ru/post/208442/
- http://sergeyteplyakov.blogspot.com/2014/10/about-design-principles.html
- http://sergeyteplyakov.blogspot.com/2014/10/solid.html
цели принципов:
- SRP предназначен для борьбы со сложностью; позволяет сделать класс высоко связанным внутри (highly cohesive), что позволит с меньшими усилиями его понимать и развивать
- OCP помогает в вопросах расширяемости и параллельной разработки;
- LSP указывает, как использовать наследование «правильно»;
- ISP выделяет разные аспекты класса; уменьшает связанность (low coupling) между классом и его клиентом, ведь теперь клиент завязан не на весь интерфейс класса, а лишь на его часть.
- DIP должен бороться с признаками плохого дизайна в архитектуре
- Модуль должен иметь только одну причину для изменения. (Чистая_архитектура. Роберт Мартин, с. 79)
- На каждый объект должна быть возложена одна единственная обязанность
В контексте принципа SRP мы будем называть обязанностью причину изменения. Если вы можете найти несколько причин для изменения класса, то у такого класса более одной обязанности
Нарушение. Нарушение этого принципа применяется к классам, содержащим в себе различную функциональность, которая может менятся в разное время по разным причинам. Например, классы бизнес-логики, которые знают о пользовательском интерфейсе или о базе данных; класс windows -сервиса c кучей бизнес-логики; статические утилитные классы, изменяющие глобальное состояние и т.п.
Цель SPR
- борьба со сложностью. При разростании класс затрудняется навигация, на глаза попадаются ненужные детали, связанные с другим аспектом (кол-во деталей превышает 7 +-2)
- Любой сложный класс/модуль должен быть разбит на несколько простых составляющих, отвечающих за определенный аспект поведения (единственную ответственность), что упрощает как понимание, так и будущее развитие.
Я хочу иметь возможность сосредоточиться на сложных аспектах системы по отдельности, поэтому когда мне становится сложно это делать, я начинаю разбивать классы и выделять новые.
SRP – это способ поиска скрытых абстракций, достаточно сложных, чтобы им отвели отдельную именованную сущность и спрятали в их недрах все детали. Разделение классов на составляющие диктуются не «осями изменений», а здравым смыслом и попыткой справиться с нарастающей сложностью системы.
Примеры:
- смешивание логики и инфраструктуры: бизнес-логика смешана с представлением, слоем персистентности, находится внутри WCF или windows-сервисов и т.п.
- класс/модуль решает задачи разных уровней абстракции: вычисляет CRC и отправляет уведомления по электронной почте; разбирает json-объект и анализирует его содержимое и т.п.
- god-объекты
- Класс, который запрашивает данные из БД и выводит их в определённом формате в .txt-файл. Содимся и думаем, что может измениться со временем:
- бд (или библиотека для работы с ней)
- формат данных (900 USD или 900.00$? 20190826T130000 или час дня двадцать шестого августа 2019 года)
- тип файла для вывода
Такой класс
- тяжело тестировать
- изменяя 1 требования мы трогаем класс, и мы можем сломать
Если в описании маленькой программной сущности (класс или метод, например) фигурирует союз «И», это такой большой красный флаг с надписью «у тебя будут проблемы, если ты не перепроверишь этот кусок код
Anti-SRP – Принцип размытой ответственности. Чрезмерная любовь к SRP ведет к обилию мелких классов/методов и размазыванию логики между ними.
Надо предугадать, где требования будут менятся, а где нет( чтобы не декомпозировать так жеско).
Если приложение не модифицируют таким образом, что эти обязанности изменяются порознь, то и разделять их нет необходимости. Более того, разделение в этом случае попахивало бы ненужной сложностью.
"Если несколько программных сущностей изменяются вместе по одним и тем же причинам, то на самом деле это одна программная сущность. Объедините их немедленно."
На практике я довольно часто говорю о том, что класс нарушает SRP, но при этом я никогда не апеллирую к «осям изменений». Я говорю:
-
о тяжеловесности интерфейса и реализации,
-
о решении классом не связанных друг с другом задач,
-
о том, что здесь много кода и мало классов,
-
о том, что в коде много лишнего шума, который нужно перенести в отдельные классы, чтобы было легче увидеть его основную суть,
-
о том что высокоуровневая логика перемешана с низкоуровневыми деталями и т.п. Иногда я готовлю классы к будущим изменениям, но в плане сопровождаемости, а не в плане нарушения SRP.
-
http://sergeyteplyakov.blogspot.com/2014/08/single-responsibility-principle.html
-
https://blog.byndyu.ru/2009/10/blog-post.html еще примеры //todo почитать
Программные сущности должны быть открыты для расширения и закрыты для модификации.
Смысл принципа OCP довольно прост: дизайн системы должен быть простым и устойчивым к изменениям. Это достигается путем фиксация интерфейса класса/модуля, и возможность изменения или подмены реализации/поведения.
- https://web.archive.org/web/20060822033314/
- http://www.objectmentor.com/resources/articles/ocp.pdf
- https://habr.com/ru/company/tinkoff/blog/472186/ (перевод)
- http://sergeyteplyakov.blogspot.com/2014/08/open-closed-principle.html
Если одно изменение в программе влечет за собой каскад изменений в зависимых модулях, то программа становится хрупкой, негибкой, непредсказуемой и непереиспользуемой. (пример: Ни один модуль, который зависит от публичной переменной класса (либо глобальной переменной), не может быть закрыт о модуля, который может писать в нее.) Принцип гласит, что надо проектировать модули, которые никогда не меняются. Когда требования меняются, нужно расширять поведение таких модулей путем добавления нового кода, а не изменением старого, уже работающего кода. Старый код изменяется только для исправления критических ошибок в нем.
Модули, отвечающие принципу открытости-закрытости, имеют два главных признака:
- Открыты для расширения. Это означает, что поведение модуля может быть расширено. То есть мы можем добавить модулю новое поведение в соответствии с изменившимися требованиями
- Закрыты для изменений (с помощью инкапсуляции). Исходный код такого модуля неприкасаем. В результате расширения поведения сущности не должны вноситься изменения в код, который эту сущность использует.
Стандартный способ расширить поведение модуля — внести в него изменения. Ключ к решения этих 2х условия - абстракция. ( Абстракция не подразумевает наличие наследования. Абстракция не может существовать без инкапсуляции!) Т.е. модулю добавляем зависимость от абстракции (как правило интерфейс, либо абстрактный класс), и таким образом, закрываем модуль от изменений. При этом детали реализации будет скрыта от клиентов за счет инкапсуляции, которая позволяет изменять реализацию без изменения интерфейса Для расширения поведения, добавляем необходимую реализацию абстракции (интерфейса) и передаем ее при вызове. (с помощью наследования, что позволяет заменить реализацию, которая не затронет существующих клиентов базового класса.)
- Расширять нужно не все. Нужно чётко определить точки, где вы дадите расширять вашу программную сущность (нельзя давать менять всё подряд). (точки изменения из Protected Variantions в GRASP)
- Определиться с механизмами, которыми будете расширять программную сущность (в любой непонятной ситуации делай выбор в пользу композиции, см Наследование vs композиция) Варианты расширения (используя полиморфизм, композицию)
-
наследование (если есть открытый для наследования базовый класс либо абстрактный класс. Клиентский код делаем зависимым от базового класса (либо абстрактного), и для изменения (расширения) поведения клиентского кода, добавляем нового наследника от базового класса и передаем его. При этом клиентский код будет закрыт от изменений за счет того, что зависит от абстрактного класса
- (-) все связанные минусы с наследованием (сильное сцепления и тп) Если выбрали наследование, помните о проблеме хрупких базовых классов. Предок должен быть закрыт от наследников настолько, насколько это возможно.
-
патерн "стратегия": целевой класс через параметр метода/конструктора принимает объект с логикой, расширяющей поведение этого класса. Далее, реализуем необходимое поведение (реализуя интерфейс) и передаем свою реализацию в модуль. Классы при этом закрываем от наследования с помощью final
-
следующий вариант: выделение интерфейса всего класса и реализация необходимого поведения (через реализацию интерфейса). Клиент зависит от интерфейса. Во время выполнения передаем конкретную реализацию
- (+) полностью убрали зацепление классов по реализации. В этом случае классы разделяют только сигнатуры методов и полностью отсутствует какое-либо унаследованное поведение. Классы объявлены как final, а значит исключаются все связанные с наследованием реализации проблемы: хрупкость базового класса, запутанность потока выполнения при использовании открытой рекурсии и т.д.
- (+) явно реализуя интерфейс через отношение implements, мы фиксируем спецификацию (контракт) класса и впоследствии может гибко управлять этим контрактом, например, в соответствии с принципом разделения интерфейсов (ISP). Детали реализации поведения надежно скрыты за интерфейсом и теперь не являются частью контракта, что существенно повышает качество архитектуры приложения.
- (+) После публикации интерфейса, контракт, который он описывает, закрывается от дальнейшей модификации. А значит и классы, которые реализуют интерфейс через implements, также закрыты от модификации контракта. При этом детали реализации этого контракта остаются открытыми для изменений.
- (+) Ключевое слово final запрещает наследование, а значит и создание дочерних классов с измененным поведением. Возможна только реализация опубликованного интерфейса. И это существенное преимущество – любое развитие архитектуры не влияет на существующий код, клиенты продолжают взаимодействовать с классом через открытый интерфейс. Классы в такой архитектуре можно сравнить со «строительными блоками», готовыми к конструированию приложения. С помощью implements мы указываем к какому типу принадлежит блок, а с помощью final делаем класс законченным и готовым к употреблению.
- (-) они ограничены в отношении наследования. Некоторые реализации могут иметь общий код, который нежелательно копипастить (нарушение DRY).
-
(решение минусы выше). (пример https://habr.com/ru/post/482154/) Предпочитаем агрегацию наследованию. Самым слабым типом отношений между классами является агрегация, и она может полноценно заменить наследование. Для этого агрегацию следует применить в форме паттерна декоратор (decorator pattern). И в этом случае мы сохраняем все преимущества классов, как законченных «строительных блоков», – запрет наследования через final и зацепление через интерфейс, реализованный с помощью implements.
- вводим интерфейс, явно определяющий контракт реализующих его классов
- реализуем класс, содержащий основную функциональность (что-то вроде базового класса, но в отличии от наследования, закрыт от модификации контракта через implements, и от создания дочерних классов через final)
- реализуем второй класс как декоратор: принимает в конструкторе декорируемый объект через базовый интерфейс и хранит его в приватном свойстве. Класс вызывает соответствующие методы базового класса и при необходимости дополняет их поведение. (Такие методы называют методами передачи (forwarding methods). Однако методы передачи могут и не включать никакой дополнительной функциональности, а просто возвращать результат «как есть». При использовании наследования, такие «однострочные» методы не требовалось бы включать в дочерний класс, а поведение было бы автоматически унаследовано из родительского класса, что в некоторой степени сократило бы объем кода. Некоторые разработчики именно по этой причине отдают предпочтение наследованию, которое сокращает объем кода. Особенно в случае, если при агрегации классы-декораторы большей частью состояли бы из таких «однострочных» методов передачи. Стоит сказать, что написание дополнительной строки кода – довольно невысокая цена за получаемые с агрегацией преимущества (слабое зацепление, следование SOLID) и избегаемые недостатки наследования (нарушение сокрытия, хрупкость архитектуры, зацепление на реализацию). Агрегация покрывает все возможности наследования. Классы SimpleCommentBlock и CountingCommentBlock реализуют общий интерфейс CommentBlock, а потому могут полиморфно замещаться в коде. Разместив основное поведение в базовом классе и дополнив его в классе-декораторе, мы можем избежать дублирования кода. Однако, если открытость класса к наследованию подталкивает нас к зацеплению на особенности реализации и повторному использованию кода, то использование модификатора final подталкивает разработчика к выбору механизма агрегации и зацеплению на поведение класса, контракт которого описан в виде интерфейса CommentBlock.
Теперь добавление нового метода viewRandomComment() в базовый класс SimpleCommentBlock никак не влияет на структуру и поведение изолированного класса-декоратора CountingCommentBlock. Если бы использовалось наследование, то метод был бы неявно включен в состав дочернего класса и нарушил логику его работы – в реализации viewRandomComment() не предусмотрен подсчет количества просмотров. Вызовы CountingCommentBlock::viewRandomComment() не учитывали бы просмотры в кеше. Кроме того, изменение деталей реализации viewComments() в базовом классе SimpleCommentBlock не повлияет на зависящие от него классы. CountingCommentBlock не опирается на реализацию поведения в базовом классе SimpleCommentBlock, он зависит только от контракта.
Пример
- про фигуры, круг и квадрат https://habr.com/ru/company/tinkoff/blog/472186/
- https://habr.com/ru/post/446816/
- https://habr.com/ru/post/482154/
О том, выделять контракт в отдельный интерфейс или нет.
https://habr.com/ru/company/funcorp/blog/545350/
Моё личное правило гласит, что если вы можете представить себе более одного способа реализации — используйте интерфейс. Если вы не можете себе представить другие способы — не используйте интерфейс.
- Для стабильных (stable) зависимостей – не выделяем.
Сущности предметной области (вроде Post, Comment) или объекты-значения (value object) являются стабильными (stable) внутренними зависимостями с одной конкретной реализацией. А потому должны в тестах использоваться напрямую (не выделяя интерфейс и не мокая их). Это соответствует стилю classical TDD (а не mockist TDD).
Различные сервисы (с бизнес поведением или нет), у которых есть Один Правильный Способ работы (пример, вычисления цен). Или Один Истинный Источник, в котором они хранятся (какой-то конкретный, от которого зависит работа клиентского кода)
Хороший пример — OrderTotalCalculator. Есть только один способ сложить различные цены в заказе, и в интерфейсе здесь смысла не будет.
- Для изменчивых (volatile) зависимостей – выделяем интерфейс, ибо такой класс может иметь несколько возможных реализаций.
ProductPricer — хороший пример интерфейса, потому что к нему легко придумать различные бизнес-правила, применяемые в разных ситуациях: GermanProductPricer, BelgianProductPricer. Также могут быть разные технические реализации: DbProductPricer, SoapProductPricer, или CachedProductPricer, оборачивающая другую реализацию.
В этом случае, нужно понять, что тестовый «двойник» – это всего лишь еще одна, упрощенная фиктивная реализация. А это значит, что тестовый «двойник» не должен наследовать и переопределять поведение оригинального класса, он должен разделять с ним общий интерфейс. И если архитектура системы построена на базе принципа инверсии зависимостей (DIP), а элементы зависят только от абстракций, то тестовый «двойник» сможет полиморфно замещать оригинальный класс без наследования.
- Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом. (c) вики (с) Роберт С. Мартин
- Наследующий класс должен дополнять, а не замещать поведение базового класса
- Если базовый класс проходит определённый юнит-тест, то его должны проходить все наследники базового класса тоже.
Цель: Должна существовать возможность использовать объекты производного класса вместо объектов базового класса. Это значит, что объекты производного класса должны вести себя согласованно (консистентно), согласно контракту базового класса.
ссылки:
- http://sergeyteplyakov.blogspot.com/2014/09/liskov-substitution-principle.html Очень хорошое описание
- https://wiki.php.net/rfc/covariant-returns-and-contravariant-parameters
- https://php.watch/articles/php-lsp
- https://www.youtube.com/watch?v=bVwZquRH1Vk
- https://madewithlove.com/liskov-substitution-principle-explained/
- https://dev-gang.ru/article/solid-%C2%ABl%C2%BB-princip-podstanovki-barbary-liskov-zbmdty8bnw/
- https://habr.com/ru/post/559724/
Почему важно следовать принципу подстановки Лисков?
- «Поскольку в противном случае иерархии наследования приведут к неразберихе. Неразбериха будет заключаться в том, что передача в метод экземпляра класса-наследника приведет к странному поведению существующего кода.
- Поскольку в противном случае юнит-тесты базового класса никогда не будут проходить для наследников.»
Основные моменты
-
Глвные моменты: Пред и пост условия. Пред условия не могут быть усилены, пост условия не могут быть ослаблены
- 1 Pre conditions д.б. теми же либо слабее (не могут быть усилены): Контрвариантность принимаемого значения (тот же либо более общий)
- 2 Post conditions: Ковариантность возвращаемого значение (пост условие)
- 3 Ковариантность выбрасываемых исключений (пост условие).
- Переопределенные методы дочерних классов должны выбрасывать тот же либо более специфичный тип исключений, который может выбрасываться в родительском классе. (The overriden methods in child classes should throw the same or more specialized exceptions that can be thrown in the parent class.)
- Никакие новые исключения не должны создаваться методами подтипа, за исключением тех случаев, когда сами эти исключения являются подтипами исключений, создаваемых методами супертипа. (No new exceptions should be thrown by methods of the subtype, except where those exceptions are themselves subtypes of exceptions thrown by the methods of the supertype).
- invariants are the same (or stronger)
-
(для php) Сигнатуры методов должны совпадать: можно расширить список параметров необязательным аргументов
-
(для php) инвариантность типа параметра в наследнике
-
(для php) хотя аргументы методов в подтипах всегда должны быть контравариантными, аргументы в конструкторах подтипов __construct () не обязательно должны быть контравариантными в php.
альтернативное описание требование в подклассам
- Производные классы не должны усиливать предусловия (не должны требовать большего от своих клиентов).
- Производные классы не должны ослаблять постусловия (должны гарантировать, как минимум тоже, что и базовый класс).
- Производные классы не должны нарушать инварианты базового класса (инварианты базового класса и наследников суммируются). + исторические ограничения («правило истории») (Подкласс не должен создавать новых мутаторов свойств базового класса)
- Производные классы не должны генерировать исключения, не описанные базовым классом.
Ковариантность - сужение типов (более специфичный тип). Для возвращаемого типа метода в дочернем классе
class Image {}
class JpgImage extends Image {}
class Renderer {
public function render(): Image;
}
class PhotoRenderer {
public function render(): JpgImage;
}
Еще пример (сразу ковариантонсть по возврат типу и контрвариантности по принимаемому аргументу)
class Foo {
public function process(int|float $value): string|int;
}
class Bar extends Foo {
public function process(int|float|string $value): int;
}
Контрвариантность - расширение типов (более абстрактный тип) в методе наследника. Он должен принять все параметры, которые может принять родитель
Про нарушение в конструкторе
abstract class CustomFieldDefinition
{
public function __construct(AccountId $accountId) { ... }
}
// Example of a Liskov Substitution Principle violation but allowed by PHP:
final class MultipleChoiceFieldDefinition extends CustomFieldDefinition
{
public function __construct(
AccountId $accountId,
array $choices
) {
// By adding an extra required argument, we just broke the method calls
// of any code trying to construct instances of CustomFieldDefinition.
}
}
So to prevent this from becoming an issue, we closed off the __construct method from being modified by making it final.
abstract class CustomFieldDefinition
{
final public function __construct(AccountId $accountId) { ... }
}
Another alternative would be making it private and having a static create(...): CustomFieldDefinition method on CustomFieldDefinition instead.
abstract class CustomFieldDefinition
{
private function __construct(AccountId $accountId) { ... }
public static function create(
AccountId $accountId,
): CustomFieldDefinition {
// We can still use the constructor here because we have access to any private
// methods and properties defined in this class, including __construct().
return new static($accountId, $variableName);
}
}
Правило истории (историческое ограничение) (для сохранения инварианта класса)
Подкласс не должен создавать новых мутаторов свойств базового класса.
Если базовый класс не предусматривал методов для изменения определенных в нем свойств, подтип этого класса так же не должен создавать таких методов. Иными словами, неизменяемые данные базового класса не должны быть изменяемыми в подклассе.
class Deposit
{
protected float $account = 0;
public function __construct(float $sum)
{
if ($sum < 0) {
throw new Exception('Сумма вклада не может быть меньше нуля');
}
$this->account += $sum;
}
}
class VipDeposit extends Deposit
{
public function getMoney(float $sum)
{
$this->account -= $sum;
}
}
С точки зрения класса Deposit поле не может быть меньше нуля. А вот производный класс VipDeposit, добавляет метод для изменения свойства account, поэтому инвариант класса Deposit нарушается. Такого поведения следует избегать.
В таком случае стоит рассмотреть добавление мутатора в базовый класс.
- Программные сущности не должны зависеть от частей интерфейса, которые они не используют (и знать о них тоже не должны).
- Клиенты не должны зависеть от методов, которые они не используют.
Есть небольшое сходство с YAGNI - «клиенту могут и не понадобятся эти методы интерфейса»
Этот принцип относится к недостаткам "жирных" интерфейсов. Говорят, что класс имеет жирный интерфейс, если функции этого интерфейса недостаточно сцепленные (not cohesive). Иными словами, интерфейс класса можно разбить на группу методов. Каждая группа предназначена для обслуживания разных клиентов. Одним клиентам нужна одна группа методов, другим – другая
Нарушение этого принципа зависит не столько от самого класса, сколько от сценариев его использования. Если в нашей бизнес-модели четко разделяются операции чтения и обновления данных (такой себе CQRS), то наличие одного класса со всеми операциями однозначно делает интерфейс слишком толстым. С другой стороны, если наше приложение напичкано кучей простых форм, которые мапятся 1 к 1 с нашими репозиториями, то тогда принцип ISP не нарушается.
Отличие принципов SRP и ISP. Мы хотим следовать SPR, чтобы наш класс был связанным внутри (highly cohesive ), что позволит с меньшими усилиями его понимать и развивать. Следование же принципу ISP уменьшает связанность (low coupling) между классом и его клиентом, ведь теперь клиент завязан не на весь интерфейс класса, а лишь на его часть.
ISP отличается от других принципов из семейства SOLID тем, что глядя лишь на класс/модуль мы не можем сказать, нарушается принцип ISP или нет. Нарушение этого принципа зависит не столько от самого класса, сколько от сценариев его использования. Если разные клиенты интересуются разными аспектами данного класса/модуля, то выделение дополнительных интерфейсов сделают такое разделение более очевидным.
Если же интерфейс класса «жирный» и непонятный, то класс не просто нарушает ISP, он нарушает SRP и ждет рефакторинга и упрощения.
Существует несколько причин для выделения у класса дополнительных интерфейсов.
-
Во-первых, может появиться необходимость выделить интерфейс для класса целиком. Это бывает необходимым для «изменчивых» зависимостей (классов, взаимодействующих с внешним миром) или для стратегий (когда нам нужно полиморфное поведение, определенное интерфейсом).
-
Во-вторых, мы можем выделить некий аспект текущего класса, который покрывает не весь функционал, а лишь его часть, т.е. его отдельный аспект. Примером может служить добавление классу «ролевых» интерфейсов (Role Interface), типа ICloneable, IComparable, IEquatable и т.п. При этом выделение таких интерфейсов обычно требуется для использования класса в новом контексте ( класс Person реализуется IComparable для корректного использования в SortedList).
-
В-третьих, может потребоваться выделение специализированных бизнес-интерфейсов, когда станет очевидным, что наш класс используется в нескольких разных контекстах. Например, когда репозиторий используется разными клиентами двумя способами: одна группа клиентов использует лишь операции для чтения данных, а другая группа – для обновления. Обычно это говорит не столько о нарушении ISP, сколько о наличии скрытой абстракции (IRepositoryReader и IRepositoryWriter).
-
В-четвертых, нам может потребоваться использовать несколько разных классов из разных иерархий наследования полиморфным образом. При этом обычно такое выделение происходит во время рефакторинга, когда необходимость в этом становится очевидной:
«Ага, вот у нас есть два класса, которые мы могли бы использовать совместно, но они уже унаследованы от разных базовых классов. Давайте выделим для каждого из них единый интерфейс и избавимся от богомерзких if-else».
- Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
http://sergeyteplyakov.blogspot.com/2014/09/the-dependency-inversion-principle.
Термины:
- Модули верхних уровней - Класс, выполняющий действие с помощью инструмента
- Модули нижних уровней - Инструмент, необходимый для выполнения действия
- Абстракция - интерфейс, соединяющий два класса
- Детали - детали работы инструмента (как инструмент работает)
Высокоуровневый класс не должен зависеть от конкретных инструментов для использования их в своей логике. Вместо этого, класс должен зависеть от интерфейса, через который можно подключить конкретный инструмент к классу
Цель: ослабление (устранение) зависимости высокоуровневых классов от низкоуровневых путем введения интерфейса
цели принципов:
- SRP предназначен для борьбы со сложностью;
- OCP помогает в вопросах расширяемости и параллельной разработки;
- LSP указывает, как использовать наследование «правильно»;
- ISP выделяет разные аспекты класса;
- DIP должен бороться с признаками плохого дизайна в архитектуре
признаками плохого дизайном:
- Дизайн является жестким (rigid ), если его тяжело изменить, поскольку любое изменение влияет на слишком большое количество других частей системы.
- Дизайн является хрупким (fragile), если при внесении изменений неожиданно ломаются другие части системы.
- Дизайн является неподвижным (immobile ), если код тяжело использовать повторно в другом приложении, поскольку его слишком сложно «выпутать» из текущего приложения.
Цель: заменить композицию агрегацией. То есть вместо создания зависимостей напрямую, класс должен требовать их у более высокого уровня через аргументы метода или конструктора. При этом зависимость должна передаваться не в виде экземпляров конкретных классов, а в виде интерфейсов или абстрактных классов.
interface IDependency
{ }
class ConcreteDependency : IDependency { }
// ConcreteClass не знает о ConcreteDependency,
// он зависит от "абстракции" - IDependency!
class ConcreteClass
{
public ConcreteClass(IDependency dependency)
{}
}
Ключевую роль в ослаблении связей между клиентом и сервисом играет принцип инверсии зависимостей (dependency inversion principle, DIP), предлагающий преобразовать прямую зависимость между модулями в обоюдную зависимость модулей от общей абстракции.
-
Модули высокого уровня (бизнес сущности с высокоуровневыми политиками), не должны зависить от низкоуровневых реализаций (библиотек для работы с бд, отправкой писем, очередями либо других инфраструктурных вещей и фреймворков). А должны зависить от абстракции.
-
На этом принципе построенны реализация направления зависимости в "Чистой архитектуре", гексогональной, онион, DDD . Все зависимости должны направлены в стороны высокоуровневых политик (против поток исполнения).
-
Модули высокого уровня определяют абстракцию (интерфейс), и используют ее, а модули нижних уровняй реализую это интерфейсы и прокидывают адаптеры на уровни выше (di, парамметры).
-
(+) это позволяет писать более чистый и независимый код (от библиотек, фреймворков и прочего)
-
(+) улучшает сопровождение
-
(+) упрощает тестирование отдельных компонентов. В тестах мы можем не использовать сложные реализации, а заменять их заглушками для простоты.
Абстракция и реализация Давайте рассмотрим вопрос «зависимости от абстракции» более подробно.
Любой класс содержит две части: открытый интерфейс и реализацию. Реализация скрыта от клиентов класса и может в разумных пределах изменяться не затрагивая клиентов. Изменение открытой/защищенной части класса влечет за собой изменение клиентов и/или наследников.
Если под «абстрактным интерфейсом» понимать открытую часть класса, то в использовании конкретных классов напрямую нет никакой проблемы. Если же под «абстрактным интерфейсом» понимать использование специфических конструкций, типа интерфейсов или абстрактных классов языка C#, то тут нужно понять, что это дает.
Когда выделять интерфейсы? С практической точки зрения мы знаем, что иногда нам можно и нужно создавать экземпляры конкретного класса напрямую, а иногда стоит выделить у класса интерфейса и передавать уже его.
Когда выделять интерфейс (.NET interface) у класса:
- Класс является реализацией некоторой стратегии и будет использовать полиморфным образом.
- Реализация класса работает с внешним окружением (файлами, сокетами, конфигурацией и т.п.). Использование напрямую классов, работающих с файлами, сокетами, базой данных и т.п. в бизнес-коде приложения может быть проблематичным. Значительно лучше выделить более высокоуровневую стратегию (IYourDataProvider ), одна из реализаций которой получает данные из файла/бд, сети и тп
- Класс находится на стыке модулей.
Когда не нужно выделять интерфейс класса:
- Класс является неизменяемым «объектом-значением» (Value Object) или объектом-данных (Data Object).
Нет никакого смысла от выделения интерфейсов для простых классов, особенно для неизменяемых объектов-значений (т.н. Value Objects). Неизменяемые объекты-значения – это идеальный механизм борьбы со сложностью: их поведение (если оно есть) простое и абсолютно стабильное.
- Класс обладает стабильным поведением (не работает с внешним окружением).
К стабильным зависимостям относятся классы, поведение которых не зависит от времени: они не работают с внешним окружением и интерфейс которых относительно стабилен. Поведение изменчивых зависимостей зависит от времени или контекста, или же их интерфейс часто изменяется.
Исходя из всего этого можно выделить несколько советов по выделению интерфейсов:
- Выделяйте высокоуровневые стратегии (IMessageProvider, а не IMSMQProvider).
- Старайтесь передавать результаты работы зависимостей, а не сами зависимости (передавайте Configuration (value object ), а не IConfigurationProvider).
- Используйте стандартные абстракции (Stream вместо своего собственного IFileStream, Task вместо IAsyncWorkItem и т.п.)
Принципы управления зависимостями:
- Делайте важные зависимости явными.
- Старайтесь минимизировать число зависимостей (если класс может о чем-то не знать, то избавьте его от этой лишней информации).
- Выделяйте изменчивые зависимости и старайтесь сделать их высокоуровневыми.
- Выделяйте стратегии
- Стремитесь к неизменяемости: объекты-значения (Value Objects ) и функции без побочных эффектов – это идеальный строительный материал.
- Не нужно «инвертировать» зависимости: дизайн – процесс итеративный, модули верхнего уровня влияют на дизайн модулей нижнего уровня и наоборот.
todo
di container
Ввел Крейг Ларман в своей книге. Суть: описание фундаментальнымх принципов распределения ответственности между классами.
GRASP'овские паттерны являются обобщением GoF'овских паттернов, а также непосредственным следствием принципов ООП. Они дополняют недостающую ступеньку в логической лестнице, которая позволяет получить GoF'овские паттерны из принципов ООП. Шаблоны GRASP являются скорее не паттернами проектирования (как GoF'овские), а фундаментальными принципами распределения ответственности между классами. Они, как показывает практика, не обладают особой популярностью, однако анализ спроектированных классов с использованием полного набора GRASP'овских паттернов является необходимым условием написания хорошего кода. GRASP покрывает все аспекты SOLID
Связь различных принципов (разные взгляды на одно и тоже)
- coheasion = imformation expert = factory = srp = encapsulatio
- inderection = dip
- LSP = полиморфизм
Полный список шаблонов GRASP состоит из 9 элементов:
Information Expert Creator Controller Low Coupling High Cohesion Polymorphism Pure Fabrication Indirection Protected Variations
-
Polymorphism
Информация должна обрабатываться там, где она содержится (напоминает TellDontAsk от Фаулера)
- https://habr.com/ru/company/otus/blog/491636/ статья с описанием
Описывает основополагающие принципы назначения обязанностей классам и объектам. Согласно описанию, информационным экспертом (объектом наделенным некоторыми обязанностями) является объект, обладающий максимумом информацией, необходимой для выполнения назначенных обязанностей.
Т.е. если объект владеет всей нужной информацией полностью для какой-то операции или функционала, то значит, что этот объект будет выполнять всё сам, либо получать делегировать выполнение этой операции от других.
- повышает связность модулей
Итак, рассмотрим пример. Есть некая система продаж. И есть класс Sale (продажа). Нам необходимо посчитать общую сумму продаж. Тогда кто будет считать общую сумму по продажам? Конечно же, класс — Sales , потому что именно он обладает всей информацией необходимой для этого.
Cоздавать экземпляры класса должен класс, которому они нужны
- https://habr.com/ru/company/otus/blog/505618/ Creator или Создатель — суть ответственности такого объекта в том, что он создает другие объекты. Сразу напрашивается аналогия с фабриками. Так оно и есть. Фабрики тоже имеют именно ответственность — Создатель.
Но есть ряд моментов, которые должны выполняться, когда мы наделяем объект ответственностью создатель:
- Создатель содержит (композит) или агрегирует создаваемые объекты (содержит в себе как свойство или коллекцию)
- Создатель использует создаваемые объекты (основной объем работы с объектом Б происходит посредством объекта А)
- Создатель знает, как проинициализировать создаваемый объект. Объект А обладает данными инициализации объекта Б (каждый раз при создании объекта Б, данные берутся из объекта А)
- Создатель записывает создаваемые объекты
Альтернатива - шаблон "Фабрика".
Controller
- это объект, который отвечает за обработку входящих системных событий (пользовательские нажатия кнопок, срабатывания таймеров т.д.), и при этом не относится к интерфейсу пользователя. Controller определяет методы для выполнения системных операций.
Controller— это объект-прослойка между UI-логикой и предметной (бизнес) логикой приложения. Создаем контроллер так, чтобы все вызовы от UI перенаправлялись именно ему и соответственно, все данные UI тоже получает только через него.
Напоминает известные MVC и MVP? Так и есть. Это по сути Presenter из MVP и контроллер из MVC.
Контроллер отвечает на такой вопрос: «Как UI должен взаимодействовать с доменной логикой приложения?» Или ещё проще: «Как взаимодействовать с системой?». Это чем-то напоминает фасад дома. Фасад тоже предоставляет облегченный доступ к целой подсистеме объектов. Так и тут: контроллер для UI своего рода фасад, который предоставляет доступ к целой подсистеме бизнес-логики.
- Отвечает за операции, запросы на которые приходят от пользователя, и может выполнять сценарии одного или нескольких вариантов использования (например, создание и удаление);
- Не выполняет работу самостоятельно, а делегирует компетентным исполнителям;
- https://habr.com/ru/company/otus/blog/505852/
- https://stackoverflow.com/questions/3085285/difference-between-cohesion-and-coupling Низкая зацепление (Low Coupling) и Высокое связность (High Cohesion).
Cистема должна состоять и слабо связанных классов, которые содержать связанную бизнес — логику.
Соблюдение этих принципов позволяет удобно переиспользовать созданные классы, не теряя понимания об их зоне ответственности.
Их имеет смысл рассматривать только в паре, потому что рассмотрение их по отдельности в пределе приводит к явно плохому коду. Эти принципы могут рассматриваться в разных контекстах, при проектировании: методы, классы, модули, слои, микросервисы.
Внутреннюю структуру проекта нужно поддерживать в таком состоянии, которое позволит внести значительные (и не очень) изменения с минимальными переделками.
Рассмотрим понятие меры зацепления модулей и меры связности модуля. Мера Coupling (зацепление) модулей определяется количеством информации которой располагает один модуль о природе другого. Coupling - связанность между двумя классам, модулями. Можно померить посмотрев на кол-во внешних зависимостей Формула стабильности - кол-во классов, на которые данный модуль операется / кол-во классов, на которые данный модуль операется +
1 - не стабильный 0 - ближе к 0
В свою очередь, мера cohesion (связности) модуля определяется степенью сфокусированности его обязанностей.
Логика внутри модулей сильно завязана на их внутренее устройство и очень слабо зависит от других модулей. Реализация каждого модуля внутри уникальна и сокрыта (инкапсулирована) от других. При этом имеет сильные связи между элементами находящимися внутри модуля, а с элементами других модулей слабые, Один слой может обращаться ко второму и использовать только небольшую часть его классов.
Плюсы (соответствие данным патернам позволяет):
- легкость модификации и сопровождение модулей (не оказывая существеного влияния на работу других модулей)
- повышает степень повторного использования
Low Coupling (зацепление, сопряжение))
- Необходимо распределить ответственности между классами так, чтобы обеспечить минимальную связанность.
- объекты в системе должны знать друг о другие как можно меньше
- паттерн, который указывает, как назначать ответственность: слабая зависимость между классами, изменение одного должно иметь минимальные последствия для другого, максимальная возможность повторного использования
- метрика, показывающая, насколько компоненты системы независимы друг от друга. Слабо связанные компоненты не зависят от внешних изменений и легко могут быть использованы повторно. IoC и DIP являются средствами для достижения слабой связности компонентов в системе.
High Cohesion (связность - сила)
- классы должны содержать связанную бизнес — логику.
- (по Макконелу, рассматривает только на уровне методов, ибо на уровне классов данный принцип вытесняется более современными концепциями: абстракция, инкапсуляция)
- характеризует соответствие выполняемых в методе операций единой цели
- предпологает, что класс спланирован с единственным и конкретным назначением
- метрика(принцип), показывающая, насколько хорошо код сгруппирован по функционалу. Выполнение принципов DRY и SRP ведет к коду с сильной связностью, в котором части, выполняющие одну и ту же задачу расположены «близко» друг к другу, а разные — изолированы. Выполнение ISP тоже ведет к сильной связности, хотя это и не так очевидно. Разделяя один интерфейс на более мелкие части вы группируете их по функционалу, оставляя в каждом интерфейсе только наиболее связанные между собой методы.
Если возвести Low Coupling в абсолют, то достаточно быстро можно прийти к тому, чтобы разместить всю функциональность в одном единственном классе. В таком случае связей не будет вообще, но все при этом понимают, что что-то тут явно не так, ведь в этот класс попадет совершенно несвязанная между собой бизнес — логика. Принцип High Cohesion говорит нам следующее: классы должны содержать связанную бизнес — логику.
Чистая выдумка. суть в выдуманном объекте (этакий хак, без которого не соблюдаются остальные важные принципы). Аналогом может быть шаблон Service в парадигме DDD.
Иногда, сталкиваемся с таким вопросом: «Какой объект наделить ответственностью, но принципы информационный эксперт, высокая сцепленность не выполняются или не подходят?». Тогда нужно использовать синтетический класс который обеспечивает высокую сцепленность. Тут без примера точно не разобраться.
Итак — ситуация. Какой класс должен сохранять наш объект Sale в базу данных? Если подчиняется принципу «информационный эксперт», то Sale , но наделив его такой ответственностью мы получаем слабую сцепленность внутри него. Тогда можно найти выход, с оздав синтетическую сущность — SaleDao или SaleRepository , которая будет сильно сцеплена внутри и будет иметь единую ответственность — и правильно сохранять Sale в базу.
Так как мы выдумали этот объект, а не спроектировали с предметной области, то и он подчиняется принципу «чистая выдумка».
Синтезированный (выдуманный) объект не относится к предметной области, но:
- Уменьшает зацепление;
- Повышает связность;
- Упрощает повторное использование.
«Pure Fabrication» отражает концепцию сервисов в модели DDD
- Пример задачи: Не используя средства класса «А», внести его объекты в базу данных.
- Решение: Создать класс «Б» для записи объектов класса «А» (см. также: «Data Access Object»).
Посредник / Перенаправление
Принцип позволяет реализовать низкое зацепление между классами, путем назначения обязанностей по их взаимодействию дополнительному объекту — посреднику.
Плюсы:
- уменьшает зацепление между классами
Примеры:
- паттерн "посредник" из GoF
- Controller из MVC - посредником между данными их их представлением. Ослабляет зацепление между данными (model ) и их представлением (view).
UI-логике на самом деле нужен не контроллер, а модель, доменная логика. Но мы не хотим, чтобы UI -логика была сильно связанна с моделью, и возможно в UI мы хотим получать данные и работать с разной предметной логикой. А связывать UI-слой с бизнес логикой было бы глупо, потому что получим код который будет сложный для изменений и поддержки. Выход — вводим контроллер как посредник между View и Model.
- искуственный класс SomeModelRepository (из принципа выше Pure Fabrication ) - является промежуточным слоем между доменными сущностями и хранилищем, в которое их нужно сохранить.
Устойчивый к изменениям / сокрытие реализации
Проблема модификации системы наиболее актуальна в условиях динамически изменяющихся требований. Суть принципа: защита элементов системы от изменения других элементов (объектов и подсистем) путем:
- определенеия "точки изменения" - места в системе, которые наиболее часто будут подвержены изменениям и модификации
- устранение "точек изменения" путем выделения/фиксирования их в абстракции (интерфейсе), (только) через интерфейс возможно взаимодействие с другими элементами системы
- поведение может варьироваться лишь через создание другое реализации интерфейса
Все это делается для того лишь, чтобы обеспечить устойчивость интерфейса. Если будет много изменений связанных с объектами, он, в таком случае считается неустойчивым, и тогда нужно выносить его в абстракцию
(см в разделе ООП) возможность трактовать однообразно разные объекты с одинаковым интерфейсом.
Полиморфизм решает проблему обработки альтернативных вариантов поведения на основе типа. Яркий пример этого — шаблон из GoF, — Strategy (Стратегия). (Chain of Resposibility, Command etc - все основаны на полиморфизме)
Основные положения Vertica https://docs.google.com/document/d/1gSRmERjHqVBRIU6OpmvAPWOYBZlGHxsrWg4CXLiq5U0/edit
- https://losst.ru/sravnenie-mysql-i-postgresql
- https://mcs.mail.ru/blog/postgresql-ili-mysql-kakaya-iz-etih-relyacionnyh-subd
Партицирование (секционирование) - разделение таблиц (и тд) на отдельные логические части по определенными критериям (с раздельными параметрами физического хранения). В рамках одной ноды (одного инстанса СУБД).
Файл с данными таблицы разрезается по какому-то условию на несколько не больших файлов — партиций. Для случая с логами разумно партиционировать таблицы по полю, содержащему даты события. Часто бывает разумно резать таблицу на partition по году по месяцу или по дням месяца/недели.
- подход к масштабированию БД (каждая БД реализует по разному)
- разделение по определенным признакам
- разделенные данные лежат отдельно (в отдельных файлах, папках бд. В отдельных физических дисках, организованных в REIT-массив)
- в партиции возможен механищм наследования схемы БД (мускл vs postgres)
- можно делить как вертикально (поколоночно режем большую таблицу), так и горизонтально (по строчно по признаку)
- можно эффективно утилизировать дисковую подсистемы (хранить разные партиции на разных физ дисках)
- все партиции в пределах одного инстанса БД (сервера)
Типы партиционирование (критериии разделения данных):
- key (по ключу страны)
- list- (список значений. США, Канада - 1 парт, Россия, Китай - 2 парт, и тд)
- range (по дате, например. с 2006 до 2010 - 1я парт, с 2011 до 2020 - 2я парт)
- hash (считаем хеш-функцию от пользователя или ключа % кол-во партиций = номер партиции)
- композитные (составные) критерии - оследовательно применённые критерии разных типов.
Возможные ловушки:
- наследования индексов и constraints (postgres. Для всех партиций нужно явно создавать индексы и constraint-ы)
- граничные значения выражений (2000 до 2010 - 1я, 2010 по 2020 -2я. Нет гарантий, куда попадет значения)
- (postg) нужно явно задавать полуоткрытые интервалы
- [2010, 2020); [2020, 2030) --- 2010 <= x < 2020; 2020 <= x < 2030
- (postg) нужно явно задавать полуоткрытые интервалы
- наследования схем (postgres) - незабываем использовать INHERITS
- нужно понимать реализацию
Выводы (цели):
- Используется в целях повышения управляемости, производительности и доступности для больших баз данных.
- лучшая утилизация диска при правильном разбиении
- может ускорить запросы (НО НУЖНО МЕРИТЬ:
- Если диск медленный, прирост можно не заметить.
- если мы разбили большой файл в 1 ТБ на 10-100 мальеньких кусочков и в запросе нам нужно просмотреть только 1 партицию, то это должно дать буст, т.к. нам не надо будет просматривать все файлы)
- может замедлить запросы (если запрос FULLSCAN, то разбивка на 100-1к партиций только ухудшит ситуацию. Предеться потрогать больше файловых дискрипторов
- читать документацию
- не решается проблема ограниченности сервера
Где используется:
- для исторических данных
- для аналитики
- для группировки данных (география, категории, языки)
В отличие от Шардинга (сегментирования), где каждый сегмент управляется отдельным экземпляром СУБД, и используются средства координации между ними (что позволяет распределить базу данных на несколько вычислительных узлов), при секционировании доступ ко всем секциям осуществляется из единого экземпляра СУБД (или симметрично из любого экземпляра кластерной СУБД, такого, как Oracle RAC).
- вертикальное партицирование - поколоночно режем большую таблицу
- горизонтальное - режим построчно в отдельные таблицы (проекции и т.д.) в рамках одной и тойже схемы (сервера, ноды). Если на разных нодах, то это уже шардинг
Шардинг (Сегментирование) (горизонтальное партиционирование) - разделение (партиционирование) базы данных на отдельные части так, чтобы каждую из них можно было вынести на отдельный сервер. Этот процесс зависит от структуры Вашей БД и выполняется прямо в приложении в отличие от репликации:
- подход к масштабированию данных, а не БД (горизонтальное масштабирование на разные сервера)
- хранение данных на различных инстансах БД (лучше на отдельных серверах)
- различные стратегии разделения данных
- более сложная реализация
НО, шардинг:
- НЕ ПАРТИЦИОНИРОВАНИЕ
- НЕ РЕПЛИКАЦИЯ
- но можно использовать все техники вместе
Главные признаки:
- много запросов на вставку данных + рост БД
- БД не помещается на одном сервере
- Данные одной таблицы растут в X раз быстрее
Цели:
- позволяет горизотнально масштабировать данные
- ускоряет обработку запросов (особенно на запись), т.к. мы пишем физически разные данные на разные сервера. Но не
факт что ускорит чтение, т.к. при селектах и джойнах данных с разных шардов, это все становиться сложнее и дольше
(либо на клиенте надо мержить данные, либо нужно доп прокси)
- репликация не ускоряет запись, т.к. данные между собой реплицируются и количество записываемых данных остается прежним при увеличении кол-ва серверов (нужно записать на каждый из них)
- повышает отказоустойчивость (при выходе из строя одно из серверов, остальные сервера с данными могут
использоваться). Но (минусы):
- при джойнах данные будут не полноценны, т.к. часть данных осталась на отвалившемся шарде
- нужно мониторить и поддерживать много серверов, а не один
- экономия денег на дорогих серверах
Где используются (проекты с огромным кол-вом данных)
- соц сети
- системы сообщения, хостинги данных (вк, ютуб)
- сервисы сбора данных (гугл аналитикс)
- гео-распределенные сервисы
Какие альтернативы для шардинга?
Инфраструктура:
- кеширование (горядчий кеш) - inmemory БД или в приложении кеш. Обычно на основе алгоритма LRU (100 последних запрашиваемых записей). Реализуется через list + hash map
- партиционирование (для таблиц с тяжелыми запросами можем выделить их на отдельный диск или REIT-массив)
- системы очередей
- реплики (масштабируем чтение через мастер-слейв)
- хранилище данных (data warehouse) - данные, которые нужны для тяжелых аналитических запросов лучше перегнать в DWH или OLAP хранилища, чтобы не занимать большую часть ресурсов СУБД тяжелыми аналитическими запросами
Дизайн приложения
- Микро-сервисные паттерны (CQRS). До записи данных на мастер можем сгенерить uuid (уникальный ключ), отправить джоб с этим uuid по шине и отдать юзеру этот uuid, чтобы позже он смог найти результат выполнения запроса (через какой-то интервал, достаточно быстрый)
- контекст и уменьшение связей данных БД - выделяем часть данных, нужных для определенного контекста, в отдельные таблицы или БД. Можем вынести отдельно
- Изменение схемы (NoSQL) - списковые данные, редкоменяющиеся, можно выделить в NoSQL
- архивирование -свежие (полугодичные данные) используются активно, остальные перекладываются в отдельные партиции (таблицы), выборки к ним дольше (но это норма)
####Виды шардинга
Как шардировать данные:
- никто не знает правильный универсальный способ
- посмотрите на свои данные
- посмотрите на запросы
- выберите правильный критерий шардирования
По организации БД:
- вертикальный - это выделение таблицы или группы таблиц на отдельный сервер.
- используется для балансировки нагрузки при неравномерной заполяемости БД и при неравномерной нагрузке на часть данных
Берем, допустим, таблицу фото, таблицу юзеров и др., растаскиваем их на отдельные сервера. Если таблицы были большие, то все становится меньше, памяти ест меньше, все хорошо, только нельзя JOIN'ить и приходится делать запросы типа WHERE IN, т.е. сначала выбираем кучу ID'шников, потом все эти ID'шники подставляем запросу, но уже к другому коннекту, к другому серверу.
- горизонтальный - это разделение одной таблицы на разные сервера. Это необходимо использовать для огромных таблиц, которые не умещаются на одном сервере.
- высокая скорость на запись
- равномерно распределяется объем
- требуется регардинг
- лучше скейлиться (можно неограничено число серверов подключать)
- гибридный
например, мы берем и делаем несколько БД с юзерами.
Можно достаточно просто выбрать сервер — остаток от деления на количество серверов. Альтернатива — завести карту, т.е. для каждой записи держать в каком-нибудь Redis'е или т.п. ключ значения, т.е. где какая запись лежит.
Сложнее — это когда не удается сгруппировать данные. Надо знать ID данных, чтобы их достать. Никаких JOIN, ORDER и т.д. Фактически мы сводим наш MySQL или PostgreSQL к key-valuе хранилищу, потому что мы с ними ничего делать не можем. https://ruhighload.com/Шардинг+и+репликация
####Виды горизонтального шардинга key (hash) based sharding Прогоняем значения колонки (или нескольких через) хеш функцию
Характеристики:
- формула F(key) => shard_id
- F и key очень важны
- наиболее распространненный способ
- F - должна быть хорошей
- с мин коллизий
- равномерное распределение ваших данных по шардам (надо тестить)
- не должна быть сложно мат функцией
Плюсы:
- просто и понятно
- равномереное распределение (при правильном подборе хеш функции на реальных данных с оценкой гистограммы распределения)
Минусы:
- добавление и удаление шарда - может быть больно
- особенности распределения нужно учитывать при разработке приложения (нужно на клиенте уметь расчитывать значение зеш функции и определять, в какой шард ходить)
range based sharding - разделение по диапазонам значений колонки
Характеристики:
- еще называют table function/virtual bucket
- статический конфиг range => shard_id
- формула: func(key) -> virtual_bucket -> shard_id
Плюсы:
- просто и понятно
- легко тестировать и анализировать
Минусы:
- возможная неравномерность распределения
- сложность обновления данные (поддержание стат конфига)
directory based sharding - мапинг конкретного значения поля на шард (через таблицу, сервис дискавери или еще как-то)
Характеристики:
- требует определенной структуры данных
- похож на range based (только вместо range - конкретный мапинг значения на шард в
- статический конфиг key -> shard_id
Плюсы:
- не нужно думать о хеш функции
- совпадает с вашей бизнес-логикой
- можно реализовать гибкую логику на стороне клиента
Минусы:
- подходит только для low-cardinality ключей (страна, тип, зона) . По id или номеру телефона - очень большой конфиг
- потенциально можно упереться в аппаратные мощности по определенному ключу (из-за разной нагрузки на каждый из шардов)
- боль с обновлением конфига
- single point of failure - сервер или таблица с конфигом - единая точка отказа
Умный клиент Приложение хранит логику определения шарда, на который нужно ходить за данными и само понимает, на какой сервер ходить
Плюсы:
- просто метод (нет доп абстракций, трат на инфраструктурные решения, вся логика в коде
- нет лишних хопов по сети
- прозрачность схемы для разроботчиков (большая осведомленность)
Минусы:
- нужно учитывать при разработке
- для джойнов нужно склеивать ответы от шардов на стороне клиента
- приложение должно знать довольно много про инфраструктуру (hosts, credentials)
- сложность с обновление
- сложность с тестированием
- как делать решардинг?
Прокси Прокси может сам определенить номер шарда, куда сходить, может сам сджойнить данные с разных шардов.
Плюсы:
- приложение ничего не знает о шардинге
- код не меняется
Минусы:
- лишний хоп (hop*)
- потеря latency
- SPOF
Прокси Приложение хранит логику определения шарда, на который нужно ходить за данными и само понимает, на какой сервер ходить
Плюсы:
- приложение ничего не знает о шардинге (но только если прокси умный)
- код не меняется
- умные прокси могут хранить пулл коннектов
Минусы:
- поддержание инфраструктуры для прокси
- лишний хоп (hop*)
- потеря latency
- SPOF - single point of failer (если не скейлиться)
- потеря в функциональность (*) - тупые прокси могу не уметь джойны и тп, только простые select * where
Координатор - прокси с небольшой логикой. Координатор знает о всех шардах, умеет умные запросы с джойнами, есть кеширование и тд
Плюсы:
- приложение ничего не знает о шардинге
- код не меняется
- есть возможность оптимизации и кеширования
Минусы:
- лишний хоп
- потеря latency
- инфраструктурная сложность
- потеря функциональност
- нагрузка
Решардинг
Задачи решардинга
- добавление/удаление нод
- потребность "передвинуть данные"
- исправление ошибок при выборе hash-функции
- устранение отказа оборудования
- когда-то придется решардить так или иначе
Стратегии: Формула решардинга: oldHash(key) -> newHash(key)
1.В лоб - если используется % от кол-ва шардов object.date % shards
- самый простой и тупой
- при изменении числа шардов ничего не надо менять и делать (макс - подправить в коде кол-во шардов)
- в хужшем случае - перемешаются все данные
- возможно надо будет удалять дубликаты
2.Пересчитать и переложить все oldHash(key) != newHash(key) - никогда
- довольно безопасно мигрировать (фоново в отдельный кластер или в этом же)
- легко тестировать
- сложно реализовать (за раз не сделать, нужно кусочками - по годам, серверам и тп). Автоматически нельзя
3.Переложить половину - лайфак. Рости в кол-ве серверов, равным пропорционально двойки 2^N, -> 2^(N+1), где N - кол-во шардов
- дорого
- вероятность oldHash(key) == newHash(key) ~ 50%
Варианты хеширования, которые уменьшают боль решардинга
-
Consistent hashing (ТУДУ нужна картинка или гиф)
- берем весь диапазон значения, которые генерит хеш-функция и раскидываем на круг (от 0 до 2^128)
- затем на это круг расставляем наши шарды (на одинаковых расстояниях)
- результат хеш функции попадаем в определенный диапазон круга и мы по часовой стрелки берем ближайший шард
- при добавлении нового шарда, туда переедет только часть данных с одного шарда (остальные данные не будут решардится)
-
HRW-hashing, он же rendezvous hashing
- хеш функция зависит от номера шарда
-
от google
- jump hash (2014)
- multi probe (2015)
- maglev (2016)
-
плохо распределили данные - разбалансированные шарды. Решения:
- подобрать лучший ключ/функцию шардирования
- решардинг (если есть смысл
-
JOIN - сложно аккумуляровать данные с разных серверов
- нужно это делать либо на клиенте либо на прокси
- может ухудшиться/улучшиться перфоменс (зависит от кол-ва сереров и сложности запроса, от издержек на передачу данных между шардами)
- (решения) - держать нужные данные в одной БД (добавлять в каждый ключ id user-а или глобальной сущности)
- делать вычисления на одной бд
- попробовать координатор
-
Проблемы эксплуатации
- организационные
- проблемы архитектуры
- проблемы выбранного решения
Репликация - полная копия хранимых объектов (таблицы, представления либо все бд)
- копирование
- один из способов масштабирования
- не ускоряет запись
- помогает ускорить чтение (с поправкой на лаг репликации)
- помагает при падении (использование реплики как механизм для защиты от падений - плохой тон)
- не backup
Обычно делается репликация мастер-слэйв, есть репликация мастер-мастер. Можно делать репликацию вручную, можно делать шардирование и можно делать партицирование.
- https://ruhighload.com/Шардинг+и+репликация
- https://ruhighload.com/Репликация+данных
- https://ruhighload.com/Как+настроить+mysql+master-slave+репликацию%3f
- https://ruhighload.com/Оптимизация+репликации+в+mysql
- https://habr.com/ru/company/otus/blog/491106/ (про sync, async и semisync (полусинхронную_) репликацию)
- master-slave, master-master
master-slave
- один источник данных
- плюсы:
- сама простая и распространненый подход
- чтение со слейвов, запись на мастер
- легкое добавление реплик (слейвов)
- минусы:
- при отказе мастера, кластер становиться неработоспособным
master-master (самый простой пример - git)
- плюсы
- нет единой точки отказа
- постоянное время работы
- легкий failover
- минусы
- очень мало РБД имплементят схему мастер-мастер (сложности в решении конфликтов по изменению одних и тех же данных)
- нет консистентности
проблема мастер мастер репликации
- Самое важное - не масштабируется запись, т.к. пишем на все реплики (для масшт-я чтения нужно шардирование
- Применение в 1 ЦОДе (цетнре обработки дынных) - сомнительная идея (плюсы не получаем, но ловим все проблемы
Вариантны применения:
- географическая распределенность - разные мастера и слейвы привязываются к разных гео регионам
- позволяет снизить latency (есть нюанс, последняя миля)
- применимо для world-wide игроков
- есть нюансы с dns, оптоволоконных колец,
- hot-standby реплика - если мастер упал, то мы переключаем трафик на другую репликую (понижаем downtime)
- offline клиенты (оплата в самолете, git-репозитории, оплата в транспорте). Реализовать сложно. CouchDB было сделана спец для этого случая
Цена мастер-мастер:
- усложнение логики
- конфликты - может получиться, что при одновременном изменении состояния будут конфликтовать. Решения:
- избегать конфликтов (добавлять в операции уникальный id мастера, с которого делаются записи)
- last/first wins - первое/последнее изменение перетерает предыдущее. СЛОЖНО, т.к. часы на серверах не синхронизированы (невозможно идеально их синхронизировать)
- ранг реплик (выигрывает запись от старшей реплики)
- слияние
- решение конфликтов на клиенте
- conflic-free replicated data type (CRDT) (присылать не точные изменения, а дельты -10 +15)
- mergeable persistent data structures
Для гео распределенных ЦОД будут преимущества:
- производительность (пользователи в разных районах земли пишут в мастер ближайшего ЦОДа)
- устойчивость к уходу ЦОДа (умер один ЦОД, используем другой)
- устойчивость к проблемам сети
- По синхронизации данных
-
sync - закомитили локально, закомитили удаленно (Postgres). Сделанное изменение видно всем и везде
- prepare the transaction in the storage engine (InnoDB))
- write the transaction to the binary logs
- complete the transaction in the storage engine
- send part of binary logs to replocas
- execute the transaction on replocas (1-3)
- return an acknowledgment to the client
- (+) данные всегда консистенты
- (-) узкое горлышко - нужнождать ответа от всех реплик
-
async - закомитили локально, все (mysql, postgres). Никаких гарантий на другом конце
- prepare the transaction in the storage engine (InnoDB))
- write the transaction to the binary logs
- complete the transaction in the storage engine
- return an acknowledgment to the client
- send part of binary logs to replocas
- execute the transaction on replocas (1-3)
-
semi-sync - закомитили локально, получил ack (mysql). Доступно здесь, уже скопировано на другой конец (больше гарантий)
- prepare the transaction in the storage engine (InnoDB))
- write the transaction to the binary logs
- complete the transaction in the storage engine
- send part of binary logs to replocas
- return an acknowledgment to the client
- (+) дожидаемся, что лог доехал до какого-то слейва. Это гарантирует, что мы не потеряем данные вообще
- (+) мы не требуем, чтобы транзакция была выполнена на всех слейвах (только принята какими-то)
- (-) риск фантомных чтений. (применили транзакцию на мастере -> отправляем на слейвы но не получаем подтверждения ни от одного -> чтения с мастера возвращают данные, которых в кластере еще не должно быть)
-
lose-less semi-sync - (шаг отправки бин-лга выполняется раньше, чем применение транзакции в движке на мастере) (mysql в facebook)
- prepare the transaction in the storage engine (InnoDB))
- write the transaction to the binary logs
- send part of binary logs to replocas
- complete the transaction in the storage engine
- return an acknowledgment to the client
- (+) перед тем, как данные появится в системе(кластере), мы увеличиваем гарантии того, что транзакция будет зафиксирована
проблема асинхронной репликации
1 отставание репликации (на replication lag порядка 0.5-1 с в норм случае)
(рекомендации):
- убивать медленные запросы (скриптом либо настройками как в posgres)
- держать отдельную реплику только для медленных запросов
- для тяжелых аналитических запросов, которые будут отставать больше 1с
- думать о кросс-СУБД репликации () - для аналитических запросов юзать OLAP хранилища (для больший и тяжелых
селектов). Для коротких запросов с ACID свойствами юзать OLTP-системы
- настроить выгрузку демоном из OLTP в OLAP хранилища
- стараться избегать паттерна запись-чтение (когда в рамках одного запрос делатся как запись, так и чтения - в одной транзакции). Это плохо, т.к. запись идет в мастер, а чтение со слейвов, и может оказаться, что вы записали данные, но изменений не видете (данные до слейва не доехали)
чтение своих записей (варианты решения)
- свои данные считаем с мастера (данные профиля и тд), а чужие с реплики
- читаем с мастера n секунд после записи
монотонное чтение - при чтении данных с разных реплик, данные разные (не до всех реплик изменения доезжают одновременно)
- ожидаентся, что пользователь не будет видеть пропадающие комментариии
- как вариант, можно привязать пользователя к реплике (по хежу от id % колво реплик)
согласованное префиксное чтение (репликация шардов может нарушать логический порядок при чтении сообщений)
- характерно для шардированных бд
- важно для сохранения причино-следственных связей
GTID
- global transaction identifier
- имеет формат server-id:transaction-id
- позволяет убедиться, что транзакция принадлежит только одному серверу
- позволяет убедиться, что транзакция применена только один раз в системе
- По источнику события передачи данных
- push - мастер рассылает данные репликам
- pull - реплики стягивают данные сами
- физическая и логическая (в mysql логическая, в postgress - физическая)
- логическая
- репликация mysql как пример (row based, statment based). В postgress тоже есть с 10+ версии
- работает с кортежами данных
- не знает, как они хранятся на диске
- CPU-bound, можно параллелить по процессорам (т.к. выполнение запросов происходит на CPU)
- физическая
- оперирует страницами (byte-to-byte)
- slave = master (идентичный байт в байт) byte-to-byte identical
- postgres WAL, InnoDB Undo/RedoLog - как примеры работающих по страницами
- IO-bound, нет смысла параллелить (т.к. это работа с диском)
- По формату binlog-a (mysql)
- SBR - statment based replication
- (+) передаются сами запросы
- гоняется небольшое колво данных (update 1млн записей 1 строкой запроса, слейву будет передан только запрос)
- все запросы в логе
- (-) каждый запрос считается на каждой ноде
- UPDATE items SET enabled=1 WHERE time < UNIX_TIMESTAMP(NOW())-60 и все поломается
- RBR - row based replication (приоритетный и безопасный, но большие требования к каналу)
- передаются измененные строки данных - более безопасный, чем SBR
- бинарный формат
- (-) непонятны границы statmenta
- его трудно читать
- before/after image - (доп избыточность) mysql по дефолту пишет строчку до и после и обе записи будут
реплицированы
- full - пишутся все колонки каждой строки (даже если изменялась всего одна)
- minimal - только измененные поля
- blob - не пишутся широкие и тяжелый колонки (гибридный формат)
- пересылается огромное кол-во данных, большие требования к каналу передачи данных
- MLR - mixed log replication (все работает по умолчанию SBR, для опасных запросов - RBR)
позиционирование
- binary log position (file name + offset)
- mysql-bin.00078:44
- локальный для сервера
- обязательно разъедеться (может кто-то ручками файлик почистит)
- gtid(source_id:transaction_id) - предпочтителен и архитектурно более удачен
- gre23rfwefr43:44
- глобален, генерируется автоматом при коммите на мастере и отправляется всем slave-ам. Slave знает, какие транзакции он уже обработал, а какие нет
- беслпатная трасировка (удобно отслеживать путь транзакции в системе)
- простой slave promotion
**Фильтрация репликации **
- она есть
- можно реплицировать данные частично
- можно обогощать данные слейва
- использовать осторожно
- опции: replicate_do_db, replicate_ignore_db, replicate_to_table...
параллельная репликация
- обычно используется однопоточная репликация
- Mysql 5.6-5.7 можно реплицировать параллельно одни и теже таблицы/несколько БД
- опции: slave-parralel-workers, slave-parallel-type(DATABASE|LOGICAL_CLOCK)
- ОСТОРОЖНО необходимо применять, только если есть понимание, что это решает проблему (больше воркеров не всегда лучше)
- плагин начиная с версии мускул 5.7 mysqlhighavailability
- по сути - синхронная репликация (силами плагина)
- нет концепта master-slave, скорее master-master
- репликация между всеми нодами
- sinfle primary (по умолчанию)
- кворум, умеет automatic failover
- flow control (когда одна из релик сильно отстала, кластер может ее выключить)
- лимит 9 нод
- автоматический conflict resolver (с использованием алгоритмов Пакоса консенсуса)
- master-hot standby
- за репликацию отвечают WAL-sender (на мастере) и WAL-receiver (на слейвах)
- это реализация физическо репликации (byte-to-byte), а не логической (как в mysql)
WAL записи - write ahead log (бинарный результат выполнения запросов)
- физические изменения страниц (сходи в блок 1435 равно 154)
- сюда попадают абсолютно все операции
- один журнал на все
- https://habr.com/ru/post/102785/
- https://ruhighload.com/Индексы+в+mysql про выбор индексов для запросов, составные индексы, кластерные индексы, explain, селективность
- https://habr.com/ru/company/postgrespro/blog/326096/ про индексы в постгре
- Визуализатор структур данных - https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
Индексы (пример с поиском главы в книге с использованием оглавления и без)
- служебная структура данных
- некий способ отображения ключа в данные
- ускоряют поиск, сортировку
- требуют доп место для хранения
- обновляются при модификации данных
Классификация:
- уникальные и нет
- простые и составные
- кластерные и некластерные (в mysql primary key - всегда класстерный)
- покрывающие и непокрывающие
- по виду (mysql): btree (на основе b+tree), rtree (для гео), hash, fulltext
Мощность (cardinality) значений поля
- low cardinality (низкая мощность) - колонка с низким кол-вом уникальных значений, малоуникальная колонка (например, тип или статус), многие строки соответствуют значению ключа
- high cardinality - сильно уникальная колонка (как правило, различные уникальные id)
Отличие работы СУБД при поиске с использованием Простых и Кластерных индексов:
- обычный индекс - обычно сначала идет проход по фалу индекса, получаем pk ключи и по этим первичным ключам лезим на диск (данные по разным pk могут находится на разных страницах на диске)
- кластерный индекс - при поиске по первичному ключу (составному или нет), данные будут доставаться с одних страниц на жестком диске (с большой долей вероятности) и осуществится меньшее кол-во дисковых операций (такая оптимизация может сильно увеличить производительность)
- todo использовать часть кластерного индекса для оптимизации выборки
Общие советы:
- проверять индексы:
- удалить ненужные (какие-то из них могут неиспользоваться и только занимают место на диски и время на их обновление)
- пересмотреть текущие (изменить порядок колонок в индексе, добавить или удалить поле в индексе)
- кластерные индексы - самые быстрые
-
бинарное дерево поиска
- не больше двух потомков
- левые меньше, правые больше
- Плюсы:
- гарантированный
- понятный поиск (в лучшем случае log N)
- Минусы: требует доп усилий на балансировку
-
сбалансированное дерево - разница высот между левым и правым поддеревом не больше 1
- Плюсы:
- решает проблемы вырожденного случая бинарного дерева
- поиск за O(h), где h - высота дерева
- Минусы: в вырожденном случае сложность в худшем случае O(n)
- Примеры:
- красно черное дерево
- фрактальное дерево
- Плюсы:
-
B tree - самобалансирующееся сильноветвистое дерево поиска
- в узле хранятся много значений (а не одно).
- t - минимальная степень (некая характеристика)
- минимум t-1, не более 2t-1 ключей (кроме корневого)
- небольшая высота дерева
- алгоритм аналогичен бинарному, но дальнейший выбор не из 2х, а из нескольких значений в узле
- поиск за O(tlog(tn)))
- обращений к диску О(logt(n))
- Плюсы:
- Помогает меньше ходить на диск за данными узлов и переберать их в ОЗУ
- макс мы сходим на диск h - раз (равного высоте дерева)
- Минусы: в вырожденном случае сложность в худшем случае O(n)
-
B+tree - ключи (указатели) на элементы на диске хранятся в листьях дерева
- все теже характеристики, что и у B-tree
- все ключи образуют связный список элементов (+ хорошо подходят для поиску по диапазона)
- Плюсы:
- Можно вставить больше ключей в узлы
- макс мы сходим на диск h - раз (равного высоте дерева)
- Минусы: для поиска ссылки на элемент на диски придется спуститься по дереву (но оно не большое 3+)
- btree (на основе b+tree в mysql , фрактальное дерево и др реализаций)
- решает класс задач: поиск, поиск по открытому/закрытому диапазону, по префиксу
- rtree - деревья, ищут координаты (задачи: найти аптеки в радиусе 100м)
- hash
- fulltext - для задач полнотекстового поиска
Помогает с поиском по:
- равенству (a = 5)
- открытому диапазону (a < 3 or a > 5)
- закрытому диапазону (3 < a < 5)
Не поможет с поиском:
- четных чисел
- суффикса (но может помочь с префиксом)
- https://www.youtube.com/watch?v=-vu4EbHZ1wY
- про мониторинг, замер производительности, анализа медленных запросов, внедрение решений
- https://www.youtube.com/watch?v=AscIBgmgeow&feature=emb_logo про индексы в мускуле
-
MyISAM поддерживает сжатие таблиц в отличии от InnoDB.
-
MyISAM имеет встроенные полнотекстный поиск в отличии от InnoDB (InnoDB с 5.6 есть фулл текст индексы)
-
InnoDB поддерживает транзакции в отличии от MyISAM.
-
InnoDB поддерживает блокировки уровня строки (MyISAM - только уровня таблицы).
-
InnoDB поддерживает ограничения внешних ключей (MyISAM - нет).
-
InnoDB более надежна при больших объемах данных.
-
InnoDB в теории немного быстрее.
-
https://ruhighload.com/%D0%A1%D1%80%D0%B0%D0%B2%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5+innodb+%D0%B8+myisam
Explain, SHOW STATUS, SHOW PROFILE, slow_log
- EXPLAIN [FORMAT = JSON]
- SHOW STATUS https://dev.mysql.com/doc/refman/5.7/en/show-status.html
- SHOW PROFILE https://ruhighload.com/%d0%9a%d0%b0%d0%ba+%d0%b8%d1%81%d0%bf%d0%be%d0%bb%d1%8c%d0%b7%d0%be%d0%b2%d0%b0 %d1%82%d1%8c+show+profile+%d0%b2+mysql%3f про SHOW PROFILE - анализ использования ресурсов запросом
https://habr.com/ru/post/70435/ полезная инфа про show profile
-
https://sql-academy.org/ (удобный, неплохой)
-
https://sql-ex.ru/learn_exercises.php (один из самых известных в СНГ) + http://www.sql-tutorial.ru/ru/content.html с теорией
-
https://sqlbolt.com/lesson/ небольшой интерактивный туториал
-
https://www.w3schools.com/sql/sql_examples.asp (список операторов с объяснениями, примерами)
-
https://en.wikibooks.org/wiki/SQL_Exercises/The_warehouse примеры sql запросов на таблице хранилище и боксы
-
http://sqlfiddle.com/#!9/d5679f/1 удобный составитель запросов (с предварительным create table и insert)
-
https://www.internet-technologies.ru/articles/mysql-distinct.html про Distinct и его сходство с group by (distinct + order by = group by)
Использовать мин 3НФ для обеспечения согласованности данных и отсутствия аномалий. Если это не проблема, а есть проблема в производительности - то денормализируем.
- https://habr.com/ru/post/254773/ ву- https://office-menu.ru/uroki-sql/51-normalizatsiya-bazy-dannykh
- https://ru.wikipedia.org/wiki/Нормальная_форма#Первая_нормальная_форма_(1NF)
Примеры проектирований бд:
- http://rema44.ru/resurs/study/dbmat/prj2.pdf
- https://publications.hse.ru/mirror/pubs/share/direct/212747315.pdf
- https://habr.com/ru/company/mailru/blog/266811/ Как работает РБД
Основные проблемы при работе БД:
- отказоустойчивость (ПО, аппаратное, разрывы сети)
- доступ к данным в конкурентной среде (клиенты с БД работают параллельно, могут перезаписывать друг друга, могут читать частично обновленные состояния)
Один из механизмов (инструментов уровня БД) решения проблем race conditions при конкурентном доступе к РБД являются транзакции и уровни их изоляции.
Транза́кция (англ. transaction)
- единая последовательность операций с базой данных, которая представляет собой логическую единицу работы с данными, имея возможность отката и подтверждения операций.
- способ группировки приложением нескольких операций записи и чтения в одну логическую единицу.
Транзакция может быть
- выполнена либо целиком и успешно (с фиксацией изменений, соблюдая целостность данных и независимо от параллельно идущих других транзакций,
- либо не выполнена вообще (с прерыванием и откатом), и тогда она не должна произвести никакого эффекта.
Транзакции обрабатываются транзакционными системами, в процессе работы которых создаётся история транзакций.
Транзакции - служат для обеспечения доступа к данным в конкурентной среде, тем самым решая проблемы согласованных изменений, и разрешая race condition
Также, транзакции обеспечивают сохранность, отказоустойчивости данных (закомиченные данные не теряются)
Для полноценного выполнения этих задач транзакция должна обладать свойствами ACID.
- A = *atomicity (атомарность) - одно из главных
- C = consistency (консистентность/целостность/согласованность) - маркетинговое свойство (для улучшения акронима ACID)
- I = *isolation (изоляция) - самое главное и тяжелое свойство
- D = durability (надежность) - полностью обеспечить невозможно
Atomicity
Атомарность означает "все или ничего". Гарантирует, что либо все операции транзакции будут успешно применены, либо не будет применена ни одна из них.
Благодаря этому появляется возможность повторять прерванные транзакции, не опасаять что часть операций уже была выполнена.
Цель: обеспечение отказоустойчивости. При отказе мы просто откатимся и повторим всю транзакции целиком заного
Consistency (согласованность/целостность) - до и после транзакции выполняются constraints. Транзакция не нарушает консистентность данных
Определяются инварианты системы
- дебит должен сходится с кредитом
- цена проданных билетов должна соответствовать выручке за сеанс
В реальности означает то, что constraints будут выполнятся.
НО по сути, Инвариант системы - это бизнес логика приложения и его поддержанием должно заниматься само приложение.
Constraints в БД - подстраховка от каких-то очевидных ошибок
Isolation - свойство, которое позволяет выполнять параллельные транзакции как последовательные
Цель: решение race condition (проблема параллельного доступа)
Существует 4 уровня изоляции, которые дают различные гарантии.
В полном смысле изоляцию обеспечивает только уровень SERIALIZABLE (выполнять поочереди)
Durability означает - данные при отдачи ответа зафиксированы в энергонезависимой памяти (fsync).
Полностью это свойство обеспечить невозможно. Диски сбоят. Но можно повышать (репликация) гарантии.
Если БД работает через ОС, то ОК от БД означает, что выполнился системный вызов fsync. После этого данные попадают в контроллер жеского диска, в котором может быть энергозависимый буфер. Т.е. есть небольшое окно между успешным коммитом транзакции "ОК" и попаданием данных на ЖД.
По этой причине, некоторые БД (oracle) работают с памятью напрямую.
Подробнее:
- https://habr.com/ru/company/otus/blog/494652/ про acid
- https://ru.wikipedia.org/wiki/ACID
- https://habr.com/ru/post/208400/
- https://habr.com/ru/company/mailru/blog/266811/
Isolation - главное (и самое труднодостегаемое) свойство транзакций.
- Serializable (самый строгий) - последовательное выполнение
- Repeatable read - при повторном чтении данных транзакцией - данные не меняются
- Read committed - транзакцие может читать закомиченные данные в другой транзакции
- Read uncommitted (самый слабый) - транзакция видит незакомиченные данные другой транзакцией. В постгре нету
Подробнее:
- https://habr.com/ru/post/469415/ - уровни изолированности в Mysql
- https://ru.wikipedia.org/wiki/Уровень_изолированности_транзакций
- https://en.wikipedia.org/wiki/Isolation_(database_systems)
- https://habr.com/ru/company/otus/blog/501294/ возможные аномалии при ослаблении уровней изоляции
- https://habr.com/ru/post/121858/ про конкурентный доступ к РБД
Алгоритм выбора уровня изоляции:
Сначала надо выбрать минимальный уровень изоляции, который гарантирует корректность поведения в данной транзакции (включая корректность данных с одной стороны и отсутствие взаимоблокировок с другой). Замечание про "в данной транзакции" весьма важное. Обычно это решение достаточно легко следует из того, что вы делаете в транзакции. Например:
- Вывод списка в грид/на форму — обычно read committed, изредка read uncommitted (только в некоторых СУБД имеет смысл, в частности в старых версиях MS SQL, или если не используете RCSI)
- Отчёты/формы, состоящие из одного запроса с соединениями/объединениями (join или union) — read committed
- Отчёты/формы, состоящие из нескольких последовательных запросов или использующие временные таблицы — надо оценить, насколько тут нужен repeatable read (и действия должны быть в одной транзакции, иначе repeatable read не имеет смысла). Если не нужен, то read committed, если нужен, то repeatable read.
- Пишущие транзакции (с проверкой условий/остатков, из нескольких запросов) — используйте по умолчанию не ниже repeatable read.
- В MS SQL лучше (с точки зрения корректности данных и отсутствия взаимоблокировок) в пишущей транзакции для тех таблиц, которые меняются в данной транзакции использовать как можно раньше serializable. В других СУБД в зависимости от того, можете ли нарваться на фантомы.
Важно, что всегда сначала выбираем минимальный уровень, обеспечивающий корректную работу, и только потом смотрим, как это можно улучшить.
После того, как определились с базовым уровнем изоляции начинаем смотреть (правильность, производительность одного потока, производительность параллельных соединений с сервером, взаимоблокировки и блокировки/race condition горячих мест) и искать компромиссы. Тут уже парой абзацев не отделаться (статья превратится в книгу) — ситуаций и компромиссов даже в одной СУБД, даже типовых быстро становятся десятки. Цикл типичный: сбор информации, гипотеза, проверка, изменение, проверка применимости, внедрение и так по кругу.
Способы обеспечения изоляции транзакций:
-
https://habr.com/ru/company/otus/blog/504190/ MVCC (optimistic СС)- оптимистичный подход
- https://habr.com/ru/post/208400/ (acid + mvcc в postgress)
-
https://habr.com/ru/company/otus/blog/506072/ locking - пессимистичный подход
-
комбинированный подход (Частично-оптимистический как в Mysql: блокировки + журнал транзакций + undo log)
-
https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html дока mysql
-
https://habr.com/ru/company/mailru/blog/266811/ тоже есть про изоляции, блокировки и т.д.
todo
todo (изучить надосуге)
- https://skillsmatter.com/skillscasts/9507-dddx-bytes
- https://github.com/mariuszgil/awesome-eventstorming
- https://habr.com/ru/company/oleg-bunin/blog/500506/
-
https://linuxgeeks.ru/bash-1.htm, https://linuxgeeks.ru/bash-2.htm примеры использования
-
jlevy/the-art-of-command-line - The Art of Command Line -
- https://githowto.com/ru/thank_you - интерактивный курс для начинающих
- https://habr.com/ru/post/106912/ Удачная модель ветвления для Git
- https://git-scm.com/book/ru/v2 книжка
Авторизация vs аутентификация https://safe-surf.ru/users-of/article/643444/
- Идентификация — процесс распознавания пользователя по его идентификатору.
- Аутентификация — процедура проверки подлинности, доказательство что пользователь именно тот, за кого себя выдает.
- Авторизация — предоставление определённых прав.
Чуть подробнее
- Идентификация (от латинского identifico — отождествлять): присвоение субъектам и объектам идентификатора и / или сравнение идентификатора с перечнем присвоенных идентификаторов. Например, представление человека по имени отчеству - это идентификация.
- Аутентификация (от греческого: αυθεντικός ; реальный или подлинный): подтверждение подлинности чего-либо или кого либо. Например, предъявление паспорта - это подтверждение подлинности заявленного имени отчества.
- Авторизация является функцией определения прав доступа к ресурсам и управления этим доступом. Авторизация — это не то же самое что идентификация и аутентификация: идентификация — это называние лицом себя системе; аутентификация — это установление соответствия лица названному им идентификатору; а авторизация — предоставление этому лицу возможностей в соответствие с положенными ему правами или проверка наличия прав при попытке выполнить какое-либо действие. Например, авторизацией являются лицензии на осуществление определённой деятельности.
Верификация и валидация
TODO








