Я недоволен тем, как получилась моя предыдущая версия этого поста, и с тех пор я пересмотрел некоторые свои мнения. Итак, вот моя попытка все исправить.

Насколько дорог кусок струны?

Если вы предлагаете «лучший способ» написания программного обеспечения, будь то инженерные методы, новый инструмент, язык программирования, процесс или инфраструктура или что-то еще, вам действительно нужно сделать только одну вещь, чтобы заслужить звание лучшего. «лучший способ»: необходимо снизить общую стоимость производства программного обеспечения. Общая стоимость, конечно, включает дизайн, управление, планирование, разработку, тестирование, отладку, контроль качества, обслуживание, поддержку, расширяемость и т. д. Существуют инструменты, фреймворки и философии, направленные на снижение стоимости каждого из этих этапов.

Обычный вопрос, который я видел на Quora, связан с этим:

Зачем кому-то использовать Java, C# или Python, если простой C намного быстрее, чем все они?

Ответ заключается в том, что, хотя C может иметь немного более быстрое время работы, чем другие (для большинства приложений ваши пользователи не заметят разницы в производительности), у него ужасно медленное время разработки, а разработчики обходятся дорого.

Обратите внимание: я говорю это не для того, чтобы принизить нашу профессию. Это простая истина: разработчики стоят дорого. Однако хорошие разработчики того стоят. Одна вещь, которая делает их достойными, — это правильное повторное использование кода. Здесь я упомяну три разных вида повторного использования кода.

Повторное использование функции

Это вопрос того, как проектировать код, который вы пишете. Таким образом, это забота всех, кто пишет код. Все, кто когда-либо думал: «Хм, я устал копировать и вставлять этот блок кода повсюду. Я помещу это в общую функцию, чтобы мне больше не приходилось этого делать!» сделал это, что (надеюсь) все мы.

Мартин Фаулер придумал Правило трех, чтобы помочь нам понять, когда это уместно, хотя это, безусловно, больше эмпирическое правило, чем закон. Этот тип повторного использования кода экономит время и нервы, устраняя лежащее в основе зло: дублирование знаний в системе. Повторное использование кода берет дублированные знания и упаковывает их в единый пакет для всех, кому они нужны. Однако это может привести к двум различным проблемам: предоставление этого единственного пакета местам, которые не хотят или не нуждаются в нем, и обертывание знаний, которые не нужно было обертывать.

Парадокс выбора

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

Однако беспорядок — не самая худшая часть этой проблемы. Если извлеченная функция на самом деле является деталью реализации, которая должна быть видна только определенным другим элементам кода, то выставление ее на всеобщее обозрение не только открывает множество парализующих возможностей для будущих разработчиков (включая вас!), но и многие из этих возможностей. просто неверны и приведут к проблемам, как к ошибкам, так и к кошмарам обслуживания, если они включены в места, где их действительно не должно быть. Определенная часть этого смягчается в OO языках путем пометки таких функций как закрытых, тем самым делая их недоступными для остального кода, которому не нужен доступ к ним.

Это все прекрасно, но что происходит, когда вам действительно нужен этот метод в одном месте за пределами этого класса? Или что произойдет, если вы обнаружите, что хотите протестировать этот закрытый метод, но ваш тестовый код не может получить к нему доступ?

На самом деле это обычно означает, что нам нужно более пристально взглянуть на структуру нашего объекта и понять, почему определенные методы находятся в своих объектах. Значит, где-то, вероятно, нарушается SRP или принцип открытости-закрытости. Но такой дизайн и мысли тяжело ложатся на наши мозги и не кажутся релевантными для функции, которую мы пытаемся реализовать в данный момент, поэтому мы просто делаем закрытый метод внутренним или общедоступным, а Яму Dhoom с инкапсуляцией.

Это не повторное использование, если это просто использование

Этот XKCD действительно пришелся мне по душе, и на самом деле именно он вдохновил меня пересмотреть концепцию повторного использования кода. Конечно, именно так обычно работает разработка программного обеспечения, но точно так же не должно быть. Поясню на примере из моей собственной карьеры. Этот пример был сделан несколько лет назад, поэтому, пожалуйста, не судите меня слишком строго. Я был моложе).

Флагманский продукт моего работодателя — это промежуточное ПО, расположенное между другими системами. В нашей отрасли существует полустандартная XML-схема (которая оказывается ни стандартной, ни схемой, но это совсем другая история), которую мы должны поддерживать. Однако мы пытались изменить отраслевую схему, чтобы она больше соответствовала нашей упрощенной и простой модели. В результате объекты нашего кода согласуются с нашей моделью, и мне поручили написать транслятор XML в другой формат, который очень мало похож на наш.

