В чем конечная цель программирования?
В том, чтобы иметь код, который легко изменять.
Это простое предложение, и, возможно, даже оно не вызовет большое количество споров. Потому что очевидно, что полезно иметь код, который легко изменять. Даже если вы не согласны с тем, что это конечная цель программирования, вы все же, скорее всего, согласитесь с полезностью выражения в целом.
Давайте разберемся. Какую проблему мы пытаемся решить?
Мне кажется, что это фундаментальная проблема программистов. Дело в том, что мы не понимаем, либо же имеем ложные представления о том, зачем мы вообще программируем.
К примеру, когда сварщики варят какую-то конструкцию на стройке, они знают, что эта конструкция необходима для того, чтобы был построен дом. Используют сварку, потому что очень важно, чтобы дом был крепким. Чтобы не было крушения дома. Чтобы люди не погибли при крушении. Это жизненно важно.
Возможно, это покажется слишком очевидным. Возможно, это звучит так, будто я описываю то, что вода мокрая, а небо голубое. Но спросите себя: о чем думают программисты, когда пишут код? Какова конечная цель их деятельности?
«Сделать продукт» — это ответ, который я получал в львином большинстве случаев.
Проблема заключается в том, что само понятие «продукт» постоянно меняется. Вы слышали когда-нибудь такое предложение: «В программировании постоянно только одно: всё всегда изменяется». Точно так же, как ни один бизнес план не выдерживал первого столкновения с потребителем, все проекты всегда изменяются по мере разработки. Появляются новые требования. Баги и ошибки заставляют нас дорабатывать и перерабатывать.
Именно поэтому «сделать продукт» — это крайне неверный способ смотреть на программирование. Этот подход подразумевает, что есть четкий план. Что все расписано и спланировано так, что планы не сорвутся и не изменятся. Что есть требования, которые не поменяются. Что не случится ошибок и багов.
Также это видение не подразумевает того, что понятие «продукт» не только не четкое, не только не изменяется, но и постоянно расширяется. У приложения появляются новые функции и варианты использования, появляются новые сервисы. Бизнес всегда будет пытаться удержать текущих пользователей расширением функционала и улучшением сервиса. Или просто изменяться и адаптироваться под новые рынки.
Я не хочу вас запутать этими рассуждениями. Мысль здесь очень проста: не существует такого понятия как «законченный продукт» в программировании. Наша работа не бывает закончена. Мы можем долго поддерживать текущий продукт, нас могут перебросить на другой или же нас могут попросту уволить. Например, потому что закончились деньги. Но ситуация, когда продукт построен и закончен, случается крайне редко. Наверняка есть исключения из этого правила, но прошу вас обратить внимание, что это именно исключения, подтверждающие правило.
Что я хочу этим сказать и к чему я веду?
Приведу пример из практики. Потому что он поможет применять на практике эти знания и вам.
Недавно я занимался обучением нового сотрудника. Ему была дана задача:
в мобильном приложении есть список. В списке отображается лишь один элемент. Остальные элементы списка скрыты.
Если нажать «показать весь список», то в списке вместо одного элемента будут отображаться все. Например, 10.
Необходимо внести изменение. Нужно, чтобы в самом начале отображался не один элемент списка, а три. То есть заходишь на страницу, видишь три элемента в списке, жмешь кнопку «показать весь список» и показывается уже 10 элементов.
Разработчик подошел к этому вопросу так же, как и, возможно, многие: он написал код так, чтобы список мог принимать аргумент. То есть можно сказать списку, сколько мы хотим элементов в списке, до того как кнопка нажата. Рассуждал он об этом просто: «Я хочу, чтобы у меня с этим списком в будущем больше не было проблем. Если когда‑нибудь надо будет использовать этот список где‑либо еще или использовать несколько списков с разным количеством начальных элементов, то нужно будет просто указать правильный аргумент. Больше никогда не будет проблем с этим элементом». Таким образом он становится future proof.
Это совершенно логично. Это имеет смысл. И это очевидно полезно.
Но когда разработчик спросил меня о том, что я думаю об этом решении, то на удивление оно заставило меня задуматься. В программировании есть много различных принципов. Есть один, который применяется очень редко и зачастую даже вызывает споры: KISS (Keep It Simple Stupid).
То есть для того чтобы взять для отображения лишь один элемент, у нас была вот такая строка:
list.sublist(0, 1)
Чтобы отображать любое количество элементов, мы собрались сделать вот такую строку:
list.sublist(0, amountOfInitialItemsToDisplay)
Казалось бы, изменение не критическое. Но при ближайшем рассмотрении оказалось, что сложность нашего кода резко возрастает. Если ранее у нас был лишь один тест, который имел очень простое описание «если кнопку не нажимали, удостоверься, что отображается лишь один элемент», то теперь нам нужно несколько тестов:
«если кнопку не нажимали и если сказано отобразить лишь один начальный элемент то удостоверься, что отображается лишь один элемент»
«если кнопку не нажимали и если сказано отобразить 3 элемента, то удостоверься, что отображается три элемента»
«если кнопку не нажимали и НЕ сказано что-либо отображать (аргумент отсутствует) то отобрази дефолтное значение — три элемента»
«если кнопку не нажимали и если сказано отобразить 0 элементов, то удостоверься что отображается 0 элементов»
«если кнопку не нажимали и если было дано негативное число, то кинь ошибку»
«если кнопку не нажимали и если было дано число, которое превышает количество элементов списка, то кинь ошибку»
Возможно, последние несколько тестов вам покажутся излишними, и эти ситуации высосаны из пальца. Действительно, я согласен с вами, это так. Но ключевая мысль заключается в том, что раньше у нас таких потенциальных ситуаций, таких проблем и стольких тестов не было. А теперь из-за небольшого изменения логики наш код стал более сложным, у нас больше кода и тестов, которые нужно поддерживать, и разбираться в этом стало труднее.
С обратной же стороны медали у нас есть размышления о том, что мы действительно можем в будущем список изменить (если мы меняем его сейчас, то кто сказал, что мы не изменим его в будущем?), список может иметь разный вид на разных страницах и действительно, он ощущается как не future proof.
Так что же выбрать?
В тот момент я спросил себя: в чем конечная цель программирования? Конечная цель программирования в том, чтобы иметь код, который легко изменять. Если посмотреть на проблему с этой точки зрения, то ответ становится очевиден.
Нужно поменять это:
list.sublist(0, 1)
На это:
list.sublist(0, 3)
Затем обновить один тест, и работа закончена.
Это способ, при котором добавлено наименьшее количество сложности. Это способ, при котором, если нас попросят изменить как-либо этот список, у нас будет наименьшее количество проблем.
Посудите сами: если бы у нас было динамическое количество элементов, то было бы больше логики, больше проверок, больше тестов и больше кода в целом.
То есть когда понадобится изменить этот список (а этот момент обязательно настанет, потому что это аксиома) эта бОльшая сложность нам бы мешала. Пришлось бы её учитывать, писать новые тесты и проверки. Получается, что сложность ведет к бОльшей сложности, и она даже может множиться в геометрической прогрессии.
В конечном итоге несмотря на то, что эти рассуждения очень логичны, они всё же казались мне чем-то неправильным. Словно незаконным. Неужели я учил все различные принципы и получал опыт, чтобы в итоге просто менять один номерок на другой? А как же SOLID? А как же DRY? А может быть, какой-нибудь паттерн программирования использовать? А может быть, извлечь логику создания списка? А может быть, сделать два класса вместо одного? А может, через наследственность как-нибудь? А может, что-нибудь ещё?
В моем конкретном примере все очень просто, и может казаться, будто эти размышления притянуты за уши. Но если отвлечься от специфики моего примера, то станет ясно, что именно так мы зачастую и размышляем. У нас есть свои знания, опыт, идеи и переживания. И очень скоро, если копнуть глубже, становится понятно, что решения мы принимаем в зависимости от множества факторов. Кто-то пишет код так, чтобы перестать переживать о будущем. Кому-то нравится использовать проверенные техники. Кому-то хочется как угодно побыстрее закрыть задачу. Кто-то просто не знает, что делает.
И ни одно из этих решений не делается с позиции здравого смысла. Потому что самое здравое, что мы можем ежедневно делать — это писать код так, чтобы его легко было изменить. Потому что львиную долю своего времени как программисты мы занимаемся тем, что как-то изменяем свой код.
В заключение хочу сказать, что, на мой взгляд, все принципы и лучшие практики, что мы применяем, на самом деле сводятся к мысли, написанной в начале статьи.
DRY == не дублируйте код, пишите так, чтобы изменения не пришлось вносить во множестве мест вместо одного.
Архитектура == правильно делите проект на слои, чтобы изменения в одном слое не затрагивали другие == пишите код так, чтобы его легко можно было менять.
SOLID == пишите код так, чтобы при добавлении функционала мы добавляли классы, а не меняли текущие == пишите классы так, чтобы в проект легко можно было вносить изменения.
Чистый код == пишите код так, чтобы его легко было прочесть и разобраться, что он делает. Чтобы легко можно было внести изменения.
И так далее.
Поэтому каждый раз, когда я задаюсь вопросами о том, как решить какую-либо проблему в коде, я просто спрашиваю себя: «В чем конечная цель программирования?». А затем придумываю способ, при котором код будет легче всего изменить в будущем. Это и вам советую.