heap.tech
лаборатория велосипедов
×

Выживание на собеседование, часть 1. Принципы SOLID

23 сентября 2016
Мир устроен так, что если, кто-то придумал какую-то идею, пусть бредовую, но с крутым заголовком и подкрепленную мощным, но бредовым, исследованием - то каждый стремится поскорее скопирастить её. Одними из самых ярких представителей умелой копирастии - рекрутинг спецов. Поэтому опытный специалист, услышав вопрос типа "почему мы должны нанять именно вас ?", или задачи "почему люки круглые", "сколько нужно поездов, чтобы люди ждали поезда не более трех минут" или "сколько мячиков для гольфа уместятся в одном автобусе" выходит в окно просит перейти к более значимым вопросам. Все эти вопросы и ответы изжеваны овер9000 раз и рассматривать их смысла нет.
В этой статье я сделаю акцент на вопросе "Что такое принципы SOLID ?", он задается в 8 из 10 компаний. Но мы то знаем, что обычно на собеседованиях спрашивают то, что выучили вчера.

Что такое SOLID - Single responsibility + Open-closed + Liskov substitution + Interface segregation + Dependency inversion. Получается это самое, поехали!

Single Responsibility

(единственная обязанность)

Это просто - каждая единица логики (класс, модуль, метод ...) должна выполнять только присущую ей задачу.
Длинный пример
class Article { public int Id; public DateTime Date; public string Title; public string Body; } class ArticlesManage { public void Create(Article article); public void Remove(int id); public void Update(Article article); } class ArticlesView { public Article GetById(int id); public Article[] GetByRange(int start, int offset, int count); } class ArticlesDBContext { public void Insert(Article article); public void Update(Article article) public void Delete(Article article); public Article[] Select(entity query); }
Article - объект, представляющий собой сущность статьи на новостном сайте. Каждая статья может быть создана, изменена, удалена и просмотрена. Все эти операции лучше разделить по разным классам - большинству пользователей нужно только просматривать статью. Править могут только её авторы. И конечно вынести логику, которая преобразует объекты и работает с хранилищем данных.
Если все четыре класса совместить в один, то получиться портянка, переполненная ненужными методами.И если, нужно просто вывести статью по её уникальному id, то будет создавать огромный объект, использующий кучу памяти. Кроме этого, поддерживать длинную портянку задача тоже не простая, гораздо проще править то, что разбито на логические части - логика с базой, логика с валидацией данных, представлении объекта и т.д.

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

Open-closed

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

Принцип принцип открытости (закрытости) звучит так "программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для изменения". Если упростить и переписать на человеческий язык, то получится так - программируйте так, чтобы для изменения логики работы произвольного участка кода его не пришлось переписывать. Нужно ввести новый уровень абстракции.
Пример
На основе предыдущего примера, класс ArticlesDBContext. Допустим перешли на новый тип хранилища, например Redis (до этого была SQL-база). Это не повлечет создание новых методов, но повлечет перепиливание их внутренностей. Поэтому создадим интерфейс IDataProvider с 4 базовыми методами, которые нужны для работы со статьями.
interface IDataProvider { public void Insert(Article article); public void Update(Article article); public void Delete(Article article); public Article[] Select(entity query); } //Теперь реализуем класс для работы с Redis. class RedisDataProvider: IDataProvider { public void Insert(Article article) { //code } public void Update(Article article) { //code } public void Delete(Article article) { //code } public Article[] Select(entity query) { //code } } //И перепишем основной класс ArticlesDBContext для работы согласно принципам SOLID, а конкретно второму принципу (Open\Closed principe, OCP). class ArticlesDBContext { IDataProvider _provider; public ArticlesDBContext(IDataProvider provider ) { this._provider = provider; } public void Insert(Article article) { this._provider.Insert(article); } public void Update(Article article) { this._provider.Update(article); } public void Delete(Article article) { this._provider.Delete(article); } public Article[] Select(entity query) { return this._provider.Select(query); } }

Ничего не понятно, еще раз и своими словами плз! Принцип OCP гласит - пишите код так, чтобы для изменения работы участка кода программы (или программы в целом) - старый код не нужно было переписывать.
И чтобы добить спрашивающего знанием материала - добавляем: нужно заранее предусматривать некий слой абстракции, который, в будущем, позволит дополнять существующий код для решения новых задач без потери функциональности кода, решающеего старые задачи.

