#ооп #проектирование #solid
В теории все понятно, а на практике постоянно затруднения. Связаны они с тем, что не понятно в каком масштабе должна рассматриваться эта самая "Единственная обязанность". Вот нужно мне работать с базой данных. Если я создам класс DataBaseInteracting можно считать что у него будет одна обязанность? Но ведь тогда он и записывает данные в базу и читает их оттуда и обновляет. Может лучше создать 3 класса: DataBaseWriter, DataBaseReader и DataBaseUpdater?
Ответы
Ответ 1
Принцип единой обязанности действует практически для любого масштаба: метод, класс, модуль, подсистема и т.д. При выборе обязанности, как и всегда, должна быть некоторая разумность. Более того, от всех принципов иногда можно отступать. Это придет с опытом. В вашем примере я бы сказал, что необходимости в разбиении класса DataBaseInteracting согласно действиям нет. Обязанность "обеспечение хранения данных" достаточно атомарна, на мой взляд, и делить ее на чтение/запись/обновление не нужно. Однако имеет смысл поделить в других плоскостях. Например, если в этом классе есть код построения SQL запросов, то он должен быть вынесен в отдельный класс. Более того, если нужна поддержка нескольких БД, то таких классов должно быть несколько. Т.о. если возникнет потребность какого-то высокоуровневого изменения в "обеспечении хранения данных", вы будете менять класс DataBaseInteracting. Если обнаружится баг с построением неоптимального запроса, вы будете менять класс, который строит запросы и даже не будете трогать DataBaseInteracting. Если вам нужно будет поддержать новую БД, вы просто добавите новый класс. Понимаете? При этом все эти классы должны лежать в проекте DAL. И в этом проекте должно лежать только то, что относится к логике взаимодействия с хранилищем (БД в вашем случае). Так вы поддержите SRP на уровне проекта.Ответ 2
Так называемый "Принцип единственной ответственности" (SRP) на самом деле не имеет никакого отношения к количеству фич, которые реализует сущность. Принципы SOLID направлены на то, чтобы решать конкретные проблемы, возникающие при работе с кодом, и в случае SRP - это проблема множественных изменений кода при изменении технического задания (ТЗ). Если код, отвечающий за выполнение пункта ТЗ разбросан по многим исходникам, то при изменении этого пункта ТЗ, придется править исходники во многих местах. По этому SRP рекомендует нам собирать весь код, отвечающий за тот или иной пункт ТЗ в одном месте. Чтобы когда ТЗ поменяется, надо было бы править только одно место в коде (один модуль, или один файл, или один класс, или одну функцию, или одну строчку). Из этого следует, что если несколько пунктов ТЗ можно сделать одним куском кода - то это нормально, а вот обратная ситуация - когда на один пункт ТЗ надо много кусков кода - это уже плохо. Ответственность размазывается по исходинкам, SRP не соблюдается, коллеги плачут от коммитов по 40 файлов. По этому если DataBaseInteracting - это один небольшой класс, то с точки зрения SRP - нет никакого смысла разбивать его на DataBaseWriter, DataBaseReader и DataBaseUpdater. Другое дело, что есть другие принципы, например OCP (замена одних кусков кода на другие), или проблема изоляции кусков кода при тестировании.Ответ 3
Проблема возникает из-за того, что крупные системы состоят из нескольких слоёв, и уровень детализации на каждом слое различается. Поэтому единственную обязанность логично определять для степени детализации характерной для слоя. Поскольку класс DatabaseInteracting кажется мне слишком абстрактным, предложу вместо него рассмотреть известный паттерн Repository (Хранилище). Есть несколько его модификаций, и чтобы не путаться, давайте за основу возьмём модификацию, предложенную Эвансом в книге по DDD. Эванс пишет, что Хранилище обеспечивает долговременное хранение объектов доменной области — это его ответственность. Она реализуется посредством операций чтения, обновления и удаления (сюда же можно присовокупить и создание, но это отличается от канона DDD). Перейдя на уровень ниже (детально рассматривая операции) мы понимаем, что для ускорения работы с базой можно применить модель master-slave, где одна БД считается главной, и ещё несколько — подчинёнными. Подчинённые БД постоянно синхронизируют своё содержание с главной БД, так что на всех серверах одни и те же данные. Запись всегда происходит в главную БД, а читать можно из любой. При большом количестве чтений такая организация приложения даёт существенный прирост в скорости. На этом уровне детализации мы понимаем, что наши подключения к серверу БД будут двух видов: для чтения и для записи-чтения. Соответственно, операция чтения будет создавать подключение первого вида, а операции обновления и удаления — второго. У вас появятся классы ReadOnlyConnection и ReadWriteConnection, но на уровне предметной логики вы будете оперировать только Хранилищем. По своему опыту могу сказать, что причиной неясного кода очень часто является смешение уровней детализации. Здесь должны помочь групповые обсуждения кода или прицельная работа ведущих программистов.Ответ 4
Во-первых, этот принцип очень спорный и потому почти не применим на практике. Почти никогда нет ТЗ, по которому пишется программный продукт. А если подробное ТЗ есть, то оно уже не меняется. Кроме того, совершенно очевидно, что для любого ТЗ можно легко придумать такую правку, из-за которой придется радикально переписывать программу. Ведь разные части программы опираются на одни и те же идеи из ТЗ, которые могут измениться. Во-вторых этот принцип противоречит принципу KISS (keep it simple stupid / чем проще, тем лучше), одному из немногих, который действительно работает при дизайне системм. Если вы разбиваете объект на части, то усложняете программу, вместо одного класса у вас получается 2, 3, 4 и больше. В результате, зачастую, если исходную программу можно было легко понять, то "улучшенную" уже сложно, т.к. в ней много объектов и они все не помещаются в сознание. тут подробнее
Комментариев нет:
Отправить комментарий