Комментарии 20
предпочтение простарнству имен
устаявщийся набор таких тегов
не путать с обчыными числами.
"503. Внутрення ошибка,
Исправьте пожалуйста.
Вы уже написали свои первые 1000 строк кода и сейчас хотите сделать их понятнее, потому что внесение изменений занимает столько-же времени, сколько написать заново, но советы из ООП, SOLID, clean architecture и т.д. непонятны вам.
Если вы написали 1000 строк, думать о том как сделать код легко сопровождаемым рано. Я вот написал наверно не менее пол миллиона и до сих пор не могу понять как ООП и SOLID может мне помочь справиться со сложностью по сравнению с процедурным слилем и god-объектами :). Или могу. Пока не уверен.
По мне, здесь нужно разделять цели.
Первая цель - научиться писать работающий код больше чем с двумя условиями и не тонуть в нем.
Вторая цель - научиться писать код, который выглядит профессионально. То что он будет выглядеть профессионально не означает, что он будет проще. Это будет просто обезьянье подражание профессионализму, но без этого видимого профессионализма и видимой стандартности решений не возьмут на работу.
Третья цель - а как действительно написать простой понятный код для сложных проблем? И тут рецепта как такового нет. Наверно каждый расскажет свой некий путь.
Самая сложная цель - третья.
Начать лучше с того, что простейшим путем борьбы со сложностью является то что раньше называли "структурное программирование", когда общая функция решает задачу и затем она вызывает под-функции, решающие частные задачи и так далее. В самом низу находятся функции, уже работающие непосредственно с данными.
Научившись применять этот стиль мышления, можно двигаться дальше.
При этом есть два направления - от структурного к чисто функциональному стилю и к ООП стилю.
Функциональный стиль начинается с того, что мы перестаем пользоваться глобальными данными по возможности и стараемся писать чистые функции. Это хорошо работает когда нужно реализовать по настоящему сложный алгоритм.
ООП стиль начинается с того, что некая одна функция выполняясь запускает внутри себя другие функции, которые имеют дело с одними и теми же данными. В итоге мы имеем слишком много передаваемых данных и соблазн замкнуть эти алгоримты в некий небольшой изолированный модуль, к котором эти данные будут доступны функциям глобально (объект).
Здесь можно создать объект. При этом по мне, важно помнить, что мы создаем объект как "элемент поведения", а не как данные о неком доменном объекте оснащенные поведением. Второй путь хорош для UI фреймворков (кнопка-объект), может быть для игр, но создает очень много сложностей в других случаях (ИМХО).
Такой объект может создаваться операцией new, отрабатывать и тут же удаляться. Это нормально. В этом случае он мало отличается от функции.
Или объект может быть постоянно существующим в памяти и реагировать на обращения, например как кнопка в UI (правда если это кнопка не в React, где она создается при каждом обновлении экрана по новой).
Дальше, если хочется, то можно попробовать оснасить структуры данных, описывающие доменные объекты поведением. Т.е. ввести не просто объекты как элементы поведения, но объекты соответствующие доменным объектам. Попробуйте, но у меня не разу это ничего не упростило. Тут можно дискутировать.
Описанного выше пройденного пути уже достаточно, чтобы написать в одиночку программу на десятки тыщ строк кода и не увязнуть.
Но что если мы пишем код все вместе большими командами?
В мозгу у человека есть некое интуитивное ощущение универсальности. Если я описываю неккие классы и функции, что я делаю? Решаю конкретную задачи или пишу фреймворк всех времен и народов для решения всего класса подобных задач.
Первое, что нужно уметь, это всегда решая конкретную задачу чуть-чуть писать фреймворк для решения подобных задач, но без фанатизма. Это делает систему более пластичной к будущим изменениям и даже может сделать ее понятнее, если соблюдать баланс.
Второе - это тесты. Когда вы написали программу подумайте, можно ли ее разобрать на части , каждую часть поставить на стенд и испытать абстрактно, выполняет ли она ту логику для которой написано? Если нельзя, это все еще плохой код. Если уже можно, значит теперь мы победили сложность окончательно.
Дальше можно уже подумать про SOLID.
Допустим, мы запихнули в один объект и некую абстрактную логику и обращение к конкретному девайсу. Т.е. нарушили "S". Как теперь ее тестировать? Стенд для испытания этого объекта станет очень сложным и специфичным. А еще непонятным. Делим это на две части и все становится проще. Здесь необходимость тестировать помогает нам осознать важность S.
Допустим, при малейшем изменении задачи нам придется лезть в кишки объекта и все там менять, вместо того чтобы добавить новое поведение не меняя объекта (буква "O")? Это значит в нашем решении было недостаточно "фреймворковости", оно было слишком частным, или это может означать что мы распилили на объекты искусственно, не в том месте как надо (что по сути близко к недостатку "фреймворковости" по сути).
Мы ставим объект на стенд для тестирования и прямо не знаем с чего начать. У него столько методов! Здесь мы нарушили букву "I". На стенде тестируемый интерфейс должен выглядеть предельно просто и понятно - зачем он и как проверить что он это делает. Клиенты у интерфейса могут быть сегодня одни а завтра другие, но если он понятно тестируется, он уже соблюдает I в достаточной мере.
Есть замечательный принцип, использованный природой - каждый ген (а ген это элемент поведения, т.е. объект) вездесущ и активируется по месту в той части программы где он нужен. Значит для изменения программы достаточно добавить до кучи новый ген или подменить существующий. Этот замечательный принцип нужно не упустить и в разработке где возможно. Это "D".
Это был мой путь. Кто прошел иной путь, делитесь.
Может у меня такое впечатление сложилось, но с N-tier 80% кода это перекладываение данных и там вообще особо не надо думать. Зато остальные 20% это именно то, где нужны правильные подходы. Когда вы готовы браться за эти 20% и после этого команда не хочет утопить вас - вы двигаетесь в правильном направлении.
Это пожалуй лучшее объяснение SOLID, что я когда либо слышал, не приведя даже не строчки кода. У вас прям талант объяснять.
Мерси, очень блгодаен. Первая статья, я в шоке.
не не, у вас есть код, это у меня нет кода. давайте ка разберемся ка про кого он...
Конфуз. А я готов и удалить свой код, раз уж такие ставки ! :)
Мне ваше объяснение нравится, но оно как бы "сверху-вниз". Понятно тем, кто через это прошел, и непонятно тем, кому предстоит. Я пытался ответить на вопрос "как сделать здесь и сейчас".
+1
Я написал не 1000 строк кода, а, наверное, уже 1000 раз по 1000 и мне сложно представить, как человек, написавший 1000 строк кода сможет хотя бы осознать большую часть рекомендаций. А когда сможет осознать, то большая часть приведённых советов начинает выглядеть спорными или узкоприменимыми.
Мой путь был слегка другим и начинался с электротехнического образования, что отложило свой отпечаток. Я также не написал миллионы строк кода, но переписывал одно и то же миллионы раз полностью с нуля. И вот мои критерии сенсея.
1) SOLID, ООП и прочие аббревиатуры — это безусловно важно, но: есть риск натягивания совы на глобус ради красивого слова, потому всегда к ним отношусь с предельной осторожностью, а чаще просто прохожу мимо. Чем хуже например Finite Universally Code Kraft? Миллион такого можно напридумать если задаться целью.
2) Чтобы миллион раз переписывать один и тот же код, он должен быть — кратким и понятным спустя много лет. Не должно возникать вопросов, зачем тут этот класс или метод и почему без него тут нельзя было бы обойтись. Так же в коде полезна классовая несправедливость — когда сразу понятно, кто тут высокуровневый аристократ, пишущий
var t = database.Execute("sp_get_report_data")
и не желающий даже знать тип возвращаемого значения, а кто простой работяга, открывающий и закрывающий 3) Goto зло, недетерминированные циклы зло, проверки на корректность введёных данных — зло в злотой степени. Если в коде написано
func(double a) {if(a<0) throw...
это плохой код. Хороший код это func(Distance a)...
где объект с отрицательным значением просто не может существовать.Поэтому идеальный код — это код без if, и даже без case. Именно в размышлениях о том, как выкинуть откуда-то if-ы и case-ы я переходил на следующий уровень квалификации.
4) new очевидно тоже зло, Stack Overflow очевидно тоже. Если кто угодно может забирать сколько угодно памяти — это даже не коммунизм, это анархия в чистом виде, а с анархией порядка не получится. Если холопу хочется денег (памяти), чтобы покушать — холоп получает не деньги, а покушать. Заодно и проблема с коррупцией и нецелевым расходом ценных ресурсов решается. Ну а за аристократами полицейские наблюдают, кто что делает тщательно записывают, а иногда даже и убивают без суда и следствия.
Статья достаточно высокого уровня
Статья неплоха, но стоит немного пополировать.
Совет 1 не очень закончен. Задать вопрос хорошо, но что с этим делать дальше.
Для разбивании я использую постоянный рефакторинг. Сначала разбить хоть как нибудь, а потом по ходу дела перенести. С каждой добавленной фичей вы начинаете больше понимать куда идёте и будет более понятно как всё организованно. Чем более изолирован и атомарен код, тем проще его переносить.
C def send_email
условием внутри категорически не соглачен. Стоит сделать интерфейс(абстрактный базовый класс) EmailSender и к нему две имплементации. Что-то типа условия в коде должно быть один раз, там где мы создаём инстанс одного или другого класса. Для остальной программы это работа с интерфейсом.
Совет 9 я не понял.
Ремарка: в архитектуре существуют метрики сложности кода, чрезмерное ветвление ухудшает показатели.
Сложность она ортоганальна архитектуре. Советы 3 и 7 как раз описывают способы борьбы с ней.
Про `def send_email` согласен, спорное решение, но я считаю это допустимым компромиссом между чистотой и простотой.
Признаться, я просто примера лучше не придумал :)
9 - можно долго раскывать, если перефразировать:
1. if
вредны, но без них никак.
2. if
должно быть поменьше в основных функциях / обработчиках.
3. Вложенные if
еще больше ухудшают чтение.
4. Много if
в одном месте - плохой признак.
if и их разновидности (switch, for, while) не вредны, вредна сложность.
Очень рекомендую именно со стороны цикломатической сложности заходить к этому вопросу. Что такое много? У каждного это своё. А вот сложность эта цифра и для всех она одинакова.
А если можно измерить, можно и автоматизировать. Где возможно я добавляю автоматизацию которая просто не пропускает сложный код.
У меня два главных способа борьбы с ней, это уже упомянутные в статье разбиение на мелкие части и ООП.
В примере ниже, мы детали реализации убрали в другое место, и переключение между реализациями убрали в другое место, то есть здесь код стал простым и понятным. И тестировать такой класс легко и приятно, просто мок сендера сделать и передать конструктор.
def __init__(self, email_sender):
self._email_sender = email_sender
def send_email(self, *args):
self._email_sender.send(*args)
Еще один важный момент на который все забивают это логические уровни, иногда их называеют домены, а на языке солида это ответственности. Есть домен бизнес логики, тут мы говорим о пользователе и есть домен адаптера для отпавки почты. Это абсолютно разные домены и они вполне независимы. Выбор каким из адаптеров отправлять почту это тоже другой домен.
Хорошая практика не смешивать логику разных урвоней в одной функции. Когда в одной строчке говоришь о дядьке, а во второй про бузину, то такой код намного сложнее читать.
Код в примере это логика пользователя. Домен отправки это реализация(и) email_sender, а код который создаёт инстанс и предаёт его в конструктор это третий кусок головоломки. Для каждой из частей причины для измениея будут свои, вы же не хотите переписывать код пльзователя если тело письма нужно будет форматировать по новому стандарту?
К сожалению главная проблема алкоголика не в том, что он не может бросить пить, а в том, что он не хочет бросать и вообще не видит в этом никакого смысла. Главная проблема г-кодера не в том, что он не знает как улучшить свой код, а в том, что он считает свой код совершенным. Если код вдруг где-то перестает работать - это проблема криворуких юзеров с их кривым железом. Или проблема тестировщиков - опять же с их кривыми руками и стендами. Если кто-то не может ничего понять в их "совершенном" коде - значит у кого-то просто недостаточно мозгов. В тот момент, когда разработчик задумывается над вопросом как бы улучшить код - он уже наполовину исцелен. Благо на тему совершенного кода написанны тонны литературы - и, кстати, очень неплохой литературы. Вот даже здесь на Хабре была не так давно статейка на тему. Суть ее можно выразить перефразируя Роберта Мартина: Код должен быть простым. Вы уже сделали свой код простым? Отлично! - теперь сделайте его еще проще!
Подавляющее число языковых средств служат как раз для снижения сложности: ООП, ФП, паттерны, прочее. Если лично для вас код с использованием какого-то приема становится сложнее, просто не используйте этот прием, пока его не подучитесь/с ним не освоитесь. Освоение — это чтение, написание и рефакторинг кода, научиться чему-то, просто читая книги или смотря видео, нельзя.
Статья из 70ых годов 20 века. Сейчас нужно писать как например вызвать метод соседнего объекта если тот ещё не создан или как распределить по файлам андроид проект.
Большое спасибо за статью!
Лично для меня актуален вопрос по архитектуре кода потому что я не понимаю как организовывать проект, как организовать код, на какие модули разделить все и по какому принципу
Если кто-то из экспертов подскажет где мне подчерпнуть информацию об этом - буду очень благодарен
Советы по архитектуре кода для начинающих