3. Liskov Substitution

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

Звучит как "Пусть q(x)является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T".
Ничоси! Это как ? Просто, если объяснить на пальцах: если у вас есть класс ArticlesDBContext, а мы возьмем и создадим класс ArticlesDBContextNative и будем наследовать его от ArticlesDBContext. Теперь изменяем все ссылки в проекте с ArticlesDBContext на ArticlesDBContextNative. Запускаем проект. Если все запустилось и работает как всегда - Лисков одобряет. Иначе - негодуэ.
Если наследуетесь от объекта - не перекрывайте ранее реализованные методы предка.

4. Interface segregation principle

(принцип разделения интерфейса)

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

Пример
Допустим в проекте, который выводит статьи на сайте есть несколько разновидностей хранилищ данных и, соответственно, несколько классов, позволяющих работать с ними. Условно разделю все операции по работе с данными как создание, чтение, изменение и удаление.
interface ICreateOperations { void Create(Article article); } interface IModifyOperations { void Edit(Article article); void ToggleVisibility(int articleId); void SetPrivate(int articleId); } interface IReadOperations { Article ReadById(int articleId); Article[] Search(string query); Article[] ReadByFilter(Filter filter); } interface IDeleteOperaions { void DeleteById(int articleId); void DeleteByFilter(Filter filter) } class ArticlesSQLDBContext: ICreateOperations, IModifyOperations, IEditOperations, IReadOperations, IDeleteOperaions { //SQL-база данных должна уметь делать все типы операций } class ArticlesRedisDBContext: ICreateOperations, IReadOperations, IDeleteOperaions { //Redis используется как кеш, без механизма синхронизации изменений. Изменение происходит физическим удалением и добавление измененной сущности }
Пример конечно абстрактный со слабыми зависимостями, но имеет право на существование.


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

5. Dependency inversion principle

(DIP, принцип инверсии зависимостей)

"Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций". Много слов, очень много. Нужно меньше, много меньше - нельзя использовать в одном классе прямой указатель на другой класс, их нужно заменить абстракциями.

Скучнейший пример
class ArticlesDBContext { public Article ReadById(int articleId) { //псевдокод ArticleEntity articleEntity = Execute("SELECT * FROM articles WHERE id = @articleId").Read<ArticleEntity>(); string htmlBody = new BBFormatter().Html(articleEntity.Body); return new Article(articleEntity.Id, articleEntity.Title, htmlBody); } } class BBFormatter { public string Html(string textWithBBtags) { //code } }

Казалось бы все прекрасно, за исключением псевдокода ), но код не удовлетворяет пятому принципу SOLID DIP (dependency invertion principle) - т.к. класс ArticlesDBContext ссылается на класс BBFormatter. Фактически ArticlesDBContext - модуль верхнего уровня, BBFormatter - модуль нижнего уровня. Согласно принципу так быть не должно, нужен слой абстракций - правильно, интерфейс. Вот как-то так
interface ITagsFormatter { public string Html(string text2format); } class ArticlesDBContext { ITagsFormatter _formatter; public ArticlesDBContext(ITagsFormatter formatter) { this._formatter = formatter; } public Article ReadById(int articleId) { //все тот-же псевдокод за исключением строчки "псевдокод" ArticleEntity articleEntity = Execute("SELECT * FROM articles WHERE id = @articleId").Read<ArticleEntity>(); string htmlBody = _formatter.Html(articleEntity.Body); return new Article(articleEntity.Id, articleEntity.Title, htmlBody); } } class BBFormatter: ITagsFormatter { public string Html(string textWithBBtags) { //code } }

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

Финалочка



All problems in computer science can be solved by another level of indirection, except of course for the problem of too many indirections.
David Wheeler

Что можно перевести как
Все проблемы программирования можно решить дополнительным слоем абстракции, кроме проблемы избыточной абстракции.
Не злоупотребляйте принципами СОЛИД, да прибудет с вами сила )

Следующий урок выживания
Упаковка и распаковка объектов в c#
 
5375
0

Оставлять комментарии могут только зарегистрированные пользователи

пока никто не оставлял комментариев