Босс намекнул, что, по его мнению, было бы неплохо, если бы у нас была возможность устанавливать эти вещи декларативно, а не в процедуре. Так что, конечно, будучи относительно новым программистом и до смерти боясь сказать Нет, я воспринял это как твердое требование.

Поскольку наша объектная модель очень плоская, а другая объектная модель очень вложенная, единственным способом сделать это декларативно, насколько я мог судить, было создание атрибута, который я мог бы поместить в каждое свойство и использовать его для объявления свойства. произвольный xpath к элементу (в отличие от System.Xml.Serialization.XmlElement, который позволяет указать только одно имя элемента, а не произвольно глубокий xpath).

Конечно, сериализация была простой; существуют утилиты для чтения произвольных xpaths из XML-документов. Но десериализация была кошмаром. Крайних случаев очень много. Даже если у вас есть xpath, скажем, четырехуровневый, как вы указываете, на каком уровне уместно создать новый элемент вместо добавления к существующему? Мне приходилось отвечать на многие подобные вопросы.

Примерно через две недели я вышел из своего темного угла с новым декларативным сериализатором XML. Я был чрезвычайно горд этим.

Этот жучок до сих пор используется в производстве. Он все еще работает. Но это ужасно. Каждый раз, когда нам приходится что-то менять или чинить (что, к счастью, не часто), я боюсь этого. И из-за ужасного беспорядка с отражением и XML под капотом, весь код представляет собой такие спагетти, что я единственный, кто действительно может пройти по нему с какой-либо уверенностью. Я бы хотел, чтобы мы могли заменить его, но тот факт, что я сделал это и заставил его работать, был, вероятно, одним из моих величайших достижений в этой части моей карьеры.

Поэтому я не мог понять, почему работодатели продолжали спрашивать меня об этом в моем резюме и почему ни один из моих ответов не казался удовлетворительным, когда они спрашивали, зачем я его создал. Я думал, что все ясно: мне нужно было иметь возможность объявлять произвольные пути xpath.

Проблема с этим проектом была не в том, что я создал что-то для решения проблемы; проблема заключалась в том, что у проблемы было гораздо более простое решение, которое можно было бы сделать гораздо дешевле. Другими словами, не стройте ракету, если вам нужен только велосипед. Если бы я просто написал единственный метод, который ничего не делает, кроме логики, необходимой для моего варианта использования, вместо того, чтобы пытаться создать полноценный декларативный сериализатор XML, я бы сделал это за пару дней, а не за пару недель, и это было бы быть радостью поддерживать, а не бременем.

Я пытаюсь сказать здесь, что навязчивое повторное использование не коренится в эмпиризме. Нет необходимости строить сложные проекты, чтобы решать простые задачи. Вы не должны делать что-то расширяемое или повторно используемое, пока у вас нет доказательств того, что это будет расширяться или использоваться повторно.

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

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

Повторное использование собственной библиотеки

Это включает в себя повторное использование кода одной организацией путем совместного использования служебной библиотеки в различных приложениях. Это решение более высокого уровня, и, вероятно, оно принимается вашим старшим архитектором. Таким образом, эти библиотеки обычно имеют конкретную заявленную цель и цель, что делает очевидным, как они вписываются в вашу систему.

Повторное использование сторонней библиотеки

Это то, что сбивает людей с толку. Прекрасная вещь в этом типе повторного использования — это абсолютная мощь задач, которые вы можете выполнить, просто вызвав функцию.

Вам не нужно разбирать JSON, создавать собственный веб-сервер, реализовывать сырой сокет TCP, писать компилятор, реализовывать ORM, писать тестовый фреймворк, строить framework», конвертируйте между часовыми поясами или реализуйте свои собственные параллельные коллекции, потому что кто-то другой уже сделал эти чрезвычайно обременительные задачи за вас.

Такое повторное использование кода — замечательная и мощная вещь. Знание того, как это сделать и когда это сделать, — это сила, которая делает инженеров-программистов достойными своих денег и является частью того, что отличает программиста от инженера.

Однако есть несколько подводных камней.

На самом деле это может не делать то, что вы хотите.

Вот еще один пример из моей собственной карьеры. Мне нужно было обернуть объекты, возвращаемые из определенных функций. Проблема заключалась в том, что их было много, и я не видел удобного способа превратить шаблон в повторно используемый код. Так я открыл для себя АОП и .NET code weavers. В частности, я тяготел к MethodBoundaryAspect.Fody. Это казалось великолепным. Он был очень похож на PostSharp. Я был на крючке. Я включил его и начал кодировать.

Когда я впервые запустил свой тест, я пришел к отрезвляющему открытию: хотя он позволяет вам запускать код в начале и в конце метода, он не позволяет вам изменять значение, возвращаемое методом, что было своего рода самое главное, для чего мне это было нужно.

Упс.

Итак, я разветвил репо, изучил CIL, сделал ветку и добавил функцию. Затем я обнаружил, что он не позволяет изменять аргументы функции в обработчике OnEntry. Так что я тоже добавил это. Потом я узнал, что одна из задокументированных фич просто не реализована. Так что я тоже добавил это.

Это было непросто, но это упражнение может оказаться еще более обременительным, особенно если библиотека большая и сложная. Вы можете обнаружить, что поддерживаете чужую библиотеку только для того, чтобы ваши собственные варианты ее использования работали.

Тем не менее, обычно оно того стоит. У вас есть преимущество над любым кодом, который уже написан, работает и тестируется в этой библиотеке, и у вас есть дополнительная поддержка сообщества (пусть и небольшого) вокруг этой библиотеки. Для сложных задач это все же лучше, чем писать с нуля.

Кривая обучения

Чем сложнее задача библиотеки, тем больше времени потребуется только на то, чтобы научиться с ней работать. Это имеет смысл. У самолета гораздо более сложная работа, чем у автомобиля, и поэтому для правильной работы требуется гораздо больше тренировок. Это не означает, что библиотеку не следует использовать только потому, что ее изучение займет некоторое время, но это означает, что кривая обучения представляет собой накладные расходы, которые могут потребовать больше размышлений, чем API plug-and-play, на который мы все надеемся, когда впервые работаю с новой библиотекой.

На самом деле, чем сложнее задача, тем больше вероятность того, что библиотека того стоит, несмотря на время раскрутки.

Смысл упоминания об этом не в том, чтобы препятствовать использованию библиотек, а в том, чтобы указать на то, что стоимость обучения реальна, и, хотя разрыв в «Ценности повторного использования» действительно растет быстрее, чем кривая обучения, кривая обучения по-прежнему растет линейно. со сложностью задачи.

Если он сломается, знания о том, как его починить, отсутствуют.

Использование сторонней библиотеки — это здорово. Лучше использовать известную, проверенную в боевых условиях стандартную библиотеку. Чем более распространена библиотека, тем лучше она будет поддерживаться. И если он с открытым исходным кодом (поскольку, скажем прямо, вы не хотите тратить деньги на этот маленький компонент вашего более крупного приложения), то он, вероятно, поддерживается большим сообществом. Это равносильно тому, чтобы звонить в отдел обслуживания клиентов, когда что-то ломается.

О, да, вещи сломаются. И я могу придумать очень немногое, что может быть более разочаровывающим, чем необходимость объяснять вашему клиенту, что ваше программное обеспечение полностью сломано из-за чего-то, что вы не можете исправить, и что вы ждете от третьей стороны, о которой они никогда не слышали и никогда не осознавал, что они были обязаны.

Опять же, это не унижение сторонних библиотек; это просто стоимость, которая будет возникать время от времени, и ее следует учитывать при выборе библиотеки, от которой можно зависеть.

Резюме

Да, есть сторонние библиотеки, которые плохо поддерживаются. Да, есть несколько затрат, связанных с зависимостью от чужих библиотек, независимо от их качества. Однако, в конце концов, более крупные задачи почти наверняка оправдывают их использование.

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

Как мне определить разницу между тем, когда лучше просто сделать своими руками, а когда лучше использовать чужой руль? Как я уже говорил, все дело в общей стоимости. Если я смогу написать его с нуля и поддерживать его сам, а другие смогут научиться поддерживать его по мере необходимости за меньшую стоимость, чем обнаружение сторонней библиотеки, изучение того, как ее использовать, добавление в нее всего необходимого и добавление пакета, потом сам напишу. Это происходит с небольшими функциями и эзотерическими функциями.

Это нормально думать: «Мне нужен фрагмент кода, который делает X», но когда я начинаю придумывать способы сделать его пригодным для повторного использования и все эти различные функции, которые он может иметь, я знаю, что пришло время проверить стороннюю библиотеку, которая решает проблема для меня. Потому что есть вероятность, что кто-то уже решил эту проблему, и у меня есть выбор: создать свою собственную или просто импортировать ее и пойти на ранний обед.

Я знаю, что я выберу с этого момента.