Русский
Русский
English
Статистика
Реклама

Совершенный код

Перевод Простые советы по написанию чистого кода React-компонентов

20.04.2021 12:16:22 | Автор: admin
Автор материала, перевод которого мы публикуем сегодня, делится советами, которые помогают делать чище код React-компонентов и создавать проекты, которые масштабируются лучше, чем прежде.



Избегайте использования оператора spread при передаче свойств


Начнём с анти-паттерна, с приёма работы, которым лучше не пользоваться в тех случаях, когда для этого нет конкретных, обоснованных причин. Речь идёт о том, что следует избегать использования оператора spread ({...props}) при передаче свойств от родительских компонентов дочерним.

Передача свойств таким образом может, на самом деле, ускорить разработку. Но этот приём значительно усложняет обнаружение ошибок в коде. Разработчик начинает меньше доверять своим компонентам, это усложняет их рефакторинг, и, как результат, ошибки появляются в них раньше, чем в случаях, когда оператор spread для передачи свойств не используется.

Объявляйте функции, использующие несколько параметров, применяя объекты с параметрами


Если функция принимает несколько параметров хорошо будет поместить их все в объект. Вот как это может выглядеть:

export const sampleFunction = ({ param1, param2, param3 }) => {console.log({ param1, param2, param3 });}

Если подходить к созданию сигнатур своих функций именно так можно получить несколько заметных преимуществ перед обычным способом объявления функций:

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

При работе с обработчиками событий используйте функции, возвращающие функции-обработчики


Если вы знакомы с функциональным программированием то знайте, что то, о чём я хочу сейчас рассказать, напоминает каррирование. Суть в том, что тут применяется заблаговременная установка некоторых параметров функции.

Взгляните на этот пример:

export default function SampleComponent({ onValueChange }) {const handleChange = (key) => {return (e) => onValueChange(key, e.target.value)}return (<form><input onChange={handleChange('name')} /><input onChange={handleChange('email')} /><input onChange={handleChange('phone')} /></form>)}

Как видите, такое оформление функции-обработчика событий помогает поддерживать дерево компонентов в чистоте.

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


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

Вот пример, в котором используется if/else:

const Student = ({ name }) => <p>Student name: {name}</p>const Teacher = ({ name }) => <p>Teacher name: {name}</p>const Guardian = ({ name }) => <p>Guardian name: {name}</p>export default function SampleComponent({ user }) {let Component = Student;if (user.type === 'teacher') {Component = Teacher} else if (user.type === 'guardian') {Component = Guardian}return (<div><Component name={user.name} /></div>)}

А вот пример использования объекта, хранящего соответствующие значения:

import React from 'react'const Student = ({ name }) => <p>Student name: {name}</p>const Teacher = ({ name }) => <p>Teacher name: {name}</p>const Guardian = ({ name }) => <p>Guardian name: {name}</p>const COMPONENT_MAP = {student: Student,teacher: Teacher,guardian: Guardian}export default function SampleComponent({ user }) {const Component = COMPONENT_MAP[user.type]return (<div><Component name={user.name} /></div>)}

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

Оформляйте в виде хуков компоненты, рассчитанные на многократное использование в проекте


Я считаю весьма полезным паттерн, о котором хочу рассказать в этом разделе.

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

import ConfirmationDialog from 'components/global/ConfirmationDialog';export default function useConfirmationDialog({headerText,bodyText,confirmationButtonText,onConfirmClick,}) {const [isOpen, setIsOpen] = useState(false);const onOpen = () => {setIsOpen(true);};const Dialog = useCallback(() => (<ConfirmationDialogheaderText={headerText}bodyText={bodyText}isOpen={isOpen}onConfirmClick={onConfirmClick}onCancelClick={() => setIsOpen(false)}confirmationButtonText={confirmationButtonText}/>),[isOpen]);return {Dialog,onOpen,};}

Пользоваться этим хуком можно так:

import React from "react";import { useConfirmationDialog } from './useConfirmationDialog'function Client() {const { Dialog, onOpen } = useConfirmationDialog({headerText: "Delete this record?",bodyText:"Are you sure you want to delete this record? This cannot be undone.",confirmationButtonText: "Delete",onConfirmClick: handleDeleteConfirm,});function handleDeleteConfirm() {//TODO: удалить}const handleDeleteClick = () => {onOpen();};return (<div><Dialog /><button onClick={handleDeleteClick} /></div>);}export default Client;

Такой подход к абстрагированию компонентов избавляет программиста от необходимости написания больших объёмов шаблонного кода для управления состоянием приложения. Если вы хотите узнать о нескольких полезных хуках React взгляните на этот мой материал.

Разделяйте код компонентов на части


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

Использование обёрток


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

Вот пример компонента, реализующего drag-and-drop с использованием react-beautiful-dnd:

import React from 'react'import { DragDropContext, Droppable } from 'react-beautiful-dnd';export default function DraggableSample() {function handleDragStart(result) {console.log({ result });}function handleDragUpdate({ destination })console.log({ destination });}const handleDragEnd = ({ source, destination }) => {console.log({ source, destination });};return (<div><DragDropContextonDragEnd={handleDragEnd}onDragStart={handleDragStart}onDragUpdate={handleDragUpdate}><Droppable droppableId="droppable" direction="horizontal">{(provided) => (<div {...provided.droppableProps} ref={provided.innerRef}>{columns.map((column, index) => {return (<ColumnComponentkey={index}column={column}/>);})}</div>)}</Droppable></DragDropContext></div>)}

А теперь взгляните на тот же компонент после того, как мы переместили drag-and-drop-логику в компонент-обёртку:

import React from 'react'export default function DraggableSample() {return (<div><DragWrapper>{columns.map((column, index) => {return (<ColumnComponentkey={index}column={column}/>);})}</DragWrapper></div>)}

Вот код обёртки:

import React from 'react'import { DragDropContext, Droppable } from 'react-beautiful-dnd';export default function DragWrapper({children}) {function handleDragStart(result) {console.log({ result });}function handleDragUpdate({ destination }) {console.log({ destination });}const handleDragEnd = ({ source, destination }) => {console.log({ source, destination });};return (<DragDropContextonDragEnd={handleDragEnd}onDragStart={handleDragStart}onDragUpdate={handleDragUpdate}><Droppable droppableId="droppable" direction="horizontal">{(provided) => (<div {...provided.droppableProps} ref={provided.innerRef}>{children}</div>)}</Droppable></DragDropContext>)}

В результате код компонента можно очень быстро прочесть и в общих чертах разобраться с тем, какую именно задачу решает этот компонент. А весь функционал, имеющий отношение к механизму drag-and-drop, размещён в компоненте-обёртке. С кодом, реализующим этот механизм, теперь тоже гораздо проще и удобнее работать.

Разделение обязанностей


Тут я хочу рассказать о моём любимом методе разделения больших компонентов на части.

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

Паттерн хук появился именно благодаря идее разделения обязанностей. Можно и даже нужно оборачивать в хуки всю логику, ответственную за взаимодействие с некими API или с глобальным состоянием.

Например давайте посмотрим на следующий компонент:

import React from 'react'import { someAPICall } from './API'import ItemDisplay from './ItemDisplay'export default function SampleComponent() {const [data, setData] = useState([])useEffect(() => {someAPICall().then((result) => {setData(result)})}, [])function handleDelete() {console.log('Delete!');}function handleAdd() {console.log('Add!');}const handleEdit = () => {console.log('Edit!');};return (<div><div>{data.map(item => <ItemDisplay item={item} />)}</div><div><button onClick={handleDelete} /><button onClick={handleAdd} /><button onClick={handleEdit} /></div></div>)}

Ниже показано то, что получилось после его рефакторинга, в ходе которого его код разделён на части с применением пользовательских хуков. А именно вот сам компонент:

import React from 'react'import ItemDisplay from './ItemDisplay'export default function SampleComponent() {const { data, handleDelete, handleEdit, handleAdd } = useCustomHook()return (<div><div>{data.map(item => <ItemDisplay item={item} />)}</div><div><button onClick={handleDelete} /><button onClick={handleAdd} /><button onClick={handleEdit} /></div></div>)}

Вот код хука:

import { someAPICall } from './API'export const useCustomHook = () => {const [data, setData] = useState([])useEffect(() => {someAPICall().then((result) => {setData(result)})}, [])function handleDelete() {console.log('Delete!');}function handleAdd() {console.log('Add!');}const handleEdit = () => {console.log('Edit!');};return { handleEdit, handleAdd, handleDelete, data }}

Хранение кода каждого компонента в отдельном файле


Часто программисты пишут код компонентов примерно так:

import React from 'react'export default function SampleComponent({ data }) {export const ItemDisplay = ({ name, date }) => (<div><h3><font color="#3AC1EF">{name}</font></h3><p>{date}</p></div>)return (<div><div>{data.map(item => <ItemDisplay item={item} />)}</div></div>)}

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

Итоги


Написание чистого кода, в основном, сводится к тому, чтобы внимательно и вдумчиво относиться к своему делу и находить время на изучение удачных и неудачных паттернов. Хорошие решения стоит включать в свои проекты, а анти-паттерны лучше обходить стороной. Собственно говоря, применение удачных паттернов помогает писать более чистый код React-компонентов. Паттерны, которыми я тут поделился, очень помогают мне в работе. Надеюсь, они пригодятся и вам.

Что вы посоветовали бы тем, кто хочет сделать код своих React-компонентов чище?

Подробнее..

Перевод Целительная сила JavaScript

26.04.2021 16:20:03 | Автор: admin

Чуть меньше года назад, когда по всему миру начали распространяться локдауны в связи с Covid-19, большинство людей начало запасаться туалетной бумагой и консервами. Но лично я стремился получить нечто другое: реализовать функцию поиска.

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

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

Разбиваем задачу на части. Записываем их в to-do-приложение (я люблю пользоваться Things). И так создаётся творческая вселенная. Каждый день я отстраняюсь всемирного коллапса общества, который разворачивается за рамками моей жизни, и погружаюсь в исследовательскую работу, один за другим вычёркивая пункты to-do. Covid был масштабным; мой список to-do был приличной длины.

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

Весь процесс работы стал побегом, но побегом с импульсом для роста. Настраиваем идеальный стиль навигации с клавиатуры, сдвигаем момент передачи полезной нагрузки поиска, находим баланс между размером индекса и полезностью поиска. И самое важное сохраняем восхитительную лёгкость кода. Завершаем, превращаем код в крошечный gist на GitHub, делимся им с сообществом. Это похоже на пас мяча другим людям: вперёд, теперь вы можете использовать это на своём веб-сайте. Сверхбыстрый и оптимизированный для клавиатуры клиентский поиск на Hugo.

Он неидеален, но чертовски хорош.

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

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

Паттерн сложился: когда меня в детстве утомляли сложности социальных ситуаций, я обращался к коду, становясь затворником. Эллен Ульман в своей книге Life in Code: A Personal History of Technology пишет: Пока я не стала программистом, я не понимала во всей полноте полезность такой изоляции: тишина, сведение жизни к мысли и форме; например, уход в тёмную комнату для работы над программой, когда отношения с людьми усложняются.

Чтение книг по ассемблеру в средней школе или программирование ПО для BBS в старшей школе ещё не осознавалось мной как спасение. Моё первое осознанное признание целительной силы кода появилось несколько лет назад, когда я рефакторил свой веб-сайт с одной системы управления контентом на другую. Кажется невообразимым, но это правда: меня исцелила CMS.

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

Такое иногда случается со мной; с некоторыми людьми такое происходит часто. Когда я ощущаю тяжесть сходящей на меня депрессии, то часто вспоминаю эпиграф к Зримой тьме Уильяма Стайрона: Ибо ужасное, чего я ужасался, то и постигло меня... Обычно это означает, что я недостаточно отдыхал. Я имею в виду не в течение дней, а, скорее, месяцев или лет. Я постепенно накреняюсь, как корабль, в который через течь поступает вода. Через какое-то время он обязательно потонет. Мой мозг постепенно тонул и я ощущал, что ему как спасение нужны были серверы. Оказалось, что серверы это одно из безопасных мест для меня.

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

Практически всё, что связано с серверами, происходит в командной строке терминала мире, избавленном от изображений и графических интерфейсов. Только текст. Каждое действие отточено до нажатия и буквально. Опечатка в единственном символе может уничтожить систему. На самом деле, при переключении в режим администратора или суперпользователя серверы десятки лет демонстрировали такое сообщение:

Мы верим, что вы выслушали привычную лекцию вашего системного администратора. Обычно она сводится к следующим трём пунктам:

1: Уважайте конфиденциальность других людей.

2: Думайте, прежде чем нажимать на клавиши.

3: С большой силой приходит большая ответственность.

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

Частично этим и вызвана привлекательность систем: движение сквозь этот хаос, со всей его извращённой поэзией grep, vi, git, apache и *.ini*, при помощи молниеносных движений пальцев по клавиатуре, изумляет. Ты ощущаешь себя алхимиком. Да и являешься им. Ты вводишь загадочные слова, почти абракадабру, в построчный текстовый интерфейс, и вот готово простое приложение, доступ к которому мгновенно появляется у огромного количества людей по всему свету.

Любимые люди часто испытывали смущение или даже подозрительность, случайно узнав, что я хорошо владею bash (разновидность терминальной оболочки для ввода команд). Как будто я скрывал от них какую-то неприятную тайну. Однажды, войдя мир текста, я несколькими быстрыми нажатиями клавиш помог сыну-подростку друга установить моды для Minecraft. По его взгляду я понял, что мгновенно стал для него кем-то вроде рок-звезды. Благодаря двум сотням нажатий появился мостик между двумя поколениями.

Я нахожу умиротворение в тёмном хаосе этого мира. Код и серверы являются для меня домом, и это чувство сложно объяснить тем, для кого они домом не стали.

Поэтому в моём накренённом, слегка депрессивном состоянии я занялся переносом веб-сайтов со старого на новый сервер. Мои задачи были записаны в моём надёжном списке to-do. Адреса URL старых сайтов знаменовали уникальные эпохи моей жизни, через объективы которых я когда-то видел себя.

Мои старые веб-сайты это призраки. Никто не будет их просматривать или атаковать их. Я перенёс их, потому что ощущал ответственность за них, чувствовал, что они имеют право продолжить жизнь в битах.

Основная часть работы с серверами была связана с тем, чтобы сделать сложные сайты менее сложными. То есть превратить динамическое в статическое. Вырвать эти сайты из их PHP-ядер, вернуть их обратно к спокойным HTML и CSS, сделать так, чтобы на их обслуживание требовалось мало времени и они были готовы к будущему. Забавно, что даже нечто простое вроде базы данных MYSQL требует обрезки и подкормки, подобно растениям. Что кажущийся безобидным PHP-скрипт становится спустя десяток лет устаревшим из-за эволюции ментальных моделей языков. Но если взять страницу на HTML из ранних 90-х, то она отрендерится почти на любом устройстве с экраном.

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

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

В конце 1990-х у нас практически не было выбора, каждому приходилось быть владельцем, уборщиком и системным инженером своей домашней страницы. Сегодня можно спихнуть эту ответственность на третьих лиц. Tumblr, Ghost, Facebook, Blogger, Wordpress возникло множество платформ, на которых можно сосредоточиться исключительно на контенте, взамен этого понизив свой уровень контроля.

Можно поднять уровень ответственности слишком высоко. Вероятно, это случилось со мной. Сколько бы усилий ты не вложил в систему, эффект от этого не может превысить некой величины. Но меня это не волнует.

Эта работа, построчное решение проблем, иногда становится именно тем, что заставляет меня вставать с кровати. Вам знакомо это чувство? Не хочу выбираться из-под одеяла? Каждое утро последнего года для всего человечества, наверное, самым большим желанием было остаться в постели, настолько разбалансированным оказался этот мир. Но потом под этим одеялом я начинаю думать Ага! Я знаю, как решить проблему X сервера, или как разобраться со странным поведением Y. Я знаю, как исправить этот код поиска. И благодаря этому я могу встать и стать человеком (или хотя бы частично человеком), войти в этот мир строк, где тебя никто не осудит. В нём только ты и механика систем; систем, которые становятся всё более красивыми, чем больше времени ты на них тратишь. Для меня такая ответственность это терапия.

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

Стек вызовов JavaScript и ещё большая магия

12.04.2021 18:13:34 | Автор: admin


В начале апреля на хабре была опубликована статья JavaScript: Стек вызовов и магия его размера её автор пришёл к выводу, что каждый кадр стека занимает (72 + 8 * число_локальных_переменных) байтов: Получается, что мы посчитали все верно и можем утверждать, что размер пустого ExecutionStack в Chrome равен 72 байтам, а размер коллстэка чуть меньше одного мегабайта. Отличная работа!

Для затравки немного изменим код, использованный AxemaFr для экспериментов:

{let i = 0;const func = () => {  i += 1.00000000000001;  func();};try {  func();} catch (e) {  console.log(i);}}

Вместо 1 теперь на каждом шаге прибавляем чуточку больше, и в результате вместо 13951 получаем 12556.000000000002 как будто бы в функции добавилась локальная переменная!

Повторим вопросы, которыми задавался Senior Frontend Developer AxemaFr: Почему же так? Что изменилось? Как понять, посмотрев на функцию, сколько раз она может выполниться рекурсивно?!

Готовим инструменты


В командной строке Chrome можно передавать аргументы для JS-движка; в частности, можно поменять и размер стека с 984 КБ на любой другой ключом --js-flags=--stack-size=.

Разобраться в том, сколько стека требуется каждой функции, нам поможет ключ --print-bytecode, уже упоминавшийся ранее. Не упоминалось то, что отладочный вывод направляется в stdout, которого у Chrome под Windows тупо нет, потому что он скомпилирован как GUI-приложение. Исправить это несложно: сделайте копию chrome.exe, и в своём любимом hex-редакторе исправьте байт 0xD4 со значения 0x02 на 0x03 (тем, кто с hex-редактором не дружит, этот байт поможет исправить скрипт на Python). Но если вы прямо сейчас читаете эту статью в Chrome, и просто запустите исправленный файл предположим, что вы назвали его cui_chrome.exe то откроется новое окно в уже существующем экземпляре браузера, и аргумент --js-flags будет проигнорирован. Чтобы запустить новый экземпляр Chrome, нужно передать ему какую-нибудь новую --user-data-dir:
cui_chrome.exe --no-sandbox --js-flags="--print-bytecode --print-bytecode-filter=func" --user-data-dir=\Windows\Temp

Без --print-bytecode-filter вы утонете в километровых дампах байткода функций, встроенных в Chrome.

После запуска браузера откройте консоль разработчика и введите код, использованный AxemaFr:

{let i = 0;const func = () => {  i++;  func();};func()}

Ещё до того, как вы нажмёте на Enter, в консольном окне позади Chrome появится дамп:
[generated bytecode for function: func (0x44db08635355 <SharedFunctionInfo func>)]Parameter count 1Register count 1Frame size 8   36 S> 000044DB086355EE @    0 : 1a 02             LdaCurrentContextSlot [2]         000044DB086355F0 @    2 : ac 00             ThrowReferenceErrorIfHole [0]         000044DB086355F2 @    4 : 4d 00             Inc [0]         000044DB086355F4 @    6 : 26 fa             Star r0         000044DB086355F6 @    8 : 1a 02             LdaCurrentContextSlot [2]   37 E> 000044DB086355F8 @   10 : ac 00             ThrowReferenceErrorIfHole [0]         000044DB086355FA @   12 : 25 fa             Ldar r0         000044DB086355FC @   14 : 1d 02             StaCurrentContextSlot [2]   44 S> 000044DB086355FE @   16 : 1b 03             LdaImmutableCurrentContextSlot [3]         000044DB08635600 @   18 : ac 01             ThrowReferenceErrorIfHole [1]         000044DB08635602 @   20 : 26 fa             Star r0   44 E> 000044DB08635604 @   22 : 5d fa 01          CallUndefinedReceiver0 r0, [1]         000044DB08635607 @   25 : 0d                LdaUndefined   52 S> 000044DB08635608 @   26 : ab                ReturnConstant pool (size = 2)Handler Table (size = 0)Source Position Table (size = 12)

Как изменится дамп, если строчку i++; заменить на i += 1.00000000000001;?
[generated bytecode for function: func (0x44db0892d495 <SharedFunctionInfo func>)]Parameter count 1Register count 2Frame size 16   36 S> 000044DB0892D742 @    0 : 1a 02             LdaCurrentContextSlot [2]         000044DB0892D744 @    2 : ac 00             ThrowReferenceErrorIfHole [0]         000044DB0892D746 @    4 : 26 fa             Star r0         000044DB0892D748 @    6 : 12 01             LdaConstant [1]         000044DB0892D74A @    8 : 35 fa 00          Add r0, [0]         000044DB0892D74D @   11 : 26 f9             Star r1         000044DB0892D74F @   13 : 1a 02             LdaCurrentContextSlot [2]   37 E> 000044DB0892D751 @   15 : ac 00             ThrowReferenceErrorIfHole [0]         000044DB0892D753 @   17 : 25 f9             Ldar r1         000044DB0892D755 @   19 : 1d 02             StaCurrentContextSlot [2]   60 S> 000044DB0892D757 @   21 : 1b 03             LdaImmutableCurrentContextSlot [3]         000044DB0892D759 @   23 : ac 02             ThrowReferenceErrorIfHole [2]         000044DB0892D75B @   25 : 26 fa             Star r0   60 E> 000044DB0892D75D @   27 : 5d fa 01          CallUndefinedReceiver0 r0, [1]         000044DB0892D760 @   30 : 0d                LdaUndefined   68 S> 000044DB0892D761 @   31 : ab                ReturnConstant pool (size = 3)Handler Table (size = 0)Source Position Table (size = 12)

Теперь разберёмся, что поменялось и почему.

Исследуем примеры


Все опкоды V8 описаны в github.com/v8/v8/blob/master/src/interpreter/interpreter-generator.cc
Первый дамп расшифровывается так:
LdaCurrentContextSlot [2]           ; a := context[2]ThrowReferenceErrorIfHole [0]       ; if (a === undefined)                                    ;   throw("ReferenceError: %s is not defined", const[0])Inc [0]                             ; a++Star r0                             ; r0 := aLdaCurrentContextSlot [2]           ; a := context[2]ThrowReferenceErrorIfHole [0]       ; if (a === undefined)                                    ;   throw("ReferenceError: %s is not defined", const[0])Ldar r0                             ; a := r0StaCurrentContextSlot [2]           ; context[2] := aLdaImmutableCurrentContextSlot [3]  ; a := context[3]ThrowReferenceErrorIfHole [1]       ; if (a === undefined)                                    ;   throw("ReferenceError: %s is not defined", const[1])Star r0                             ; r0 := aCallUndefinedReceiver0 r0, [1]      ; r0()LdaUndefined                        ; a := undefinedReturn

Последний аргумент опкодов Inc и CallUndefinedReceiver0 задаёт feedback slot, в котором для оптимизатора накапливается статистика об использованных типах. На семантику байткода это не влияет, так что нас сегодня совсем не интересует.

Под дампом есть приписка: Constant pool (size = 2) и действительно видим, что в байткоде используются две строки "i" и "func" для подстановки в сообщение исключения, когда символы с такими именами undefined. Есть приписка и над дампом: Frame size 8 в соответствии с тем, что в функции используется один регистр интерпретатора (r0).

Стековый кадр нашей функции состоит из:

  • единственного аргумента this;
  • адреса возврата;
  • числа переданных аргументов (arguments.length);
  • ссылки на constant pool c используемыми строками;
  • ссылки на context с локальными переменными;
  • ещё трёх указателей, нужных движку; и наконец,
  • места для одного регистра.

Итого 9*8=72 байта, как синьор AxemaFr и вычислил.

Из семи перечисленных слагаемых, теоретически, меняться могут три число аргументов, наличие constant pool, и число регистров. Что у нас получилось в варианте с 1.00000000000001?

LdaCurrentContextSlot [2]      ; a := context[2]ThrowReferenceErrorIfHole [0]  ; if (a === undefined)                               ;   throw("ReferenceError: %s is not defined", const[0])Star r0                        ; r0 := aLdaConstant [1]                ; a := const[1]Add r0, [0]                    ; a += r0Star r1                        ; r1 := a                               ; ...дальше как раньше

Во-первых, прибавляемая константа заняла третье место в constant pool; во-вторых, для её загрузки понадобился ещё один регистр, так что стековый кадр функции вырос на 8 байтов.

Если не использовать в функции именованные символы, то можно обойтись без constant pool. На github.com/v8/v8/blob/master/src/execution/frame-constants.h#L289 описан формат стекового кадра V8 и указано, что когда constant pool не используется, то размер стекового кадра сокращается на один указатель. Как в этом удостовериться? На первый взгляд кажется, что функция, не использующая именованные символы, не может быть рекурсивной; но взгляните-ка:

{let i = 0;function func() {  this()();};const helper = () => (i++, func.bind(helper));try {  helper()();} catch (e) {  console.log(i);}}

[generated bytecode for function: func (0x44db0878e575 <SharedFunctionInfo func>)]Parameter count 1Register count 1Frame size 8   37 S> 000044DB0878E8DA @    0 : 5e 02 02 00       CallUndefinedReceiver1 <this>, <this>, [0]         000044DB0878E8DE @    4 : 26 fa             Star r0   43 E> 000044DB0878E8E0 @    6 : 5d fa 02          CallUndefinedReceiver0 r0, [2]         000044DB0878E8E3 @    9 : 0d                LdaUndefined   47 S> 000044DB0878E8E4 @   10 : ab                ReturnConstant pool (size = 0)Handler Table (size = 0)Source Position Table (size = 8)

Цель Constant pool (size = 0) достигнута; но переполнение стека, как и раньше, происходит через 13951 вызов. Это значит, что даже когда constant pool не используется, стековый кадр функции всё равно содержит указатель на него.

А удастся ли добиться меньшего размера стекового кадра, чем вычисленное AxemaFr минимальное значение? да, если внутри функции не использовать ни один регистр:
{function func() {  this();};let chain = ()=>null;for(let i=0; i<15050; i++)  chain = func.bind(chain);chain()}

[generated bytecode for function: func (0x44db08c34059 <SharedFunctionInfo func>)]Parameter count 1Register count 0Frame size 0   25 S> 000044DB08C34322 @    0 : 5d 02 00          CallUndefinedReceiver0 <this>, [0]         000044DB08C34325 @    3 : 0d                LdaUndefined   29 S> 000044DB08C34326 @    4 : ab                ReturnConstant pool (size = 0)Handler Table (size = 0)Source Position Table (size = 6)

(При этом цепочка из 15051 вызова уже приводит к RangeError: Maximum call stack size exceeded.)

Таким образом, вывод синьора AxemaFr о том, что размер пустого ExecutionStack в Chrome равен 72 байтам, успешно опровергнут.

Уточняем предсказания


Мы можем утверждать, что минимальный размер стекового кадра для JS-функции в Chrome равен 64 байтам. К этому нужно прибавить по 8 байтов за каждый объявленный формальный параметр, ещё по 8 байтов за каждый фактический параметр сверх числа объявленных, и ещё по 8 байтов за каждый использованный регистр. По регистру отводится для каждой локальной переменной, для загрузки констант, для обращения к переменным из внешнего контекста, для передачи фактических параметров при вызовах, и т.д. Точное число использованных регистров по исходному тексту на JS вряд ли возможно определить. Стоит отметить, что интерпретатор JS поддерживает неограниченное число регистров они не имеют отношения к регистрам процессора, на котором интерпретатор выполняется.

Теперь понятно, почему:
  • добавление неиспользуемого формального параметра (func = (x) => { i++; func(); };) потребляет столько же памяти, как дополнительная локальная переменная;
  • передача необъявленного фактического параметра (func = () => { i++; func(1); };) потребляет вдвое больше памяти, чем дополнительная локальная переменная потому что для передачи понадобился дополнительный регистр:
    [generated bytecode for function: func (0x44db08e12da1 <SharedFunctionInfo func>)]Parameter count 1Register count 2Frame size 16   34 S> 000044DB08E12FE2 @    0 : 1a 02             LdaCurrentContextSlot [2]         000044DB08E12FE4 @    2 : ac 00             ThrowReferenceErrorIfHole [0]         000044DB08E12FE6 @    4 : 4d 00             Inc [0]         000044DB08E12FE8 @    6 : 26 fa             Star r0         000044DB08E12FEA @    8 : 1a 02             LdaCurrentContextSlot [2]   35 E> 000044DB08E12FEC @   10 : ac 00             ThrowReferenceErrorIfHole [0]         000044DB08E12FEE @   12 : 25 fa             Ldar r0         000044DB08E12FF0 @   14 : 1d 02             StaCurrentContextSlot [2]   39 S> 000044DB08E12FF2 @   16 : 1b 03             LdaImmutableCurrentContextSlot [3]         000044DB08E12FF4 @   18 : ac 01             ThrowReferenceErrorIfHole [1]         000044DB08E12FF6 @   20 : 26 fa             Star r0         000044DB08E12FF8 @   22 : 0c 01             LdaSmi [1]         000044DB08E12FFA @   24 : 26 f9             Star r1   39 E> 000044DB08E12FFC @   26 : 5e fa f9 01       CallUndefinedReceiver1 r0, r1, [1]         000044DB08E13000 @   30 : 0d                LdaUndefined   48 S> 000044DB08E13001 @   31 : ab                ReturnConstant pool (size = 2)Handler Table (size = 0)Source Position Table (size = 12)
    
  • изменение в предыдущем примере добавляемого значения на 1.00000000000001 не влияет на размер стекового кадра потому что один и тот же r1 используется и для загрузки константы, и для передачи фактического параметра.


Подробнее..

Перевод Тонкости работы консоли разработчика Chrome

17.04.2021 14:23:59 | Автор: admin

Начнем с простого. Вот фрагмент кода JavaScript, который создает небольшой массив чисел, а затем изменяет его. Массив записывается в консоль как до, так и после изменения:

const numbers = [2, 3, 4, 5];console.log(numbers);// Square the numbersfor (let i = 0; i<numbers.length; i++) {    numbers[i] = numbers[i]**2;}console.log(numbers);

Более осторожный подход будет использовать Array.map() для обработки массива вместо цикла for...of. (Таким образом, ваши изменения будут применены неразрушающим образом к новому массиву.) Но есть причина, по которой я выбрал этот подход. Он демонстрирует первый пример некой странной причуды в консоли разработчика.

Чтобы увидеть проблему в действии, откройте эту страницу в браузере на базе Chromium (например, Chrome или Edge), затем откройте консоль разработчика и затем разверните списки массивов в консоли. Вы увидите два массива, но оба они будут экземплярами измененного массива:

Почему это происходит? Если окно консоли закрыто во время выполнения кода, и вы регистрируете объект, срабатывает шаблон ленивого вычисления. Ваша команда console.log() фактически регистрирует ссылку на массив. В интересах экономии памяти и оптимизации производительности Chrome не пытается извлечь информацию из массива, пока вы не развернете ее в консоли, то есть после того, как она будет преобразована в окончательную форму.

Есть много способов обойти эту проблему. Откройте консоль перед загрузкой страницы. Или явно преобразуйте массив в строку, когда вы его регистрируете, потому что строки не будут использовать ленивую оценку:

console.log(numbers.toString());

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

const objects = [ {name: 'Sadie', age: 12},{name: 'Patrick', age: 18}];console.log(objects);objects[0].age = 14;console.log(objects);

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

Как решить эту проблему? Вы можете преобразовать массив в строку, но не с помощью простого вызова toString(). Вместо этого вам нужно будет написать алгоритм, который будет делать это самостоятельно, или использовать библиотеку, или использовать JSON. Но, конечно, настоящая проблема в том, что эта ошибка может возникнуть в иерархии вложенных объектов, когда вы этого не ожидаете, и вы невольно примете информацию в консоли разработчика, если она окажется неточной. Лучшая защита - знать о возможности ленивой оценки. Еще одна хорошая практика - написать целевой код ведения консоли для вывода определенных свойств с примитивными типами данных. Не регистрируйте целые объекты и полагайтесь на расширяемое представление в консоли, если это не является абсолютно необходимым.

Подробнее..

Погружение во внедрение зависимостей (DI), или как взломать Матрицу

03.06.2021 16:16:28 | Автор: admin

Давным-давно в далекой Галактике, когда сестры Вачовски еще были братьями, искусственный разум в лице Архитектора поработил человечество и создал Матрицу Всем привет, это снова Максим Кравец из Holyweb, и сегодня я хочу поговорить про Dependency Injection, то есть про внедрение зависимостей, или просто DI. Зачем? Возможно, просто хочется почувствовать себя Морфеусом, произнеся сакраментальное: Я не могу объяснить тебе, что такое DI, я могу лишь показать тебе правду.

Постановка задачи

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

Пифия

Фабула, надеюсь, всем известна есть Матрица, к ней подключены люди. Люди пытаются освободиться, им мешают Агенты. Главный вопрос кто победит? Но это будет в конце фильма, а мы с вами пока в самом начале. Так что давайте поставим себя на место Архитектора и подумаем, как нам создать Матрицу?

Что есть программы? Те самые, которые управляют птицами, деревьями, ветром.

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

Что нам нужно обеспечить для функционирования Матрицы? Механизм внедрения, или (внимание, рояль в кустах), инжекции (Injection) функционала классов, отвечающих за всю вышеперечисленную флору, фауну и прочие природные явления, внутрь Матрицы.

Подождем, пока грузчики установят в кустах очередной музыкальный инструмент, и зададимся вопросом: а что произойдет с Матрицей после того, как мы в нее инжектируем нужный нам функционал? Все правильно у нее появятся зависимости (Dependency) от внешних по отношению к ней классов.

Пазл сложился? С одной стороны да. Dependency Injection это всего лишь механизм внедрения в класс зависимости от другого класса. С другой что это за механизм, для чего он нужен и когда его стоит использовать?

Первым делом, посмотрим на цитату в начале текста и обратим внимание на предложение: Программы совершенствуются. То есть переписываются. Изменяются. Что это означает для нас? Работа нашей Матрицы не должна зависеть от конкретной реализации класса зависимости.

Кажется, ерунда какая-то зависимость на то и зависимость, чтобы от нее зависеть!

А теперь следите за руками. Мы внедряем в Матрицу не конкретную реализацию зависимости, а абстрактный контракт, и реализуем механизм предоставления конкретной реализации, соответствующей этому контракту! Остались сущие пустяки понять, как же это все реализовать.

Внедрение зависимости в чистом виде

Оставим романтикам рассветы и закаты, птичек и цветочки. Мы, человеки, должны вырваться из под гнета ИИ вообще и Архитектора в частности. Так что будем разбираться с реализацией DI и параллельно освобождаться из Матрицы. Первая итерация. Создадим класс matrix, непосредственно в нем создадим агента по имени Смит, определим его силу. Там же, внутри Матрицы, создадим и претендента, задав его силу, после чего посмотрим, кто победит, вызвав метод whoWin():

class Matrix {  agent = {    name: 'Smith',    damage: 10000,  };  human = {    name: 'Cypher',    damage: 100,  };  whoWin(): string {    const result = this.agent.damage > this.human.damage      ? this.agent.name      : this.human.name;    return result;  }}const matrixV1 = new Matrix();console.log(Побеждает , matrixV1.whoWin());

Да, Сайфер не самый приятный персонаж, да еще и хотел вернуться в Матрицу, так что на роль первого проигравшего подходит идеально.

Побеждает  Smith

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

class Human {  name;  damage;  constructor(name, damage) {    this.name = name;    this.damage = damage;  }  get name(): string {    return this.name;  }  get damage(): number {    return this.damage;  }}class Matrix {  agent = {    name: 'Smith',    damage: 10000,  }; human;  constructor(challenger) {    this.human = challenger;  }  whoWin(): string {    const result = this.agent.damage > this.human.damage      ? this.agent.name      : this.human.name;    return result;  }

Мы добавили класс Human, принимающий на вход конструктора имя и силу человека, и возвращающий их в соответствующих методах. Также мы внесли изменения в наш класс Матрицы теперь информацию о претенденте на победу он получает через конструктор. Давайте проверим, сможет ли Тринити победить Агента Смита?

const Trinity = new Human('Trinity', 500);const matrixV1 = new Matrix(Trinity);console.log('Побеждает ', matrixV1.whoWin());

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

Побеждает  Smith

Но стоп! Давайте посмотрим, что случилось с Матрицей? А случилось то, что класс Matrix и результаты его работы стал зависеть от класса Human! И нашему оператору, отправляющему Тринити в Матрицу, достаточно немного изменить код, чтобы обеспечить победу человечества!

class Human {   get damage(): number {    return this.damage * 1000;  }}

...

Пьем шампанское и расходимся по домам?

Чем плох подход выше? Тем, что класс Matrix ждет от зависимости challenger, передаваемой в конструктор, наличие метода damage, поскольку именно к нему мы обращаемся в коде. Но об этом знает Архитектор, создавший Матрицу, а не наш оператор! В примере мы можем угадать. А если не знать заранее название метода? Может быть, надо было написать не damage, а power? Или strength?

Инверсия зависимостей

Знакомьтесь! Dependency inversion principle, принцип инверсии зависимостей (DIP). Название, кстати, нередко сокращают, убирая слово принцип , и тогда остается только Dependency inversion (DI), что вносит путаницу в мысли новичков.

Принцип инверсии зависимостей имеет несколько трактовок, мы приведем лишь две:

  1. Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.

  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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

Давайте внедрим в наш класс Matrix некий абстрактный класс AbstractHuman, а конкретную реализацию в виде класса Human попросим имплементировать эту абстракцию:

abstract class AbstractHuman {  abstract get name(): string;  abstract get damage(): number;}class Human implements AbstractHuman{  name;  damage;  constructor(name, damage) {    this.name = name;    this.damage = damage;  }  get name(): string {    return this.name;  }  get damage(): number {    return this.damage;  }}class Matrix {  agent = {    name: 'Smith',    damage: 10000,  }; human;  constructor(challenger: AbstractHuman) {    this.human = challenger;  }  whoWin(): string {    const result = this.agent.damage > this.human.damage      ? this.agent.name      : this.human.name;    return result;  }}const Morpheus = new Human('Morpheus', 900);const matrixV2 = new Matrix(Morpheus);console.log('Побеждает ', matrixV2.whoWin());

Морфеуса жалко, но все же он не избранный.

Побеждает  Smith

Вторая версия Матрицы пока что выигрывает, но что получилось на текущий момент? Класс Matrix больше не зависит от конкретной реализации класса Human задачу номер один мы выполнили. Класс Human отныне точно знает, какие методы с какими именами в нем должны присутствовать пока контракт в виде абстрактного класса AbstractHuman не будет полностью реализован (имплементирован) в конкретной реализации, мы будем получать ошибку. Задача номер два также выполнена.

Порадуемся за архитектора, кстати! В новой реализации он может создать отдельный класс для мужчин, отдельный для женщин, если понадобится сделать отдельный класс для брюнеток и отдельный для блондинок Согласитесь, таким модульным кодом легче управлять.

В бою с Морфеусом побеждает  SmithВ бою с Тринити побеждает  Smith

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

...class TheOne implements AbstractHuman{  name;  damage;  constructor(name, damage) {    this.name = name;    this.damage = damage;  }  get name(): string {    return this.name;  }  get damage(): number {    return this.damage * 1000;  }}const Neo = new TheOne('Neo, 500);const matrixV5 = new Matrix(Neo);

Свершилось!

В бою с Нео побеждает  Нео

Инверсия управления

Давайте посмотрим, кто управляет кодом? В нашем примере мы сами пишем и класс Matrix, и класс Human, сами создаем инстансы и задаем все параметры. Мы управляем нашим кодом. Захотели внесли изменения и обеспечили победу Тринити.

Увы, по условиям мира, придуманного Вачовски, мы можем лишь вклиниваться в работу Матрицы, добавляя свои кусочки программного кода. Матрица управляет не только фантомами внутри себя, но и тем, как с ней работать извне!

Возможно, авторы трилогии увлекались программированием, потому что ситуация целиком и полностью списана с реальности и даже имеет свое название Inversion of Control (IoC).

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

Кстати, уже использованный нами выше DIP (принцип инверсии зависимостей) одно из проявлений механизма IoC.

К-контейнер н-нада?

Последний шаг передача управления разрешением зависимостей. Кому и какой инстанс предоставить, использовать singleton или multiton также решается не программистом (оператором), а фреймворком (Матрицей).

Вариантов решения задачи множество, но все они сводятся к одной идее.

  • на верхнем уровне приложения создается глобальный объект,

  • в этом объекте регистрируется абстрактный интерфейс и класс, который его имплементирует,

  • модуль запрашивает необходимый ему интерфейс (абстрактный класс),

  • глобальный объект находит класс, имплементирующий данный интерфейс, при необходимости создает инстанс и передает его в модуль.

Конкретные реализации у каждого фреймворка свои: где-то используется Локатор сервисов/служб (Service Locator), где-то Контейнер DI, чаще называемый IoC Container. Но на уровне базовой функциональности отличия между подходами стираются до неразличимости.

У нас есть класс, который мы планируем внедрить (сервис). Мы сообщаем фреймворку о том, что этот класс нужно отправить в контейнер. Наиболее наглядно это происходит в Angular мы просто вешаем декоратор Injectable.

@Injectable()export class SomeService {}

Декоратор добавит к классу набор метаданных и зарегистрирует его в IoC контейнере.

Когда нам понадобится инстанс SomeService, фреймворк обратится к контейнеру, найдет уже существующий или создаст новый инстанс сервиса и вернет его нам.

Крестики-нолики, а точнее плюсы и минусы

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

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

Вместо заключения, или как это использовать практически?

Окей, если необходимость добавления промежуточного слоя в виде контракта более-менее очевидна, то где на практике нам может пригодиться IoC?

Кейс 1 тестирование.

  • У вас есть модуль, который отвечает за оформление покупки в интернет-магазине.

  • Функционал списания средств мы вынесем в отдельный сервис и внедрим его через DI. Этот сервис будет обращаться к реальному эквайрингу банка Х.

  • Нам нужно протестировать работу модуля в целом, но мы не готовы совершать реальную покупку при каждом тесте.

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

Кейс 2 расширение функционала.

  • Модуль прежний, оформление покупки в интернет-магазине.

  • Поступает задача добавить возможность оплаты не только в банке Х, но и в банке Y.

  • Мы пишем еще один платежный сервис, реализующий взаимодействие с банком Y и имплементирующий тот же контракт, что и сервис банка X.

  • Плюсы мы можем подменять банк, мы можем давать пользователю выбор, в какой банк платить. При этом мы никак не меняем наш основной модуль.

Кейс 3 управление на уровне инфраструктуры.

  • Модуль прежний.

  • Для production работаем с боевым сервисом платежей.

  • Для разработки подгружаем моковый вариант, симулирующий списание средств.

  • Для тестового окружения пишем третий сервис, который будет симулировать списание средств, а заодно вести расширенный лог состояния.

Надеюсь, этот краткий список примеров вас убедил в том, что вопроса, использовать или не использовать DI, в современной разработке не стоит. Однозначно использовать. А значит надо понимать, как это работает. Надеюсь, мне удалось не только помочь Нео в его битве со Смитом, но и вам в понимании, как устроен и работает DI.

Если есть вопросы или дополнения по теме, буду рад продолжить общение в комментариях. Напишите, о чем рассказать в следующей статье? А если хотите познакомиться с нами ближе, я всегда на связи в Телеграме @maximkravec.

Подробнее..

Да хватит уже писать эти регулярки

08.06.2021 16:18:29 | Автор: admin

Здравствуйте, меня зовут Дмитрий Карловский и раньше я тоже использовал Perl для разработки фронтенда. Только гляньте, каким лаконичным кодом можно распарсить, например, имейл:


/^(?:((?:[\w!#\$%&'\*\+\/=\?\^`\{\|\}~-]){1,}(?:\.(?:[\w!#\$%&'\*\+\/=\?\^`\{\|\}~-]){1,}){0,})|("(?:((?:(?:([\u{1}-\u{8}\u{b}\u{c}\u{e}-\u{1f}\u{21}\u{23}-\u{5b}\u{5d}-\u{7f}])|(\\[\u{1}-\u{9}\u{b}\u{c}\u{e}-\u{7f}]))){0,}))"))@(?:((?:[\w!#\$%&'\*\+\/=\?\^`\{\|\}~-]){1,}(?:\.(?:[\w!#\$%&'\*\+\/=\?\^`\{\|\}~-]){1,}){0,}))$/gsu

Тут, правда, закралось несколько ошибок. Ну ничего, пофиксим в следующем релизе!


Шутки в сторону



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



А с внедрением новых фичей, они теряют и лаконичность:


/(?<слово>(?<буквица>\p{Script=Cyrillic})\p{Script=Cyrillic}+)/gimsu

У регулярок довольно развесистых синтаксис, который то и дело выветривается из памяти, что требует постоянное подсматривание в шпаргалку. Чего только стоят 5 разных способов экранирования:


/\t//\ci//\x09//\u0009//\u{9}/u

В JS у нас есть интерполяция строк, но как быть с регулярками?


const text = 'lol;)'// SyntaxError: Invalid regular expression: /^(lol;)){2}$/: Unmatched ')'const regexp = new RegExp( `^(${ text }){2}$` )

Ну, или у нас есть несколько простых регулярок, и мы хотим собрать из них одну сложную:


const VISA = /(?<type>4)\d{12}(?:\d{3})?/const MasterCard = /(?<type>5)[12345]\d{14}/// Invalid regular expression: /(?<type>4)\d{12}(?:\d{3})?|(?<type>5)[12345]\d{14}/: Duplicate capture group nameconst CardNumber = new RegExp( VISA.source + '|' + MasterCard.source )

Короче, писать их сложно, читать невозможно, а рефакторить вообще адски! Какие есть альтернативы?


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


Полностью своя реализация регулярок на JS. Для примера возьмём XRegExp:


  • API совместимо с нативным.
  • Можно форматировать пробелами.
  • Можно оставлять комментарии.
  • Можно расширять своими плагинами.
  • Нет статической типизации.
  • Отсутствует поддержка IDE.

В общем, всё те же проблемы, что и у нативных регулярок, но втридорога.


Генераторы парсеров


Вы скармливаете им грамматику на специальном DSL, а они выдают вам JS код функции парсинга. Для примера возьмём PEG.js:


  • Наглядный синтаксис.
  • Каждая грамматика вещь в себе и не компонуется с другими.
  • Нет статической типизации генерируемого парсера.
  • Отсутствует поддержка IDE.
  • Минимум 2 кб в ужатопережатом виде на каждую грамматику.

Пример в песочнице.


Это решение более мощное, но со своими косяками. И по воробьям из этой пушки стрелять не будешь.


Билдеры нативных регулярок


Для примера возьмём TypeScript библиотеку $mol_regexp:


  • Строгая статическая типизация.
  • Хорошая интеграция с IDE.
  • Композиция регулярок с именованными группами захвата.
  • Поддержка генерации строки, которая матчится на регулярку.

Это куда более легковесное решение. Давайте попробуем сделать что-то не бесполезное..


Номера банковских карт


Импортируем компоненты билдера


Это либо функции-фабрики регулярок, либо сами регулярки.


const {    char_only, latin_only, decimal_only,    begin, tab, line_end, end,    repeat, repeat_greedy, from,} = $mol_regexp

Ну или так, если вы ещё используете NPM


import { $mol_regexp: {    char_only, decimal_only,    begin, tab, line_end,    repeat, from,} } from 'mol_regexp'

Пишем регулярки для разных типов карт


// /4(?:\d){12,}?(?:(?:\d){3,}?){0,1}/gsuconst VISA = from([    '4',    repeat( decimal_only, 12 ),    [ repeat( decimal_only, 3 ) ],])// /5[12345](?:\d){14,}?/gsuconst MasterCard = from([    '5',    char_only( '12345' ),    repeat( decimal_only, 14 ),])

В фабрику можно передавать:


  • Строку и тогда она будет заэкранирована.
  • Число и оно будет интерпретировано как юникод кодепоинт.
  • Другую регулярку и она будет вставлена как есть.
  • Массив и он будет трактован как последовательность выражений. Вложенный массив уже используется для указания на опциональность вложенной последовательности.
  • Объект означающий захват одного из вариантов с именем соответствующим полю объекта (далее будет пример).

Компонуем в одну регулярку


// /(?:(4(?:\d){12,}?(?:(?:\d){3,}?){0,1})|(5[12345](?:\d){14,}?))/gsuconst CardNumber = from({ VISA, MasterCard })

Строка списка карт


// /^(?:\t){0,}?(?:((?:(4(?:\d){12,}?(?:(?:\d){3,}?){0,1})|(5[12345](?:\d){14,}?))))(?:((?:\r){0,1}\n)|(\r))/gmsuconst CardRow = from(    [ begin, repeat( tab ), {CardNumber}, line_end ],    { multiline: true },)

Сам список карточек


const cards = `    3123456789012    4123456789012    551234567890123    5512345678901234`

Парсим текст регуляркой


for( const token of cards.matchAll( CardRow ) ) {    if( !token.groups ) {        if( !token[0].trim() ) continue        console.log( 'Ошибка номера', token[0].trim() )        continue    }    const type = ''        || token.groups.VISA && 'Карта VISA'        || token.groups.MasterCard && 'MasterCard'    console.log( type, token.groups.CardNumber )}

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


Результат парсинга


Ошибка номера 3123456789012Карта VISA 4123456789012Ошибка номера 551234567890123MasterCard 5512345678901234

Заценить в песочнице.


E-Mail


Регулярку из начала статьи можно собрать так:


const {    begin, end,    char_only, char_range,    latin_only, slash_back,    repeat_greedy, from,} = $mol_regexp// Логин в виде пути разделённом точкамиconst atom_char = char_only( latin_only, "!#$%&'*+/=?^`{|}~-" )const atom = repeat_greedy( atom_char, 1 )const dot_atom = from([ atom, repeat_greedy([ '.', atom ]) ])// Допустимые символы в закавыченном имени сендбоксаconst name_letter = char_only(    char_range( 0x01, 0x08 ),    0x0b, 0x0c,    char_range( 0x0e, 0x1f ),    0x21,    char_range( 0x23, 0x5b ),    char_range( 0x5d, 0x7f ),)// Экранированные последовательности в имени сендбоксаconst quoted_pair = from([    slash_back,    char_only(        char_range( 0x01, 0x09 ),        0x0b, 0x0c,        char_range( 0x0e, 0x7f ),    )])// Закавыченное имя сендборксаconst name = repeat_greedy({ name_letter, quoted_pair })const quoted_name = from([ '"', {name}, '"' ])// Основные части имейла: доменная и локальнаяconst local_part = from({ dot_atom, quoted_name })const domain = dot_atom// Матчится, если вся строка является имейломconst mail = from([ begin, local_part, '@', {domain}, end ])

Но просто распарсить имейл эка невидаль. Давайте сгенерируем имейл!


//  SyntaxError: Wrong param: dot_atom=foo..barmail.generate({    dot_atom: 'foo..bar',    domain: 'example.org',})

Упс, ерунду сморозил Поправить можно так:


// foo.bar@example.orgmail.generate({    dot_atom: 'foo.bar',    domain: 'example.org',})

Или так:


// "foo..bar"@example.orgmail.generate({    name: 'foo..bar',    domain: 'example.org',})

Погонять в песочнице.


Роуты


Представим, что сеошник поймал вас в тёмном переулке и заставил сделать ему "человекопонятные" урлы вида /snjat-dvushku/s-remontom/v-vihino. Не делайте резких движений, а медленно соберите ему регулярку:


const translit = char_only( latin_only, '-' )const place = repeat_greedy( translit )const action = from({ rent: 'snjat', buy: 'kupit' })const repaired = from( 's-remontom' )const rooms = from({    one_room: 'odnushku',    two_room: 'dvushku',    any_room: 'kvartiru',})const route = from([    begin,    '/', {action}, '-', {rooms},    [ '/', {repaired} ],    [ '/v-', {place} ],    end,])

Теперь подсуньте в неё урл и получите структурированную информацию:


// `/snjat-dvushku/v-vihino`.matchAll(route).next().value.groups{    action: "snjat",    rent: "snjat",    buy: "",    rooms: "dvushku",    one_room: "",    two_room: "dvushku",    any_room: "",    repaired: "",    place: "vihino",}

А когда потребуется сгенерировать новый урл, то просто задайте группам нужные значения:


// /kupit-kvartiru/v-moskveroute.generate({    buy: true,    any_room: true,    repaired: false,    place: 'moskve',})

Если задать true, то значение будет взято из самой регулярки. А если false, то будет скипнуто вместе со всем опциональным блоком.


И пока сеошник радостно потирает руки предвкушая первое место в выдаче, незаметно достаньте телефон, вызовите полицию, а сами скройтесь в песочнице.


Как это работает?


Нативные именованные группы, как мы выяснили ранее, не компонуются. Попадётся вам 2 регулярки с одинаковыми именами групп и всё, поехали за костылями. Поэтому при генерации регулярки используются анонимные группы. Но в каждую регулярку просовывается массив groups со списком имён:


// time.source == "((\d{2}):(\d{2}))"// time.groups == [ 'time', 'hours', 'minutes' ]const time = from({    time: [        { hours: repeat( decimal_only, 2 ) },        ':',        { minutes: repeat( decimal_only, 2 ) },    ],)

Наследуемся, переопределям exec и добавляем пост-процессинг результата с формированием в нём объекта groups вида:


{    time: '12:34',    hours: '12,    minutes: '34',}

И всё бы хорошо, да только если скомпоновать с нативной регуляркой, содержащей анонимные группы, но не содержащей имён групп, то всё поедет:


// time.source == "((\d{2}):(\d{2}))"// time.groups == [ 'time', 'minutes' ]const time = wrong_from({    time: [        /(\d{2})/,        ':',        { minutes: repeat( decimal_only, 2 ) },    ],)

{    time: '12:34',    hours: '34,    minutes: undefined,}

Чтобы такого не происходило, при композиции с обычной нативной регуляркой, нужно "замерить" сколько в ней объявлено групп и дать им искусственные имена "0", "1" и тд. Сделать это не сложно достаточно поправить регулярку, чтобы она точно совпала с пустой строкой, и посчитать число возвращённых групп:


new RegExp( '|' + regexp.source ).exec('').length - 1

И всё бы хорошо, да только String..match и String..matchAll клали шуруп на наш чудесный exec. Однако, их можно научить уму разуму, переопределив для регулярки методы Symbol.match и Symbol.matchAll. Например:


*[Symbol.matchAll] (str:string) {    const index = this.lastIndex    this.lastIndex = 0    while ( this.lastIndex < str.length ) {        const found = this.exec(str)        if( !found ) break        yield found    }    this.lastIndex = index}

И всё бы хорошо, да только тайпскрипт всё равно не поймёт, какие в регулярке есть именованные группы:


interface RegExpMatchArray {    groups?: {        [key: string]: string    }}

Что ж, активируем режим обезьянки и поправим это недоразумение:


interface String {    match< RE extends RegExp >( regexp: RE ): ReturnType<        RE[ typeof Symbol.match ]    >    matchAll< RE extends RegExp >( regexp: RE ): ReturnType<        RE[ typeof Symbol.matchAll ]    >}

Теперь TypeScript будет брать типы для groups из переданной регулярки, а не использовать какие-то свои захардкоженные.


Ещё из интересного там есть рекурсивное слияние типов групп, но это уже совсем другая история.


Напутствие



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


Подробнее..

Чему равно выражение -33u3 на С? Не угадаете. Ответ -4. Приглашаю на небольшое расследование

04.05.2021 18:23:50 | Автор: admin

Вот пример для проверки:

#include <iostream>int main(){    std::cout << "-3/3u*3 = " << int(-3/3u*3) << "\n";}

Посмотреть результат можно тут.

Или попробуйте поиграться с этим примером здесь или здесь.

Вообще-то мне не удалось найти хоть какой-то компилятор С++, который бы выдавал результат отличный от -4. Даже старый GCC-4.1.2, clang-3.0.0 или Borland C 1992 года. Так же заметил, что результат одинаковый и для константы, вычисляемой в момент компиляции и для времени выполнения.

Предлагаю внимательно рассмотреть результат выражения -3/3u*3.

Если убрать приведение к типу intв примере выше, то получим 4294967292 или 0xFFFFFFFС(-4). Получается, что компилятор на самом деле считает результат беззнаковым и равным 4294967292. До этого момента я был свято уверен, что если в выражении используется знаковый тип, то и результат будет знаковым. Логично же это.

Если посмотреть откуда берется -4 вместо -3, посмотрим внимательней на ассемблерный код примера, например здесь.

Пример изменю, чтобы результат вычислялся не в момент компиляции, а в момент выполнения:

int main(){    volatile unsigned B = 3;    int A = -3/B*3;}

Для x86-64 clang 12.0.0 видим, что используется беззнаковое деление, хотя числитель откровенно отрицательное -3:

        mov     dword ptr [rbp - 4], 3    // B = 3        mov     ecx, dword ptr [rbp - 4]        mov     eax, 4294967293        xor     edx, edx        div     ecx                       // беззнаковое деление !!        imul    eax, eax, 3               // знаковое умножение        mov     dword ptr [rbp - 8], eax

Для x64 msvc v19.28 тот же подход к делению:

        mov     DWORD PTR B$[rsp], 3      // B = 3        mov     eax, DWORD PTR B$[rsp]        mov     DWORD PTR tv64[rsp], eax        xor     edx, edx        mov     eax, -3                             ; fffffffdH        mov     ecx, DWORD PTR tv64[rsp]        div     ecx        imul    eax, eax, 3        mov     DWORD PTR A$[rsp], eax

Получается, что для деления беззнакового числа на знаковое используется БЕЗЗНАКОВАЯ операция деления процессора div. Кстати, следующая команда процессора, это правильное знаковое умножение imul. Ну явный баг компилятора. Банальная логика же подсказывает, что знаковый тип выиграет в приведении типа результата выражения если оба знаковый и беззнаковый типы используются в выражении. И для знакового деления требуется знаковая команда деления процессора idiv, чтоб получить правильный результат со знаком.

Проблема еще и в том, что число 4294967293 не делится на 3 без остатка:4294967293 = 1431655764 * 3 + 1и при умножении 1431655764 обратно на 3, получаем 4294967292 или -4. Так что прикинуться веником и считать, что 4294967293 это то же -3, только вид сбоку, для операции деления не прокатит.

Двоично-дополнительное придставление отрицательных чисел.

Благодаря представлению чисел в двоично-дополнительном виде, операции сложения или вычитания над знаковыми и без-знаковыми числами выполняются одной и той же командой процессора (add для сложения и sub для вычитания). Процессор складывает (или вычитает) только знаковое со знаковым или только без-знаковое с без-знаковым. И для обоих этих операций используется одна команда add (или sub) и побитово результат будет одинаковый (если бы кто-то решил сделать раздельные операции сложения для знаковых и без-знаковых типов). Различие только во флагах процессора. Так что считать знаковое без-знаковым и складывать их оба как без-знаковых корректно и результат будет побитово правильным в обоих случаях. Но для деления и умножения этот подход в корне неправильный. Процессор внутри использует только без-знаковые числа для деления и умножения и результат приводит обратно в знаковое с правильным признаком знака. И для этого процессор использует разные команды для знакового (idiv) и без-знакового деления (div) и так же и для умножения (imul и соответственно mul).

Я когда обнаружил, что используется без-знаковое деление, решил, что это бага компилятора. Протестировал много компиляторов: msvc, gcc, clang. Все показали такой же результат, даже древние трудяги. Но мне довольно быстро подсказали, что это поведение описано и закреплено в самом стандарте.

Действительно, стандарт говорит об этом прямо:

Otherwise, if the unsigned operand's conversion rank is greater or equal to the conversion rank of" "the signed operand, the signed operand is converted to the unsigned operand's type.

Иначе, если ранг преобразования без-знакового операнда больше или равен рангу преобразования знакового операнда, то знаковый операнд приводится к типу без-знакового операнда.

Вот где оказывается собака зарыта: "the signed operand is converted to the unsigned operand's type"!! Ну почему, почему, Карл!! Логичнее наоборот: "the unsigned operand is converted to the signed operand's type", разумеется при соблюдении ранга преобразования. Ну вот как -3 представить без-знаковым числом?? Наоборот кстати можно.

Интересная получается сегрегация по знаковому признаку!

Заметил, что это правило почему-то работает только для операции деления, а операция умножения вычисляется правильно.

Проверим на ассемблере здесь этот пример:

int main(){    volatile unsigned B = 3;    int C = -3*B;}
Вот ассемблерный код:

mov dword ptr [rbp - 4], 3 mov eax, dword ptr [rbp - 4] imul eax, eax, 4294967293 mov dword ptr [rbp - 8], eax

Стандарт ничего не говорит о неприменимости этого правила для операции умножения. И деление и умножение должны быть БЕЗЗНАКОВМИ.

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

Ага! Наивный!

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

Хоть это исследование и было больше года назад, я до сих пор под впечатлением от многих вещей в этой истории:

  • Как я не натыкался на это раньше? Не один десяток лет интенсивно кодирую на С и С++ с погружением в ассемблер, но только сейчас споткнулся на неё. Хотя может и натыкался ранее, но не мог поверить что причина именно в этом.

  • Ассемблер я выучил раньше С и поэтому всегда считал С удобной заменой Ассемблера. Но правила работы процессоров неизменны и даже языки высокого уровня им следуют.

Интересно, что это правило всё же не работает и для операций с указателями. С ними нет такого поведения. Видать понимают разработчики компиляторов, что иначе рухнет очень многое. Посмотрите этот пример здесь:

int main(){    const unsigned a[] = {3,4,5,6,7};    unsigned p = (&a[0] - &a[3])/3u*3;    // -3    unsigned b = -3/3u*3;   // -4}

Хоть я и понимаю, что могу ошибаться в логике работы этого мира, но задумайтесь, в следующий раз садясь в современный, нашпигованный вычислительной логикой самолёт (или автомобиль), а не сработает ли вдруг не оттестированный кусок кода в какой-то редкой нештатной ситуации, и не выдаст ли он -4 вместо элементарных -3, и не постигнет ли его участь подобная Boeing 737 MAX?

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

Ошибка в команде просессора FDIV у Интела

Помните, в начале 2000-х была выявлена ошибка с вычислением в команде FDIV у Интела. Там было различие в 5 знаке после запятой в какой-то операции деления. Какой был шум тогда!!
Но исправили оперативно и быстро. В компиляторы добавили условный флаг для обхода этой команды. Интел срочно исправил логику в кристалле и выпустил новые чипы.

И это всего лишь 5-й знак после запятой! Многие его даже и не заметили, подумаешь, мелочь какая! А тут -4 вместо -3 и считаем знаковое без-знаковым и вместо -3 имеем еще и 4294967292! И тишина в ответ! И в этой тишине тихо падают Боинги.

Предвижу возражение, что есть рекомендация использовать преимущественно без-знаковый тип. Понимаю, с таким правилом Стандарта по другому никак. Но как -4 в беззнаковое перевести, и чтоб всё остальное работало? И почему на других языках и всех процессорах не надо этими танцами с бубном заниматься, а в С++ надо? И что делать с константными выражениями, вычисляемыми компилятором? Вместо элегантных выражений, в которых мы уверены, надо записывать их с оглядкой, чтоб у компилятора небыло изжоги!

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

Как хорошую подсказку познавательно добавить предупреждение в компилятор когда он применяет это правило из Стандарта:"Signed value is intentionally converted to unsigned value. Sorry for crashing one more airplane. Have a nice flight!" Вот удивимся тогда, как мало мы тестируем и как много нам открытий чудных приносит компилятор друг.

Можно еще исправить Стандарт, ограничив правило только операциями сложения и вычитания. Как компромис. Но это крайне маловероятно в этой Вселенной. Да и Боингов еще много летает.

Представьте студента (С) на экзамене у преподавателя по информатике (П) в ВУЗе.

- П: Хорошо, последний вопрос на 5. Можно ли привести знаковое число к беззнаковому типу?
- С: Хе, можно. НУЖНО! Обязательно НУЖНО! Ставте 5, я пойду.
- П: Как НУЖНО?? О_О. Подумайте. Как можно представить, например, -4 беззнаковым числом? - С: Чего тут думать! Стандарт С++ сказал, что НУЖНО, значит НУЖНО и точка. А то что -4 станет беззнаковым очень большим числом - это уже ни печалька Стандарта, ни моя. - П: Подумайте еще раз. Вы на экзамене по информатике и вас спрашивают о базовых вещах, которые общие для всех языков программирования и процессоров, а не только для С++. - С: Чего вы пристали к мне со своими языками и процессорами. У меня в билете вопрос про С++, вот я про С++ и отвечаю. А вы про какой то там Ассемблер, базовые вещи, языки программирования! Стандарт С++ сказал, компилятор сделал, я ответил! У вас есть вопросы к Стандарту про базовые вещи, вот ему их и задавайте, а я ответил правильно! - П: Да уж. Подстава конкретная.

Подробнее..

Перевод Золотой век программирования окончен?

17.04.2021 12:09:16 | Автор: admin

В наши дни кажется, что все, что вам нужно, это подписаться на Twitter или выступить на TEDx, чтобы назвать себя футурологом. Но Тим ОРейли - основатель легендарной компании по обучению технологиям OReilly Media - не такой гуру. Его вдумчивые наблюдения за тенденциями в области вычислительной техники и его поддержка открытого исходного кода (задолго до того, как это считалось крутым) дают ему должное уважение.

Поэтому, когда Тим О'Рейли недавно высказал мнение про будущее индустрии программного обеспечения, профессиональные разработчики обратили на это внимание. Вот что он сказал:

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

Звучит логично. Вы, вероятно, слышали другие версии аргумента сегодняшние элитные навыки программирования - это базовая компетенция завтрашнего дня. Но действительно ли это отражает будущее индустрии программного обеспечения?

Кодирование как грамотность

Трудно спорить с целью повышения грамотности кода. Даже если вы не планируете писать собственное программное обеспечение, есть реальная ценность в понимании того, как работает код. Даже непрограммисты могут использовать базовые навыки, необходимые для создания веб-сайта, делать выводы из массива данных или автоматизировать простые задачи. И кто не проводит день в теплых объятиях алгоритмов Google, Facebook или YouTube? Программист или нет, понимание того, как работают машины вокруг нас, является важным аспектом, когда они формируют нашу жизнь.

Идея о том, что написание кода является частью этого нового типа грамотности, который в конечном итоге превратится из почти магической силы в обычную человеческую практику, - не новость. Но есть основания для подозрений. В конце концов, всего десять лет назад мы искали научную грамотность с целью дать рядовым гражданам знания, необходимые им для принятия взвешенных решений и выбора лидеров, которые могли бы проложить разумный курс в быстро меняющемся мире. Но обещания научной грамотности не оправдались. Сегодня средний человек в значительной степени полагается на современную науку, которой он не понимает или не верит, от компьютеров (квантовая механика) до вакцин (генетика) и GPS (общая теория относительности Эйнштейна). Если научная грамотность - это цель, которая никогда не была реализована, можем ли мы действительно ожидать, что грамотность в области программирования будет намного лучше?

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

Наши усилия по повышению грамотности в программировании на удивление мало подходят для подготовки будущих программистов. Фактически, мы часто уводим новичков от искусства программирования в крошечные ограниченные песочницы. В прошлом эти песочницы представляли собой макросы Excel и (если у вас были немного больше амбиций) Visual Basic и Access. Теперь это Power Apps. Чем больше меняются вещи, тем больше они остаются неизменными - если вы хотите стать профессиональным программистом, вам все равно нужно отклоняться от стандартного пути.

Современное образование снизило планку грамотности. Но это не изменило определение того, что значит быть профессиональным программистом.

Чего стоит программист?

Другая часть комментария Тима ОРейли более тонкая. Проще говоря, если грамотность в программировании растет, угрожает ли это привилегированному месту программистов в рабочей силе? Некоторые из сегодняшних программистов помнят, как им велели изучать что-нибудь более прочное, например математику, потому что нужда в навыках программирования был близок к сокращению. Но есть и обратная сторона: предсказание без временной шкалы на самом деле вовсе не предсказание. Сегодня у нас больше программистов, чем когда-либо прежде, и армия иностранных рабочих, стремящихся удовлетворить наши ИТ-потребности. Нет недостатка в программистах. Но не хватает квалифицированных программистов. И этот дефицит оказался на удивление долговечным. Даже несмотря на то, что мы расширили традиционное образование, продвигая предметы STEM глубже в учебную программу, и нетрадиционное образование (со взрывом учебных курсов по программированию на основе проектов), нехватка квалифицированных разработчиков осталась неизменной. По текущим оценкам, нехватка талантливых программистов в ближайшие годы будет только расти. Легко предположить, что нехватка квалифицированных программистов отражает недостаток опыта в новых и появляющихся технологиях, таких как машинное обучение и аналитика больших данных. И хотя это правда, что разработчики в этих областях пользуются большим спросом, это не похоже на источник дефицита. Вместо этого не хватает квалифицированных разработчиков среднего уровня. Это люди, которые владеют основами программирования, но также понимают эффективную командную работу, сотрудничество и другие полезные навыки.

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

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

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

Приход нового золотого века

Цитата Тима О'Рейли намекает на будущее, в котором навыки программирования не станут залогом успешной карьеры. Но что, если золотой век вычислительной техники не про легкие деньги и обеспеченность работой? Что, если то, что мы теряем, на самом деле менее важно, чем то, что мы приобрели за полвека разработки программного обеспечения?

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

Сегодня, с появлением веб-сайтов с бесплатными учебными пособиями, курсами YouTube, стримерами по программированию Twitch и StackOverflow, барьеры для входа практически исчезли. Вместо стены тайных знаний, отпугивающей новичков, теперь у нас есть сообщество экспертов, которые приглашают новичков и готовы поделиться своей работой над проектами с открытым исходным кодом. Если мы выберем золотой век, измерив трение, мешающее нашим идеям, это будет золотой век.

В истории не было времени, когда было бы проще превратить идею в программный проект и поделиться им со всем миром.

Мы не можем сказать, когда закончится золотой век, но одно можно сказать наверняка. Двери королевства по-прежнему открыты настежь.

Подробнее..

Перевод Почему принципы SOLID не являются надежным решением для разработки программного обеспечения

05.05.2021 14:10:44 | Автор: admin
Фото от https://unsplash.com/@lazycreekimagesФото от https://unsplash.com/@lazycreekimages

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

Фактически, объектно-ориентированное программирование работает лучше всего, когда мы можем отделить то, что останется, от того, что изменится.Принципы SOLID помогают разделять это.

Мне лично нравится идея, лежащая в основе принципов SOLID и я многому из нее научился.

Тем не менее

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

Чтобы проиллюстрировать, как эта проблема влияет на принципы SOLID, давайте рассмотрим каждый из принципов.

Принцип единственной ответственности

Уклассане должно быть более одной причины дляизменения.Другими словами, у каждого класса должна быть только одна ответственность.

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

class Calculate {    fun add (a, b) = a + b    fun sub (a, b) = a - b    fun mul (a, b) = a * b    fun div (a, b) = a / b }

Для некоторых это идеально, так как у него одна обязанность - вычислить.

Но кто-то может возразить: Эй!Он делает 4 вещи!Сложить, вычесть, умножить и разделить!

Кто прав?Я скажу, это зависит от обстоятельств.

Если ваша программа использует толькоCalculate для выполнения вычислений, тогда все хорошо.Дальнейшее его абстрагирование было бы чрезмерной инженерией.

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

Определение единственной ответственности зависит от контекста программы и может меняться со временем.Мы можем следовать принципу сейчас, но не навсегда.

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

Программные объекты ... должны быть открыты для расширения, но закрыты для модификации.

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

Давайте посмотрим на код ниже:

interface Operation {   fun compute(v1: Int, v2: Int): Int}class Add:Operation {   override fun compute(v1: Int, v2: Int) = v1 + v2}class Sub:Operation {   override fun compute(v1: Int, v2: Int) = v1 - v2}class Calculator {   fun calculate(op: Operation, v1: Int, v2: Int): Int {      return op.compute(v1, v2)   } }

В приведённом выше коде есть классCalculator, который принимает объект Operation для вычисления.Мы можем легко расширить этот класс с помощью операций Mul и Div без изменения кода самого классаCalculator.

class Mul:Operation {   override fun compute(v1: Int, v2: Int) = v1 * v2}class Div:Operation {   override fun compute(v1: Int, v2: Int) = v1 / v2}

Отлично, мы соблюдаем принцип открытости/закрытости!

Но однажды появилось новое требование. Cкажем, нам нужна новая операция Inverse.Она просто возьмет один операнд, например X, и вернет результат 1/X.

Кто на земле мог подумать, что это произойдет?Мы исправили функцию сompute нашего рабочего интерфейса, чтобы она имела 2 параметра.Теперь нам нужна новая операция, у которой всего 1 параметр.

Как теперь избежать модификации класса Calculator?Если бы мы знали это заранее, возможно, мы не писали бы наш класс калькулятора и интерфейс операций как таковые.

Изменения никогда нельзя полностью спланировать.Если бы это можно было полностью спланировать, возможно, программное обеспечение нам больше и не понадобилось бы :)

Принцип подстановки Лискоу

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

Когда мы были молоды, мы узнавали основные атрибуты животных.Они подвижны.

interface Animal {   fun move()}class Mammal: Animal {   override move() = "walk"}class Bird: Animal {   override move() = "fly"}class Fish: Animal {   override move() = "swim"}fun howItMove(animal: Animal) {   animal.move()}

Это соответствует принципу замены Лискоу.

Но мы знаем, что сказанное выше не совсем правильно.Некоторые млекопитающие плавают, некоторые летают, а некоторые птицы ходят.Итак, мы меняем код на:

class WalkingAnimal: Animal {   override move() = "walk"}class FlyingAnimal: Animal {   override move() = "fly"}class SwimmingAnimal: Animal {   override move() = "swim"}

Круто, все по-прежнему хорошо, так как наша функция все еще может использовать Animal:

fun howItMove(animal: Animal) {   animal.move()}

Но сегодня я кое-что обнаружил.Есть животные, которые вообще не двигаются.Они называются Sessile.Может нам стоит изменить код так:

interface Animalinterface MovingAnimal: Animal {   move()}class Sessile: Animal {}

Теперь это нарушит приведенный ниже код.

fun howItMove(animal: Animal) {   animal.move()}

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

Даже в реальном мире существует множество исключений.Мир программного обеспечения - это не реальный мир.Все возможно.

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

Многие клиентские интерфейсы лучше, чем один интерфейс общего назначения.

Давайте посмотрим на животное царство.У нас есть интерфейс Animal, как показано ниже.

interface Animal {   fun move()   fun eat()   fun grow()   fun reproduction()}

Однако, как мы поняли выше, есть некоторые животные, которые не двигаются, и это Sessile.Поэтому мы должны выделить функциюmove как отдельный интерфейс.

interface Animal {   fun eat()   fun grow()   fun reproduction()}interface MovingObject {   fun move()}class Sessile : Animal {   //...}class NonSessile : Animal, MovingObject {   //...}

Затем мы хотели бы иметь еще и PlantВозможно, нам следует отделитьgrowиreproduction:

interface LivingObject {   fun grow()   fun reproduction()}interface Plant: LivingObject {   fun makeFood()}interface Animal: LivingObject {   fun eat()}interface MovingObject {   fun move()}class Sessile : Animal {   //...}class NonSessile : Animal, MovingObject {   //...}

Мы довольны тем, что выделяем как можно больше клиентских интерфейсов.Это похоже на идеальное решение.

Однако, кто-то начинает кричать: Дискриминация!Некоторые животные бесплодны, это не значит, что они больше не LivingObject!.

Похоже, теперь нам нужно отделитreproductionот интерфейсаLivingObject.

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

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

Принцип инверсии зависимостей

Положитесь на абстракции, а не на что-то конкретное.

Мне нравится этот принцип, поскольку он относительно универсален.Он применяет идеюконцепции независимого решения, в которой некоторая сущность в программе хотя и зависит от класса, но по-прежнему независима от него.

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

Давайте посмотрим на пример ниже.Он действительно применяет принцип инверсии зависимостей.

interface Operation {    fun compute (v1: Int, v2: Int): Int    fun name (): String }class Add: Operation {    override fun compute (v1: Int, v2: Int) = v1 + v2    override fun name () = "Add" }class Sub: Operation {    override fun compute (v1: Int, v2: Int) = v1 - v2    override fun name () = "Subtract" }class Calculator {    fun calculate (op: Operation, v1: Int, v2: Int): Int {       println ("Running $ {op.name ()}")       return op.compute (v1, v2)    } }

Calculator Не зависит отAdd илиSub.Новместо этогоон выполняетAdd иSub , которые зависят отOperation.Это выглядит хорошо.

Однако, если кто-то из группы разработчиков Android использует его, это будет проблемой.println не работает в Android.Нам понадобитсяLod.d взамен.

Чтобы решить эту проблему, мы должны сделатьCalculator независящим напрямую от println.Вместо этого мы должны внедрить интерфейс Printer:

interface Printer {   fun print(msg: String)}class AndroidPrinter: Printer {   override fun print(msg: String) = Log.d("TAG", msg)}class NormalPrinter: Printer {   override fun print(msg: String) = println(msg)}class Calculator(val printer: Printer) {   fun calculate(op: Operation, v1: Int, v2: Int): Int {      printer.print("Running ${op.name()}")      return op.compute(v1, v2)   } }

Это решает проблему соблюдения принципа инверсии зависимостей.

Но если Android никогда не будет использовать этотCalculator, и мы создадим такой интерфейс заранее, возможно, мы нарушилиYAGNI.


TL; DR;

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

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

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

Программное обеспечение по своей природе МЯГКОЕ и сделать его навсегда следующим SOLID сложно.Для программного обеспечения применение принципов SOLID - это цель, а не судьба.

Подробнее..

Перевод Thunderbird, RNP и важность хорошего API

12.05.2021 14:11:23 | Автор: admin


Недавно мне довелось побеседовать с разработчикомThunderbirdо проектировании API. В ходе этой беседы я поделился соображениями о RNP,новой реализации OpenPGP, которую Thunderbird недавно стал использовать вместо GnuPG.

Собеседник скептически отнесся к моему тезису о том, что API RNP нуждается в улучшении, и спросил, разве это не субъективно какие API лучше, а какие хуже?. Согласен, у нас нет хороших метрик для оценки API. Но не соглашусь, что мы в принципе не в силах судить об API.

На самом деле, подозреваю, что большинство опытных программистов узнают плохой API, если увидят его. Думаю, далее в этой статье получится разработать хорошую эвристику, которую я попытаюсь выстроить на моем собственном опыте работы с (и над) GnuPG,Sequoia и RNP. Затем я рассмотрю API RNP. К сожалению, этот API не только можно запросто использовать неправильно он к тому же обманчив, поэтому пока его не следует использовать в контекстах, где принципиальная роль отводится соблюдению безопасности. Но целевая аудитория Thunderbird это люди, известные своей уязвимостью, в частности, журналисты, активисты, юристы и их партнеры, отвечающие за коммуникацию; все эти люди нуждаются в защите. На мой взгляд, это означает, что в Thunderbird должны лишний раз подумать, стоит ли использовать RNP.

Примечание: также предлагаю ознакомиться с этим электронным письмом:Lets Use GPL Libraries in Thunderbird!, которое я отправилв постовую рассылку по планированию развития Thunderbird.

Каковы черты плохого API?


Прежде, чем мы вместе с Юстусом и Каем приступили к проектуSequoia, мы все втроем работали над GnuPG. Мы не только сами копались в gpg, но и беседовали, и сотрудничали со многими последующими пользователями gpg. Люди смогли сказатьмного хорошего по поводу GnuPG.


Что касается критикиgpg, наиболее значительными нам показались два вида замечаний по поводу API. Первое сводится к следующему: API gpg слишком догматичен. Например, вgpgприменяется подход, основанный на использовании личной базы ключей (keyring). Таким образом, просмотреть или использовать сертификат OpenPGP можно лишь в том случае, если он импортирован в личную базу ключей. Но некоторые разработчики желают сначала просмотреть сертификат, и лишь потом импортировать его. Например, при поиске сертификата на сервере ключей по его отпечатку, можно проверить и убедиться, что возвращенный сертификат действительно тот, что нужен, поскольку его URL является самоаутентифицируемым. Это можно сделать при помощи gpg, но только обходным путем, огибая принципы той модели программирования, которая в него заложена. Базовая идея такова: создать временный каталог, добавить в него конфигурационный файл, приказать gpgиспользовать альтернативный каталог, импортировать туда сертификат, проверить сертификат, после чего очистить временный каталог. Это официальная рекомендация, добавленнаяЮстусомна основе наших бесед с последующими пользователями gpg. Да, этот метод работает. Но для него требуется писать код, специфичный для операционной системы, этот код медленный, и в нем часто заводятся ошибки.

Другой класс замечаний, с которыми мы сталкивались неоднократно, заключается в том, что для работы с gpgтребуется знать массу неочевидных вещей чтобы не злоупотреблять этим механизмом. Или, выражаясь иначе, нужно проявлять крайнюю осмотрительность при использовании APIgpg, чтобы ненароком не привнести в код уязвимость.

Чтобы лучше понять второй повод для беспокойства, рассмотрим уязвимости EFAIL. Основная проблема, связанная с API дешифрования gpg: при дешифровании сообщенияgpgвыдает обычный текст, даже если ввод был поврежден.gpgв таком случае действительно возвращает ошибку, но некоторые программы все равно выводят обычный текст в поврежденном виде. Так как, почему нет? Определенно, лучше показать хотя бы часть сообщения, чем не показать ничего, верно? Так вот, уязвимости EFAIL демонстрируют, как злоумышленник может этим воспользоваться, чтобы внедрить веб-багв зашифрованное сообщение. Когда пользователь просматривает это сообщение, веб-баг просачивается из сообщения. Уф.

Итак, на чьей совести этот баг? РазработчикиGnuPGнастаивали, чтопроблема на уровне приложений, в том, что они используют gpgнеправильно:

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

gpgпросигнализировала об ошибке; приложения не соблюдают контракт API. Должен согласиться с разработчиками GnuPG и добавить: интерфейсgpg был (и остается) бомбой замедленного действия, поскольку не подсказывает пользователю, как действовать правильно. Напротив, легкое и, казалось бы, полезное действие является неверным. ИAPI такоготипа, к сожалению, обычны в GnuPG.

Из чего слагается хороший API?


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

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

Типы


Покажу на примере, как мы используем типы в Sequoia, и как они помогают нам сделать хороший API. Чтобы пример был понятнее, полезно будет напомнить некоторый контекст, касающийся OpenPGP.


Простой сертификат OpenPGP

В OpenPGP существует несколько фундаментальных типов данных, а именно: сертификаты, компоненты (например, ключи и пользовательские ID), а также подписи привязки. Корень сертификата это первичный ключ, полностью определяющий отпечаток сертификата (fingerprint = Hash(primary key)). В состав сертификата обычно входят такие компоненты как подключи и пользовательские ID. OpenPGP привязывает компонент к сертификату при помощи так называемой подписи привязки. Когда мы используем в качестве отпечатка обычный хеш первичного ключа и используем подписи для привязки компонентов к первичному ключу, создаются условия, чтобы впоследствии можно было добавить и дополнительные компоненты. В состав подписей привязки также входят свойства. Поэтому есть возможность изменить компонент, например, продлить срок действия подключа. Вследствие этого с конкретным компонентом может быть ассоциировано несколько действительных подписей. Подписи привязки являются не только фундаментальной, но и неотъемлемой частью механизма безопасности OpenPGP.

Поскольку может существовать множество действительных подписей привязки, требуется способ выбирать из них нужную. В качестве первого приближения предположим, что нужная нам подпись самая недавняя, неистекшая, неотозванная действительная подпись, создание которой не отложено на будущее. Но что такое действительная подпись? В Sequoia подпись должна не только пройти математическую проверку, но и согласовываться с политикой. Например, в силу противодействия скомпрометированным коллизиям, мыдопускаем SHA-1 только в очень небольшом количестве ситуаций. (Пол Шауб, работающий надPGPainless, недавноподробно написал об этих сложностях.) Вынуждая пользователя API держать в уме все эти соображения, мы создаем почву для уязвимостей. В Sequoia легкий способ получить время истечения это безопасный способ. Рассмотрим следующий код, который работает как надо:

let p = &StandardPolicy::new(); let cert = Cert::from_str(CERT)?;for k in cert.with_policy(p, None)?.keys().subkeys() {    println!("Key {}: expiry: {}",             k.fingerprint(),             if let Some(t) = k.key_expiration_time() {                 DateTime::<Utc>::from(t).to_rfc3339()             } else {                 "never".into()             });}


cert это сертификат. Начинаем с применения политики к нему. (Политики определяются пользователем, но, как правило,StandardPolicyне только достаточна, но и наиболее уместна). Фактически здесь создается представление сертификата, в котором видны только компоненты с действительной подписью привязки. Важно, что она также изменяет и предоставляет ряд новых методов. Метод keys, к примеру, изменен так, что возвращаетValidKeyAmalgamationвместоKeyAmalgamation. (Это слияние, так как результат включает не только Key, но и все подписи, связанные с ним; некоторые считают, что этот процесс было бы удачнее назвать Катамари.\_()_/) УValidKeyAmalgamationесть действительная подпись привязки, согласно вышеприведенным критериям. А также предоставляет такие методы как key_expiration_time, что имеет смысл только с действительным ключом! Также отметим: возвращаемый тип, используемый сkey_expiration_time, эргономичен. Вместо того, чтобы возвращать необработанное значение, key_expiration_timeвозвращает SystemTime, безопасный и удобный в работе.

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

Примеры


Релиз 1.0библиотеки Sequoia состоялся в декабре 2020 года. За девять месяцев до того мы вышли на ситуацию полной работоспособности (feature complete) и были готовы к релизу. Но выжидали. Следующие девять месяцев ушли у нас на добавление документации и примеров к публичному API. Посмотрите документацию к структуре данныхCertв качестве примера, посмотрите, что у нас получилось. Как указано в нашем посте, нам не удалось предоставить примеры для всех функций до одной, но мы успели довольно много. В качестве бонуса к написанию примеров мы также успели найти несколько шероховатостей, которые при этом отполировали.

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

API RNP


RNP это свежая реализация OpenPGP, разработанная преимущественно Ribose. Примернодва года назад в Thunderbird решили интегрироватьEnigmailв Thunderbird и одновременно с этимзаменить GnuPG на RNP. Тот факт, что в Thunderbird выбрали RNP не только лестен для RNP; он также означает, что RNP стал, пожалуй, самой востребованной реализацией OpenPGP для шифрования почты.

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

Инфраструктура с критически важными требованиями к безопасности


К сожалению, RNP пока не дошла до состояния в котором, на мой взгляд, ее можно безопасно развертывать. Enigmail пользовались не только люди, озабоченные приватностью своих данных, но и журналисты, активисты и адвокаты, которым важна собственная безопасность и безопасность их собеседников. В интервью, данном в 2017 году, Бенджамин Исмаил, глава Азиатско-Тихоокеанского отделения организации Репортеры без границ, сказал:

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

Интервью с Бенджамином Исмаиломиз организацииРепортеры без границ

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

RNP и подписи привязки подключа


Говоря о том, как мы используем типы в Sequoia для того, чтобы усложнить неправильное использование API, я показал, как узнать срок истечения действия ключа, написав всего несколько строк кода. Хотел начать с примера, демонстрирующего человеку, неискушенному в OpenPGP или RNP, как тот же самый функционал можно реализовать при помощи RNP. Следующий код перебирает подключи сертификата (key) и выводит на экран срок истечения действия каждого подключа. Напоминаю: время истечения действия хранится в подписи привязки подключа, а значение 0свидетельствует, что срок действия ключа не истечет никогда.

int i;for (i = 0; i < sk_count; i ++) {  rnp_key_handle_t sk;  err = rnp_key_get_subkey_at(key, i, &sk);  if (err) {    printf("rnp_key_get_subkey_at(%d): %x\n", i, err);    return 1;  }   uint32_t expiration_time;  err = rnp_key_get_expiration(sk, &expiration_time);  if (err) {    printf("#%d (%s). rnp_key_get_expiration: %x\n",           i + 1, desc[i], err);  } else {    printf("#%d (%s) expires %"PRIu32" seconds after key's creation time.\n",           i + 1, desc[i],           expiration_time);  }}

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

#1 (doesn't expire) expires 0 seconds after key's creation time.#2 (expires) expires 94670781 seconds after key's creation time.#3 (expired) expires 86400 seconds after key's creation time.#4 (invalid sig) expires 0 seconds after key's creation time.#5 (no sig) expires 0 seconds after key's creation time.


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

Получить время истечения срока действия ключа в секундах.Обратите внимание: 0 означает, что ключ не истечет никогда.

Поскольку время истечения срока действия ключа сохранено в подписи привязки, я, эксперт по OpenPGP, понимаю это так: вызов кrnp_key_get_expirationувенчается успехом лишь в том случае, если у подключа есть действительная подпись привязки. На самом же деле оказывается, что, если действительная подпись привязки отсутствует, то функция просто принимает по умолчанию значение 0, что, учитывая вышеприведенное замечание, пользователь API ожидаемо интерпретирует так: данный ключ действует бессрочно.

Чтобы улучшить этот код, сначала необходимо проверить, есть ли у ключа действительная подпись привязки. Некоторые функции, делающие именно это, недавно были добавлены в RNP для решения проблемыCVE-2021-23991. В частности, разработчики RNP добавили функциюrnp_key_is_valid, чтобы возвращать информацию о том, действителен ли ключ. Это дополнение улучшает ситуацию, но требует от разработчика явно выбирать, должны ли проводиться эти критичные для безопасности проверки (а не явно отказываться от уже заданных проверок как было бы в случае работы с Sequoia). Поскольку проверки безопасности не относятся к выполнению полезной работы, о них легко забыть: код работает, даже если проверка безопасности проведена не была. А поскольку чтобы правильно выбрать, что проверять, нужны экспертные знания, о проверках забывают.

Следующий код предусматривает проверку безопасности и пропускает любые ключи, которые rnp_key_is_validсочтет недействительными:

int i;for (i = 0; i < sk_count; i ++) {  rnp_key_handle_t sk;  err = rnp_key_get_subkey_at(key, i, &sk);  if (err) {    printf("rnp_key_get_subkey_at(%d): %x\n", i, err);    return 1;  }   bool is_valid = false;  err = rnp_key_is_valid(sk, &is_valid);  if (err) {    printf("rnp_key_is_valid: %x\n", err);    return 1;  }   if (! is_valid) {    printf("#%d (%s) is invalid, skipping.\n",           i + 1, desc[i]);    continue;  }   uint32_t expiration_time;  err = rnp_key_get_expiration(sk, &expiration_time);  if (err) {    printf("#%d (%s). rnp_key_get_expiration: %x\n",           i + 1, desc[i], err);  } else {    printf("#%d (%s) expires %"PRIu32" seconds after key's creation time.\n",           i + 1, desc[i],           expiration_time);  }}


Вывод:

#1 (doesn't expire) expires 0 seconds after key's creation time.#2 (expires) expires 94670781 seconds after key's creation time.#3 (expired) is invalid, skipping.#4 (invalid sig) is invalid, skipping.#5 (no sig) is invalid, skipping.


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

Хотя бывает и так, что мы не хотим использовать тот ключ или сертификат, срок действия которого истек, иногда мы к ним прибегаем. Например, если пользователь забудет продлить срок действия ключа, то у него должна быть возможность увидеть, что ключ истек, и тогда проверить сертификат, а также продлить в таком случае срок действия ключа. Хотя gpg --list-keysне показывает истекшие ключи, при редактировании сертификата все-таки видны истекшие подключи, так, чтобы пользователь мог продлить срок их действия:

$ gpg --edit-key 93D3A2B8DF67CE4B674999B807A5D8589F2492F9Secret key is available.sec ed25519/07A5D8589F2492F9created: 2021-04-26 expires: 2024-04-26 usage: Ctrust: unknown    validity: unknownssb ed25519/1E2F512A0FE99515created: 2021-04-27 expires: never    usage: Sssb cv25519/8CDDC2BC5EEB61A3created: 2021-04-26 expires: 2024-04-26 usage: Essb ed25519/142D550E6E6DF02Ecreated: 2021-04-26 expired: 2021-04-27 usage: S[ unknown] (1). Alice <alice@example.org>


Есть и другие ситуации, в которых истекший ключ не должен считаться недействительным. Предположим, например, что Алиса посылает Бобу подписанное сообщение: я заплачу тебе 100 евро за год, а срок действия ключа подписи истекает через полгода. Когда год закончится, будет ли Алиса должна Бобу, если исходить из этой подписи? По-моему, да. Подпись была действительна, когда ставилась. Тот факт, что срок действия ключа уже истек, не имеет значения. Разумеется, когда ключ истек, подписи, скрепленные им после момента его истечения, должны считаться недействительными. Аналогично, сообщение не следует шифровать истекшим ключом.

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

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

int i;for (i = 0; i < sk_count; i ++) {  rnp_key_handle_t sk;  err = rnp_key_get_subkey_at(key, i, &sk);  if (err) {    printf("rnp_key_get_subkey_at(%d): %x\n", i, err);    return 1;  }   uint32_t valid_till;  err = rnp_key_valid_till(sk, &valid_till);  if (err) {    printf("rnp_key_valid_till: %x\n", err);    return 1;  }   printf("#%d (%s) valid till %"PRIu32" seconds after epoch; ",         i + 1, desc[i], valid_till);   if (valid_till == 0) {    printf("invalid, skipping.\n");    continue;  }   uint32_t expiration_time;  err = rnp_key_get_expiration(sk, &expiration_time);  if (err) {    printf("rnp_key_get_expiration: %x\n", err);  } else {    printf("expires %"PRIu32" seconds after key's creation time.\n",           expiration_time);  }}


Результаты:

#1 (doesn't expire) valid till 1714111110 seconds after epoch; expires 0 seconds after key's creation time.#2 (expires) valid till 1714111110 seconds after epoch; expires 94670781 seconds after key's creation time.#3 (expired) valid till 1619527593 seconds after epoch; expires 86400 seconds after key's creation time.#4 (invalid sig) valid till 0 seconds after epoch; invalid, skipping.#5 (no sig) valid till 0 seconds after epoch; invalid, skipping.


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

Но давайте подробнее рассмотримrnp_key_valid_till. Во-первых, в OpenPGP время истечения ключа хранится как беззнаковый 32-разрядный отступ от времени создания ключа, также в беззнаковом 32-разрядном формате. Следовательно, функция должна была бы использовать более широкий тип или, как минимум, проверять код на переполнение. (Ясообщил об этой проблеме, и ее уже исправили.)

Но, даже если игнорировать этот косяк, функция все равно странная. В OpenPGP ключ может быть действителен в течение нескольких периодов времени. Допустим, срок действия ключа истекает 1 июля, а пользователь продлевает его только с 10 июля. В период с 1 по 10 июля ключ был недействителен, а подписи, сгенерированные в это время, также должны считаться недействительными. Итак, что же должна возвращать рассматриваемая функция для такого ключа? Гораздо важнее, как пользователь такого API должен интерпретировать результат? Уместно ли вообще использовать такой API? (Да, я спрашивал.)

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

Не подумайте, что я специально придираюсь именно к этой проблеме с API RNP. Это просто сложность, о которой я недавно размышлял. Когда мы заново реализовали API RNP, чтобы создать альтернативный бэкенд OpenPGPдля Thunderbird, мы столкнулисьсо многими подобными проблемами.

Заключение


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

Тем не менее, API RNP опасен. А Thunderbirdиспользуетсяв контекстах с критическими требованиями к безопасности. В интервью от 2017 годаМихал Рысьек Вознякиз Центра по исследованию коррупции и организованной преступности (OCCRP) четко сообщил, что на кону чьи-то жизни:

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

ИнтервьюсМихалом Рысьеком Вознякомиз Центра по исследованию коррупции и организованной преступности

Как это отразится на Thunderbird? Вижу три варианта. Во-первых, Thunderbird мог бы переключиться обратно на Enigmail. Можно подумать, что портирование Enigmail на Thunderbird 78 далось бы сложно, но я слышал от многих разработчиков Thunderbird, что это технически осуществимо вполне подъемными усилиями. Но одна из причин, по которым Thunderbird предпочла уйти от Enigmail огромное время, которое разработчикам Enigmail приходилось тратить, чтобы помочь пользователям правильно установить и сконфигурировать GnuPG. Поэтому такой путь неидеален.

Во-вторых, Thunderbird могла бы переключиться на иную реализацию OpenPGP. В наше время ихцелая кучана выбор. Лично я считаю, что Thunderbird следовало бы переключиться на Sequoia. Конечно же, я разработчик Sequoia, поэтому необъективен. Но дело здесь не в деньгах: мне платит фонд, а на свободном рынке мне предложили бы, пожалуй, вдвое больше, чем я зарабатываю сейчас. Я работаю ради того, чтобы защитить пользователей. Но, даже кроме API Sequoia и преимуществ реализации, Thunderbird в данном случае выигрывает и еще в одном отношении: мы уже заставили эту реализацию работать. Несколько недель назад мы выпустили Octopus, альтернативный бекенд OpenPGP для Thunderbird. У него не только функциональный паритет с RNP, но и есть ряд ранее недостававших фич, например, интеграция с gpg, а также залатаны некоторые бреши в безопасности и выполнено несколько нефункциональных требований.

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



VPS от Маклауд идеально подходят для разработки API.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Перевод Почему в мире так много отстойного ПО

17.05.2021 14:19:21 | Автор: admin
Мы буквально окружены отстойным программным обеспечением. Пенсионные фонды спотыкаются об написанные десятки лет назад пакетные скрипты с ошибочными допущениями. Из кредитных организаций утекает более сотни миллионов номеров социального обеспечения и других конфиденциальных данных. И это ещё не говоря о куче забагованного и раздражающего ПО, создаваемых и мелкими поставщиками, и крупными корпорациями.

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

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


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

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

И наконец, когда разработчики достигают определённого порога навыков, они пересекают его
и попадают в третью категорию. В категорию, где каждый достиг такого высокого уровня компетентности (относительно решаемой ими задачи), что дальнейший личный рост минимально отразится на конечном продукте. Например, любой случайно выбранный инженер из Google может создать CRUD-приложение столь же качественно, как и Джефф Дин.



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

Однако между нами и этой утопией стоят две проблемы.

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

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

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

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



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

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

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

И наконец, есть профессии, которые сложно освоить, и с множеством возможностей, но имеющие нормативные входные барьеры. Например, врачебная деятельность. В мире до Obamacare было много людей, которые не могли позволить себе страхование здоровья и с радостью бы выбрали хоть какое-нибудь здравоохранение вместо полного его отсутствия (стоит ли так делать это уже другой сложный вопрос). Тем не менее, из-за строгих правил предоставлять медицинские услуги могут только доказавшие высокую компетентность. Аналогична ситуация и во многих инженерных дисциплинах, подвергающихся сильному нормативному контролю, например, в строительстве мостов и небоскрёбов, а также в производстве медицинского оборудования.



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

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

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

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



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

Кроме того, в нём рассматривается только та часть уравнения, которая относится к разработчикам. Менеджеры и CEO, не дающие разработчикам достаточно времени для создания безупречного ПО и предпочитающие выпустить нечто сносное ещё одна серьёзная причина проблемы, которую мы исследуем в другой статье.

image
Подробнее..

Разукрашиваем вывод в консоли теория и практика

23.05.2021 14:09:40 | Автор: admin


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


Управляющие последовательности ANSI


ANSI escape sequences или Управляющие последовательности ANSI это стандарт, дающий возможность управлять курсором, цветами, начертание в текстовых консолях. Такие последовательности воспринимаются отрисовщиком терминала, как команды отображать последующий текст в определенном формате. Есть также последовательность, которая сбрасывает предыдущие команды, и отображение текста становиться обычным. Существует несколько форматов управляющих последовательностей, различающихся возможностями и появившимися в разных версиях кодировок. Поговорим об этих форматах подробнее.


8 основных цветов и стили


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


Чтобы изменить текущий цвет шрифта или фона можно использовать следущий синтаксис:


  • Начинается управляющая последовательность с любого из этих трёх представлений: \x1b[ (hex) или \u001b[ (Unicode) или \033[ (oct)
  • Далее следуют аргументы, разделённые между собой ;(можно указывать в любом порядке)
  • В конце ставится буква m

Возможные аргументы


  • Изменения стиля


    Модификатор Код
    1 Жирный
    2 Блеклый
    3 Курсив
    4 Подчёркнутый
    5 Мигание
    9 Зачёркнутый

  • Изменения цвета шрифта


    Цвет Код
    30 Чёрный
    31 Красный
    32 Зелёный
    33 Жёлтый
    34 Синий
    35 Фиолетовый
    36 Бирюзовый
    37 Белый

  • Изменения цвета фона


    Цвет Код
    40 Чёрный
    41 Красный
    42 Зелёный
    43 Жёлтый
    44 Синий
    45 Фиолетовый
    46 Бирюзовый
    47 Белый


Бонус: другие интересные модификаторы, которые могут поддерживаться не всеми платформами


Модификатор Код
38 RGB цвет (см. раздел "Совсем много цветов")
21 Двойное подчёркивание
51 Обрамлённый
52 Окружённый
53 Надчёркнутый

Пример корректного синтаксиса: \033[3;36;44m. После вывода этой конструкции стиль будет изменён для всего последующего текста. Чтобы вернуться к изначальному состоянию можно использовать \033[0m, тогда весь текст с этого места вернётся к изначальному форматированию.


Давайте поэкспементируем. Для примеров я буду использовать Python.



Важно заметить, что форматирование повлияло и на консоль питона, а не только на ее вывод. Именно поэтому очень важно закрывать все "тэги" изменения форматирования.


Часто используемые сочетания (copy-paste-able)


Код Описание
\033[0m вернуться к начальному стилю
\033[31m <your text goes here> \033[0m красный текст для обозначения ошибок
\033[1;31m <your text goes here> \033[0m жирный красный текст для обозначения критических ошибок
\033[32m <your text goes here> \033[0m зеленый текст успешное выполнение
\033[3;31m <your text goes here> \033[0m красный курсив текст ошибки
\033[43m <your text goes here> \033[0m выделение основного, как будто жёлтым маркером

Больше цветов: аж целых 256


Некоторые терминалы поддерживают вывод целых 256 цветов. Если команда echo $TERM выводит xterm-256color, то ваш терминал всё корректно обработает.


В этом формате синтаксис немного другой:



Для генерации кодов цветов можно использовать генератор.


А палитру доступных цветов можно увидеть на картинке ниже.


Палитра цветов


Совсем много цветов


Этот формат не всегда поддерживается стандартными консолями.


Некотрые будут негодовать: "256 цветов и нет моего любимого терракотового, какой ужас!". Для таких ценителей существует формат, который уже поддерживает 24 битные цвета (3 канала RGB по 256 градаций).
Для не ценителей поясню, что терракотовый кодируется как (201, 100, 59) или #c9643b.
Синтаксис в этом формате выглядит вот так:


  • \033[38;2;r;g;bm цвет текста
  • \033[48;2;r;g;bm цвет фона


Python: Использование библиотеки Colorama


Библиотека Colorama позволяет форматировать текст, не запоминая коды цветов. Рассмотрим её использование на примере:


from colorama import init, Fore, Back, Styleinit()print(Fore.RED + 'some red text\n' + Back.YELLOW + 'and with a yellow background')print(Style.DIM + 'and in dim text\n' + Style.RESET_ALL + 'back to normal now')

Вывод программы:



Style позволяет изменить стиль, Fore цвет шрифта, Back цвет фона. Использовать переменные из colorama нужно также, как и коды изменения стиля. Но плюс использования библиотеки в том, что Fore.RED более читаем, чем \033[0;31m


Если в colorama.init() указать параметр autoreset=True, то стиль будет автоматически сбрасываться (в конец каждого print будут добавлены сбрасывающие стили последовательности), поэтому вам не придётся об этом каждый раз вспоминать.


А что не так с Windows?


Просто так синтаксис, описанный в начале статьи, не работает в командной строке Windows. Поддержка цветов появлялась постепенно, причём в странном варианте. В начале программы надо сделать системный вызов, активирующий отрисовку цветов. А в более старых версиях необходимо заменить все ANSI последовательности на системные вызовы.


Но colorama.init() сделает всё за вас в большинстве версий Windows. Однако если вы используете другую операционную систему, то функцию init() вызывать в начале программы не обязательно. Также некоторые IDE на Windows (например, PyCharm) тоже поддерживают цвета без каких-либо махинаций.
А еще Windows не поддерживает многие модификаторы, такие как жирный текст. Подробнее можно почитать на странице Colorama


Termcolor


Ещё одна библиотека для вывода цветного текста с более удачным, на мой взлгяд, синтаксисом.


from termcolor import colored, cprinttext = colored('Hello, Habr!', 'red', attrs=['blink'])print(text)cprint('Hello, Habr!', 'green', 'on_red')


Кстати, проблему с Windows всё ещё можно починить с помощью colorama.init()


Выводы


Стандартные 8 цветов позволяют разнообразить вывод в консоль и расставить акценты. 256 цветов намного расширяют возможности, хотя и поддерживаются не всеми консолями. Windows, к сожалению, не поддерживает многие основные модификаторы, например, курсив. Также есть некоторые цвета, которые не прописаны в стандартах, но могут поддерживаться вашей операционной системой. Если вы хотите больше цветов, то вы можете поискать их в Гугле.
Пока что не любой терминал поддерживает 24-битные цвета и все модификаторы, но мы вряд ли увидим сильные изменения в этой сфере. Так что пока нам остаётся выбирать самые красивые варианты из тех, что доступны в любимом терминале.


Источники





Облачные серверы от Маклауд быстрые и безопасные.


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Перевод Практическое руководство по именованию классов, функций и переменных

24.05.2021 00:09:13 | Автор: admin

Перевод сделан для Hexlet.io

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

Придумывать названия сложно!

В этой статье мы сосредоточимся на методе именования (P)A/HC/LC для того, чтобы улучшить читаемость кода. Эти рекомендации можно применить к любому языку программирования, в статье для примеров кода используется JavaScript.

Что значит (P)A/HC/LC?

В этой практике используется следующий шаблон для именования функции:

префикс? (P) + действие (A) + высокоуровневый контекст (HC) + низкоуровневый контекст? (LC)

Что обозначает префикс (P)?

Префиксрасширяет смысл функции.

- is

Описывает свойство или состояние текущего контекста (обычно логическое значение).

const color = 'blue';const isBlue = (color === 'blue'); // свойствоconst isPresent = true; // состояниеif (isBlue && isPresent) {  console.log('Blue is present!');}
- has

Указывает, имеет ли текущий контекст определенное значение или состояние (обычно логическое значение).

/* Плохо */const isProductsExist = (productsCount > 0);const areProductsPresent = (productsCount > 0);/* Хорошо */const hasProducts = (productsCount > 0);
- should

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

const shouldUpdateUrl = (url, expectedUrl) => url !== expectedUrl;

Читайте также:

Цикл статей Совершенный код. Ошибки именования в программировании от Кирилла Мокевнина

Действие это сердце функции

Действие это глагольная часть имени функции. Это самая важная часть в описании того, что делает функция.

- get

Получает доступ к данным немедленно (сокращение от getter для внутренних данных).

function getFruitsCount() {  return this.fruits.length;}
- set

Безусловно присваивает переменной со значением A значение B.

let fruits = 0;const setFruits = (nextFruits) => {  fruits = nextFruits;};setFruits(5);console.log(fruits); // 5
- reset

Возвращает переменную к её начальному значению или состоянию.

const initialFruits = 5;let fruits = initialFruits;setFruits(10);console.log(fruits); // 10const resetFruits = () => {  fruits = initialFruits;};resetFruits();console.log(fruits); // 5
- fetch

Выполняет запрос данных, для которого требуется время (например, асинхронный запрос).

const fetchPosts = (postCount) => fetch('https://api.dev/posts', {...});
- remove

Удаляет что-то откуда-то.

Например, если у вас есть коллекция выбранных фильтров на странице поиска, удаление одного из них из коллекции этоremoveFilter, а неdeleteFilter(именно так вы и скажете на английском языке):

const removeFilter = (filterName, filters) => filters.filter((name) => name !== filterName);const selectedFilters = ['price', 'availability', 'size'];removeFilter('price', selectedFilters);
- delete

Полностью стирает что-то. После операции сущность перестаёт существовать.

Представьте, что вы редактор контента, и есть пост, от которого вы хотите избавиться. Как только вы нажали на кнопку delete-post, CMS выполнила действиеdeletePost, а неremovePost.

const deletePost = (id) => database.find({ id }).delete();
- compose

Создает новые данные из существующих. Обычно это применимо к строкам, объектам или функциям.

const composePageUrl = (pageName, pageId) => </span><span style="box-sizing: border-box; background-color: rgb(248, 248, 248);">${</span><span style="box-sizing: border-box; background-color: rgb(248, 248, 248);">pageName</span><span style="box-sizing: border-box; background-color: rgb(248, 248, 248);">.</span><span style="box-sizing: border-box; background-color: rgb(248, 248, 248);">toLowerCase</span><span style="box-sizing: border-box; background-color: rgb(248, 248, 248);">()}</span><span style="box-sizing: border-box; color: rgb(221, 17, 68);">-</span><span style="box-sizing: border-box; background-color: rgb(248, 248, 248);">${</span><span style="box-sizing: border-box; background-color: rgb(248, 248, 248);">pageId</span><span style="box-sizing: border-box; background-color: rgb(248, 248, 248);">}</span><span style="box-sizing: border-box; color: rgb(221, 17, 68);">;
- handle

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

const handleLinkClick = () => {  console.log('Clicked a link!');};link.addEventListener('click', handleLinkClick);

Контекст

Контекст это область, с которой работает функция.

Функция это часто действие с чем-то. Важно указать, какова её рабочая область или, по крайней мере, ожидаемый тип данных.

/* Чистая функция, работающая с примитивами */const filter = (list, predicate) => list.filter(predicate);/* Функция, работающая непосредственно с сообщениями */const getRecentPosts = (posts) => filter(posts, (post) => post.date === Date.now());/*Некоторые специфические для языка допущения позволяют опустить контекст.Например, в JavaScript фильтр обычно работает с массивом (Array).Добавление явного filterArray будет избыточным.*/

В итоге

Пять принципов именования переменных

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

1. Следуйте S-I-D

Имя должно быть коротким (Short), интуитивно понятным (Intuitive) и описательным (Descriptive).

/* Плохо */const a = 5; // "a" может обозначать что угодноconst isPaginatable = (postsCount > 10); // "Paginatable" звучит крайне неестественноconst shouldPaginatize = (postsCount > 10); // Придуманные глаголы - это так весело!/* Хорошо */const postsCount = 5;const hasPagination = (postsCount > 10);const shouldDisplayPagination = (postsCount > 10); // альтернатива

2. Избегайте сокращений

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

/* Плохо */const onItmClk = () => {};/* Хорошо */const onItemClick = () => {};

3. Избегайте дублирования контекста

Всегда удаляйте контекст из имени, если это не снижает его читабельность.

class MenuItem {  /* Имя метода дублирует контекст (которым является "MenuItem") */  handleMenuItemClick(event) {    ...  }  /* Читается как MenuItem.handleClick() */  handleClick(event) {    ...  }}

4. Отражайте в имени ожидаемый результат

/* Плохо */const isEnabled = (itemsCount > 3);/* Хорошо */const isDisabled = (itemsCount <= 3);

5. Учитывайте единственное/множественное число

Как и префикс, имена переменных могут быть единственного или множественного числа в зависимости от того, имеют ли они одно значение или несколько.

/* Плохо */const friends = 'Bob';const friend = ['Bob', 'Tony', 'Tanya'];/* Хорошо */const friend = 'Bob';const friends = ['Bob', 'Tony', 'Tanya'];

6. Используйте осмысленные и произносимые имена

/* Просто ужасно */const yyyymmdstr = moment().format("YYYY/MM/DD");/* Гораздо лучше */const currentDate = moment().format("YYYY/MM/DD");
Подробнее..

Принцип подстановки Барбары Лисков (предусловия и постусловия)

28.05.2021 00:20:41 | Автор: admin

Почему у многих возникают проблемы с этим принципом? Если взять не заумное, а более простое определение, то оно звучит так:

Наследующий класс должен дополнять, а не замещать поведение базового класса.

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

В данной статье мы НЕ будем рассматривать общие примеры данного принципа, о которых уже есть много материалов (пример с квадратом и прямоугольником или управления термостатами). Здесь мы немного подробнее остановимся на таких понятиях как Предусловия, Постусловия, рассмотрим что такое ковариантность, контравариантность и инвариантность, а также что такое исторические ограничения или правило истории.

Предусловия не могут быть усилены в подклассе

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

<?phpclass Customer{    protected float $account = 0;    public function putMoneyIntoAccount(int|float $sum): void    {        if ($sum < 1) {            throw new Exception('Вы не можете положить на счёт меньше 1$');        }        $this->account += $sum;    }}class  MicroCustomer extends Customer{    public function putMoneyIntoAccount(int|float $sum): void    {        if ($sum < 1) {            throw new Exception('Вы не можете положить на счёт меньше 1$');        }        // Усиление предусловий        if ($sum > 100) {             throw new Exception('Вы не можете положить на больше 100$');        }        $this->account += $sum;    }}

Добавление второго условия как раз является усилением. Так делать не надо!

К предусловиям также следует отнести Контравариантность, она касается параметров функции, которые может ожидать подкласс.

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

Этот пример показывает, как расширение допускается, потому что метод Bar->process() принимает все типы параметров, которые принимает метод в родительском классе.

<?phpclass Foo{    public function process(int|float $value)    {       // some code    }}class Bar extends Foo{    public function process(int|float|string $value)    {        // some code    }}

Пример ниже показывает, как дочерний класс VIPCustomer может принимать в аргумент переопределяемого метода putMoneyIntoAccount более широкий (более абстрактный) объект Money, чем в его родительском методе (принимает Dollars).

<?phpclass Money {}class Dollars extends Money {}class Customer{    protected Money $account;    public function putMoneyIntoAccount(Dollars $sum): void    {        $this->account = $sum;    }}class VIPCustomer extends Customer{    public function putMoneyIntoAccount(Money $sum): void    {        $this->account = $sum;    }}

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

Постусловия не могут быть ослаблены в подклассе

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

<?phpclass Customer{    protected Dollars $account;    public function chargeMoney(Dollars $sum): float    {        $result = $this->account - $sum->getAmount();        if ($result < 0) { // Постусловие            throw new Exception();        }        return $result;    }}class  VIPCustomer extends Customer{    public function chargeMoney(Dollars $sum): float    {        $result = $this->account - $sum->getAmount();        if ($sum < 1000) { // Добавлено новое поведение            $result -= 5;          }               // Пропущено постусловие базового класса              return $result;    }}

Условное выражение проверяющее результат является постусловием в базовом классе, а в наследнике его уже нет. Не делай так!

Сюда-же можно отнести и Ковариантность, которая позволяет объявлять в методе дочернего класса типом возвращаемого значения подтип того типа (ШО?!), который возвращает родительский метод.

На примере будет проще. Здесь в методе render() дочернего класса, JpgImage объявлен типом возвращаемого значения, который в свою очередь является подтипом Image, который возвращает метод родительского класса Renderer.

<?phpclass Image {}class JpgImage extends Image {}class Renderer{    public function render(): Image    {    }}class PhotoRenderer extends Renderer{    public function render(): JpgImage    {    }}

Таким образом в дочернем классе мы сузили возвращаемое значение. Не ослабили. Усилили :)

Инвариантность

Здесь должно быть чуть проще.

Все условия базового класса - также должны быть сохранены и в подклассе.

Инварианты это некоторые условия, которые остаются истинными на протяжении всей жизни объекта. Как правило, инварианты передают внутреннее состояние объекта.

Например типы свойств базового класса не должны изменяться в дочернем.

<?php class Wallet{    protected float $amount;    // тип данного свойства не должен изменяться в подклассе}

Здесь также стоит упомянуть исторические ограничения (правило истории):

Подкласс не должен создавать новых мутаторов свойств базового класса.

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

<?phpclass Deposit{    protected float $account = 0;    public function __construct(float $sum)    {        if ($sum < 0) {            throw new Exception('Сумма вклада не может быть меньше нуля');        }        $this->account += $sum;    }}class VipDeposit extends Deposit{    public function getMoney(float $sum)    {        $this->account -= $sum;    }}

С точки зрения класса Deposit поле не может быть меньше нуля. А вот производный класс VipDeposit, добавляет метод для изменения свойства account, поэтому инвариант класса Deposit нарушается. Такого поведения следует избегать.

В таком случае стоит рассмотреть добавление мутатора в базовый класс.

Выводы

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

Стоит упомянуть, что нужно страться избавляться от пред/пост условий. В идеале они должны быть определенны как входные/выходные параметры метода (например передачей в сигнатуру готовых value objects и возвращением конкретного валидного объекта на выход).

Надеюсь, было полезно.

Источники

  1. Вики - Принцип подстановки Барбары Лисков

  2. Metanit

  3. PHP.watch

  4. Telegram канал, с короткими заметками

Подробнее..

Перевод Актуальность принципов SOLID

05.06.2021 22:17:23 | Автор: admin

Впервые принципы SOLID были представлены в 2000 году в статье Design Principles and Design Patterns Роберта Мартина, также известного как Дядюшка Боб.

С тех пор прошло два десятилетия. Возникает вопрос - релевантны ли эти принципы до сих пор?

Перед вами перевод статьи Дядюшки Боба, опубликованной в октябре 2020 года, в которой он рассуждает об актуальности принципов SOLID для современной разработки.

Недавно я получил письмо с примерно следующими соображениями:

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

Принцип подстановки Лисков давно устарел, потому что мы уже не уделяем столько внимания наследованию, сколько уделяли 20 лет назад. Думаю, нам стоит рассмотреть позицию Дена Норса о SOLID - Пишите простой код

В ответ я написал следующее письмо.

Принципы SOLID сегодня остаются такими же актуальными, как они были 20 лет назад (и до этого). Потому что программное обеспечение не особо изменилось за все эти годы, а это в свою очередь следствие того, что программное обеспечение не особо изменилось с 1945 года, когда Тьюринг написал первые строки кода для электронного компьютера. Программное обеспечение - это все еще операторы if, циклы while и операции присваивания - Последовательность, Выбор, Итерация.

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

Итак, пройдемся по принципам по порядку.

SRP - Single Responsibility Principle Принцип единственной ответственности.

Объединяйте вещи, изменяющиеся по одним причинам. Разделяйте вещи, изменяющиеся по разным причинам.

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

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

Слайды Дена Норса полностью упускают этот момент и убеждают меня в том, что он вообще не понимает сам принцип (либо иронизирует, что более вероятно предположить, зная Дена). Его ответ на SRP - Пишите простой код. Я согласен. SRP - один из способов сохранять код простым.

OSP - Open-Closed Principle Принцип открытости-закрытости

Модуль должен быть открытым для расширения, но закрытым для изменения.

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

Или... хотим ли мы отделить абстрактные понятия от деталей реализации? Хотим ли мы изолировать бизнес-правила от надоедливых мелких деталей графического интерфейса, протоколов связи микросервисов и тонкостей поведения различных баз данных? Конечно хотим!

И снова слайд Дэна преподносит это совершенно неправильно.

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

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

LSP - Liskov Substitution Principle Принцип подстановки Лисков

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

Люди (включая меня) допустили ошибку, полагая что речь идет о наследовании. Это не так. Речь о подтипах. Все реализации интерфейсов являются подтипами интерфейса, в том числе при утиной типизации. Каждый пользователь базового интерфейса, объявлен этот интерфейс или подразумевается, должен согласиться с его смыслом. Если реализация сбивает с толку пользователя базового типа, то будут множиться операторы if/switch.

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

Слайды Дэна по этой теме полностью верны, он просто упустил суть принципа. Простой код - это код, который поддерживает четкие отношения подтипов.

ISP - Interface Segregation Principle Принцип разделения интерфейса

Интерфейсы должны быть краткими, чтобы пользователи не зависели от ненужных им вещей.

Мы по-прежнему работаем с компилируемыми языками. Мы все еще зависим от дат изменений, чтобы определить, какие модули должны быть перекомпилированы и перевыпущены. Покуда это справедливо, нам придется столкнуться с проблемой - когда модуль A зависит от модуля B во время компиляции, но не во время выполнения, изменения в модуле B ведут к перекомпиляции и перевыпуску модуля A.

Проблема особенно остро стоит в статически типизированных языках, таких как Java, C#, C++, GO, Swift и т.д. Динамически типизированные языки страдают гораздо меньше, но тоже не застрахованы от этого - существование Maven и Leiningen тому доказательство.

Слайд Дэна на эту тему ошибочен.

(Примечание. На слайде Ден обесценивает утверждение Клиенты не должны зависеть от методов, которые они не используют фразой Это же и так правда!!)

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

(Примечание. Речь о фразе Если классу нужно много интерфейсов - упрощайте класс!)

Да, если вы можете разбить класс с двумя интерфейсами на два отдельных класса, то это хорошая идея (SRP). Но такое разделение часто недостижимо и даже нежелательно.

DIP - Dependency Inversion Principle Принцип инверсии зависимостей

Направляйте зависимости согласно абстракциям. Высокоуровневые модули не должны зависеть от низкоуровневых деталей.

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

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

Лучший способ создать путаницу - сказать всем будьте проще и не дать никаких дальнейших инструкций.

Подробнее..

Перевод Про комментарии к коду

16.06.2021 00:20:01 | Автор: admin
image

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

Javadoc самый бесполезный


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

image

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

Самодокументируемый код


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

Когда комментировать


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

image

Еще один пример:

image

Часто можно услышать совет пишие комменте ПОЧЕМУ, а не ЧТО. Хотя это, вероятно, охватывает большинство моих комментариев, это не то, как я думаю о том, когда комментировать. Вместо этого я обычно пишу комментарий, когда есть что-то особенно сложное, либо в домене, либо в том, как выполняется реализация.

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

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

Логи как комментарии


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

image

Анализ комментариев


Когда я впервые подумал о том, чтобы проверить, сколько комментариев содержится во всех моих коммитах, я подумал, что будет достаточно одной строки, чтобы найти комментарии во всех моих коммитах Python (я комментирую только с помощью #):

git log --author=Henrik -p|grep '^+[^+]'|grep '#' | wc -l

Однако вскоре я понял, что мне нужны более подробные сведения. Я хотел провести различие между комментариями в конце строки и комментариями всей строки. Я также хотел узнать, сколько блоков комментариев (последовательных строк комментариев) у меня было. Я также решил исключить тестовые файлы из анализа. Кроме того, я хочу обязательно исключить любой закомментированный код, который там оказался (к сожалению, таких случаев было несколько). В конце концов я написал скрипт на python для анализа. Входными данными для скрипта были выходные данные git log --author=Henrik -p.

Из выходных данных я увидел, что 1299 из 17817 добавленных строк моих содержали комментарии. Был 161 комментарий в конце строки и 464 однострочных комментария. Самый длинный блок комментариев составлял 11 строк, и было 96 случаев блоков комментариев, которые имели 3 или более последовательных строк.

Выводы


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

В поисках упорядоченного множества в Python разбираемся с теорией и выбираем лучшую реализацию

24.05.2021 10:16:39 | Автор: admin


Множество (Set) структура данных, которая позволяет достаточно быстро (в зависимости от реализации) применить операции add, erase и is_in_set. Но иногда этого не достаточно: например, невозможно перебрать все элементы в порядке возрастания, получить следующий / предыдущий по величине или быстро узнать, сколько элементов меньше данного есть в множестве. В таких случаях приходится использовать Упорядоченное множество (ordered_set). О том, как оно работает, и какие реализации есть для питона далее.


Стандартный Set


В языке Python есть стандартная стукрура set, реализованная с помощью хэш-таблиц. Такую структуру обычно называют unordered_set. Данный метод работает так: каждый элемент присваивается какому-то классу элементов (например, класс элементов, имеющих одинаковый остаток от деления на модуль). Все элементы каждого класса хранятся в одтельном списке. В таком случае мы заранее знаем, в каком списке должен находиться элемент, и можем за короткое время выполнить необходимые операции. Равновероятность каждого остатка от деления случайного числа на модуль позволяет сказать, что к каждому классу элементов будет относиться в среднем size / modulo элементов.


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


Что есть в других языках


В языке c++ есть структура std::set, которая поддерживает операции изменения, проверку на наличие, следующий / предыдущий по величине элемент, а также for по всем элементам. Но тут нет операций получения элемента по индексу и индекса по значению, так что надо искать дальше (индекс элемента количество элементов, строго меньших данного)


И решение находится достаточно быстро: tree из pb_ds. Эта структура в дополнение к возможностям std::set имеет быстрые операции find_by_order и order_of_key, так что эта структура именно то, что мы ищем.


Она реализована с помощью красно-чёрных деревьев. Смысл этой струкруты в том, что все элементы образуют собой двоичное дерево поиска, которое балансируется так, чтобы высота не превышала логарифм. Нам это даёт возможность с помощью одного спуска по дереву выполнить необходимые операции. Также с этой задачей может справиться Декартово дерево (Дерамида) по неявному ключу или AVL дерево.


Таким образом, целью этой статьи станет поиск аналога этой структуры в Python.


Как будем тестировать скорость работы структур данных


Для оценки времени работы я написал программу, которая будет выполнять последовательно несколько типов операций:


  1. Добавление в множество миллиона случайных чисел (при данном сиде среди них будет 999'936 различных)
  2. Проверка миллиона случайных чисел на присутствие в множестве
  3. Прохождение циклом по всем элементам в порядке возрастания
  4. В случайном порядке для каждого элемента массива узнать его индекс (а, соответственно, и количество элементов, меньше данного)
  5. Получение значения i-того по возрастанию элемента для миллиона случайных индексов
  6. Удаление всех элементов множества в случайном порядке

from SomePackage import ordered_setimport randomimport timerandom.seed(12345678)numbers = ordered_set()# adding 10 ** 6 random elements - 999936 uniquelast_time = time.time()for _ in range(10 ** 6):    numbers.add(random.randint(1, 10 ** 10))print("Addition time:", round(time.time() - last_time, 3))# checking is element in set for 10 ** 6 random numberslast_time = time.time()for _ in range(10 ** 6):    is_element_in_set = random.randint(1, 10 ** 10) in numbersprint("Checking time:", round(time.time() - last_time, 3))# for all elementslast_time = time.time()for elem in numbers:    now_elem = elemprint("Cycle time:", round(time.time() - last_time, 3))# getting index for all elementslast_time = time.time()requests = list(numbers)random.shuffle(requests)for elem in requests:    answer = numbers.index(elem)print("Getting indexes time:", round(time.time() - last_time, 3))# getting elements by indexes 10 ** 6 timesrequests = list(numbers)random.shuffle(requests)last_time = time.time()for _ in range(10 ** 6):    answer = numbers[random.randint(0, len(numbers) - 1)]print("Getting elements time:", round(time.time() - last_time, 3))# deleting all elements one by onerandom.shuffle(requests)last_time = time.time()for elem in requests:    numbers.discard(elem)print("Deleting time:", round(time.time() - last_time, 3))

SortedSet.sorted_set.SortedSet


Пакет с многообещающим названием. Используем pip install sortedset


К сожалению, автор не приготовил нам функцию add и erase в каком-либо варианте, поэтому будем использовать объединение и вычитание множеств


Использование:


from SortedSet.sorted_set import SortedSet as ordered_setnumbers = ordered_set()numbers |= ordered_set([random.randint(1, 10 ** 10)])  # добавлениеnumbers -= ordered_set([elem])  # удаление

Протестируем пока на множествах размера 10'000:


Задача Время работы
Добавление 16.413
Проверка на наличие 0.018
Цикл по всем элементам 0.001
Получение индексов 0.008
Получение значений по индексам 0.015
Удаление 30.548

Как так получилось? Давайте загляем в исходный код:


def __init__(self, items=None):    self._items = sorted(set(items)) if items is not None else []def __contains__(self, item):    index = bisect_left(self._items, item)

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


Вывод: почти бесполезно, несколько строчек кода завернули в класс


sortedcontainers.SortedSet


Внеший пакет, для установки можно использовать pip install sortedcontainers. Посмотрим же, что он нам покажет


Задача Время работы
Добавление 3.924
Проверка на наличие 1.198
Цикл по всем элементам 0.162
Получение индексов 3.959
Получение значений по индексам 4.909
Удаление 2.933

Но, не смотря на это, кажется мы нашли то, что искали! Все операции выполняются за приличное время. По сравнению с ordered_set некоторые операции выполняются дольше, но за то операция discard выполняется не за o(n), что очень важно для возможности использования этой структуры.


Также пакет нам предлагает SortedList и SortedDict, что тоже может быть полезно.


И как же оно работает?


На странице пакета мы можем прочитать, что реализована структура не так, как мы предполагали в начале статьи.


Из-за особенностей реализации языка Python, в нём быстро работают list, а также bisect.insort (найти бинарным поиском за o(log n) место, куда нужно вставить элемент, а потом вставить его туда за o(n)). Insert работает достаточно быстро на современных процессорах. Но всё-таки в какой-то момент такой оптимизации не хватает, поэтому структуры реализованы как список списков. Создание или удаление списков происходит достаточно редко, а внутри одного списка можно выполнять операции даже за быструю линию.


Если говорить кратко, то принцип действия похож на корневую оптимизацию.


Проблема с ordered_set


Что вообще такое упорядоченное множество? Это множество, в котором мы можем сравнить любые 2 элемента и найти среди них больший / меньший. В течение всей статьи под операцией сравнения воспринималась операция сравнения двух элеметнов по своему значению. Но все пакеты называющиеся ordered_set считают что один элемент больше другого, если он был добавлен раньше в множество. Так что с формулировкой ordered_set нужно быть аккуратнее и уточнять, имеется ввиду ordered set или sorted set.


Bintrees



Так есть же модуль bintrees! Это же то, что нам нужно? И да, и нет. Его разработка была приостановлена в 2020 году со словами Use sortedcontainers instead.


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


pip install bintrees


Название AVLTree говорит само за себя, RBTree красно-чёрное дерево, BinaryTree несбалансированное двоичное дерево, префикс Fast означает реализацию на Cython (соответственно, необходимо наличие Visual C++, если используется на Windows).


Задача AVLTree FastAVLTree RBTree FastRBTree BinaryTree FastBinaryTree
Добавление 21.946 2.285 20.486 2.373 11.054 2.266
Проверка на наличие 5.86 2.821 6.172 2.802 6.775 3.018
Цикл по всем элементам 0.935 0.297 0.972 0.302 0.985 0.295
Удаление 12.835 1.509 25.803 1.895 7.903 1.588

Результаты тестирования отчётливо показывают нам, почему использовать деревья поиска на Python плохая идея в плане производительности. А вот в интеграции с Cython всё становится намного лучше.


Оказывается, эта структура и SortedSet очень похожи по производительности. Все 3 Fast версии структур bintrees достаточно близки, поэтому будем считать, что оттуда мы используем FastAVLTree.


Задача SortedSet FastAVLTree
Добавление 3.924 2.285
Проверка на наличие 1.198 2.821
Цикл по всем элементам 0.162 0.297
Получение индексов 3.959 n/a
Получение значений по индексам 4.909 n/a
Удаление 2.933 1.509

Как мы видим, AVL в полтора раза быстрее в скорости добавления элементов и почти в 2 раза быстрее в операциях удаления. Но он в те же 2 раза медленнее в проверке на наличие и цикле по всем элементам. К тому же не стоит забывать, что 2 операции он выполнять не умеет, то есть не является тем ordered_set, что мы ищем.


Использование:


import bintreesnumbers = bintrees.FastAVLTree()numbers.insert(value, None)  # второй параметр - значение, как в словаре

Что же выбрать


Мои рекомендации звучат так: если вам нужны операции find_by_order и order_of_key, то ваш единственный вариант sortedcontainers.SortedSet. Если вам нужен только аналог std::map, то выбирайте на своё усмотрение между SortedSet и любым из fast контейнеров из bintrees, опираясь на то, каких операций ожидается больше.


Можно ли сделать что-то быстрее


Скорее нет, чем да. Использование Cython один из самых мощных способов оптимизации, а AVL считается очень быстрым решением исходной задачи. Про остальные операции ordered_set можно сказать, что модификация красно-чёрного дерева так, чтобы оно поддерживало эти операции, вряд ли будет быстрее SortedContainers, так что смысла изобретать велосипед я не вижу.




Облачные VPS серверы от Маклауд быстрые и безопасные.


Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!


Подробнее..

Комментарии ложь

16.04.2021 20:09:46 | Автор: admin

Если вы программист, то есть много практик, которые можно ненавидеть.

Жестко запрограммированные значения. Двойная логика. Сложные иерархии наследования. Но должны ли входить комментарии в этом списке?

Давным-давно, во времена доисторического программирования, писать комментарии считалось обязательной составляющей написания кода. Когда вы писали код, его наполняли короткие и точные комментарии. Затем чистый код и agile программирование стали править миром. Пуристы кричали о вреде комментариев, которые наполняли неструктурированный и уязвимый код. Внезапно комментарии стали ошибкой, антипаттерном, сборниками откровенной лжи.

Многие разработчики почувствовали себя загнанными в угол.

Ложный выборЛожный выбор

Но так ли очевиден выбор? Или же все-таки есть способ писать комментарии и при этом уважать себя, вставая утром? Чтобы узнать ответ, давайте рассмотрим, как комментируют ужасный код и как даже плохой комментарий может спасти жизни.

Ленивые комментарии

Самая частая ошибка, которую совершают разработчики, это комментирование кода вместо написания хорошего кода.

Например, возьмем этот фрагмент кода, взятый из реального приложения:

double delta = h*h-r1*r1;double r2 = Math.Sqrt(delta);

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

// Calculate side length using the Pythagorean Theorem// and put the value into variable "r2"double delta = h*h-r1*r1;double r2 = Math.Sqrt(delta);

Это первая проблема с комментариями - они открывают дверь вредным привычкам. Они соблазняют вас сойти с дороги непрерывного рефакторинга на простой путь описания небрежного кода.

В этом примере есть много способов избежать комментария. Вот один из возможных вариантов, где все нужное говорят имена переменных:

double lengthSideB = Math.Sqrt(  Math.Pow(hypotenuse,2) - Math.Pow(lengthSideA,2);)

Или вы можете вынести операцию в отдельный метод и назвать метод должным образом:

double sideA = Pythagoras.GetLengthOfSide(hyptenuse, sideB);

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

Ложные комментарии

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

/**  * Constructor.  *   * @param name (required) brand name of the product. Must have  * content. Length must be in range 1..50.   * @param price (optional) purchase price of the product.  * @param units (required) number of units currently in stock.  * Can not be less than 0.*/public Product(string name, decimal price, integer units){   ...}

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

Все врет

Комментарии не заслуживают доверия. Вы не найдете модульных тестов, которые могли бы гарантировать их истинность. Но это не уникальная проблема. На самом деле почти все может лгать. Типичный проект кодирования повторяется во многих различных областях. Есть требования к архитектуре, документация, схема данных - все это может противоречить друг другу. Рассмотрим документацию API. Вы можете создать его со всеми нужными типами, членами и сигнатурами методов, используя рефлексию. Здесь нет возможности солгать. Но после этого кто-то должен написать описания, которые придают смысл этим структурам кода. Тот факт, что документация API может лгать, является разочарованием для разработчиков, работающих с быстро меняющимися технологиями, но это вряд ли повод отказываться от них. С документацией проблемы не заканчиваются. Даже если вы замените каждый комментарий тщательно названными переменными и именами методов, эти имена также могут быть превращены в ложь из-за неосторожного редактирования. Другими словами, риск поверить в комментарий реален. Но не путайте риск использования ложного комментария с риском его написания.

Комментарии - ложь - говорит нам перестать доверять комментариям, а не писать их.

Комментарии не бесполезны

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

// to match ITG1's late arrows.  -KGlobalOffsetSeconds=-0.006

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

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

I spent some time this weekend looking at very well-named, very clean, uncommented code implementing a research algorithm. Im high-level familiar with it, the guy sitting next to me was the inventor, and the code was written a few years ago by someone else. We couldbarelyfollow it. Paul Nathan

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

Комментарии представляют собой страховой полис

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

  • Помните о плохих методах работы (ленивых комментариях и небрежном коде)

  • Осторожно относиться ко лжи (никому не доверять)

  • Управление мусорным комментарием (с помощью инструментов в среде IDE)

Главный вопрос прост. Стоит ли платить за комментарии? Мнения расходятся, но если вы используете комментарии вдумчиво и разумно - если вы используете их для уменьшения когнитивной нагрузки кода, а не для противодействия плохим практикам - они могут повысить ценность даже самого чистого кода.

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

Подробнее..

Перевод Компилятор всё оптимизирует? Ну уж нет

17.06.2021 12:20:28 | Автор: admin
Многие программисты считают, что компиляторы это волшебные чёрные ящики, на вход в которые можно подать хаотичный код, а на выходе получить красивый оптимизированный двоичный файл. Доморощенные философы часто начинают рассуждать о том, какие фишки языка или флаги компилятора следует использовать, чтобы раскрыть всю мощь магии компилятора. Если вы когда-нибудь видели кодовую базу GCC, то и в самом деле могли поверить, что он выполняет какие-то волшебные оптимизации, пришедшие к нам из иных миров.

Тем не менее, если вы проанализируете результаты работы компиляторов, то узнаете, что они не очень-то хорошо справляются с оптимизацией вашего кода. Не потому, что пишущие их люди не знают, как генерировать эффективные команды, а просто потому, что компиляторы способны принимать решения только в очень малой части пространства задач. [В своём докладе Data Oriented Design (2014 год) Майк Эктон сообщил, что в проанализированном фрагменте кода компилятор теоретически может оптимизировать лишь 10% задачи, а 90% он оптимизировать не имеет никакой возможности. Если бы вам интересно было узнать больше о памяти, то стоит прочитать статью What every programmer should know about memory. Если вам любопытно, какое количество тактов тратят конкретные команды процессора, то изучите таблицы команд процессоров]

Чтобы понять, почему волшебные оптимизации компилятора не ускорят ваше ПО, нужно вернуться назад во времени, к той эпохе, когда по Земле ещё бродили динозавры, а процессоры были чрезвычайно медленными. На графике ниже показаны относительные производительности процессоров и памяти в разные годы (1980-2010 гг.). [Информация взята из статьи Pitfalls of object oriented programming Тони Альбрехта (2009 год), слайд 17. Также можно посмотреть его видео
(2017 год) на ту же тему.]


Проблема, демонстрируемая этим графиком, заключается в том, что производительность процессоров за эти годы значительно выросла (ось Y имеет логарифмический масштаб), а производительность памяти росла гораздо меньшими темпами:

  • В 1980 году задержка ОЗУ составляла примерно 1 такт процессора
  • В 2010 году задержка ОЗУ составляла примерно 400 тактов процессора

Ну и что же? Мы тратим чуть больше тактов на загрузку из памяти, но компьютеры всё равно намного быстрее, чем раньше. Какая разница, сколько тактов мы тратим?

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

В таблице ниже указаны параметры задержки самых распространённых операций. [Таблица взята из книги Systems Performance: Enterprise and the cloud (2nd Edition 2020).] В столбце Задержка в масштабе указана задержка в значениях, которые проще понимать людям.

Событие Задержка Задержка в масштабе
1 такт ЦП 0,3 нс 1 с
Доступ к кэшу L1 0,9 нс 3 с
Доступ к кэшу L2 3 нс 10 с
Доступ к кэшу L3 10 нс 33 с
Доступ к основной памяти 100 нс 6 мин
Ввод-вывод SSD 10-100 мкс 9-90 ч
Ввод-вывод жёсткого диска 1-10 мс 1-12 месяцев

Посмотрев на столбец задержек в масштабе, мы быстро поймём, что доступ к памяти затратен, и что в случае подавляющего большинства приложений процессор просто бездельничает, ожидая ответа от памяти. [Узким местом не всегда является память. Если вы записываете или считываете много данных, то узким местом, скорее всего, будет жёсткий диск. Если вы рендерите большой объём данных на экране, то узким местом может стать GPU.]

На то есть две причины:

  1. Языки программирования, которые мы используем и сегодня, создавались во времена, когда процессоры были медленными, а задержки памяти не были такими критичными.
  2. Best practices отрасли по-прежнему связаны с объектно-ориентированным программированием, которое показывает на современном оборудовании не очень высокую производительность.

Языки программирования


Язык Время создания
C 1975 год
C++ 1985 год
Python 1989 год
Java 1995 год
Javascript 1995 год
Ruby 1995 год
C# 2002 год

Перечисленные выше языки программирования придуманы более 20 лет назад, и принятые их разработчиками проектные решения, например, глобальная блокировка интерпретатора Python или философия Java всё это объекты, в современном мире неразумны. [Все мы знаем, какой бардак представляет собой C++. И да, успокойтесь, я знаю, что в списке нет вашего любимого нишевого языка, а C# всего 19 лет.] Оборудование подверглось огромным изменениям, у процессоров появились кэши и многоядерность, однако языки программирования по-прежнему основаны на идеях, которые уже не истинны.

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

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

Да, компьютеры чрезвычайно быстры, но только если вы пишете ПО таким образом, что оно хорошо взаимодействует с железом. На одном и том же оборудовании вы может работать и очень плавная 3D-игра и заметно лагающий MS Word. Очевидно, что проблема здесь не в оборудовании и что мы можем выжать из него гораздо больше, чем среднестатистическое приложение.

Совершенствовалось оборудование, но не языки


Ещё одна проблема современных языков программирования в том, что они недалеко ушли от программирования в стиле C. Не скажу, что есть что-то плохое в написании циклов for, но становится всё сложнее и сложнее использовать весь потенциал процессоров и в то же время управлять сложностью современных программ.

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

Объяснение будет долгим, но давайте начнём с примера:

Представьте, что мы симулируем колонию муравьёв. Административный отдел колонии был уничтожен атакой муравьеда, поэтому он не знает, сколько муравьёв-воинов осталось живо в колонии.

Поможем нашему муравью-администратору посчитать муравьёв-воинов!

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

class Ant {    public String name = "unknownAnt";    public String color = "red";    public boolean isWarrior = false;    public int age = 0;}// shh, it's a tiny ant colonyList<Ant> antColony = new ArrayList<>(100);// fill the colony with ants// count the warrior antslong numOfWarriors = 0;for (Ant ant : antColony) {    if (ant.isWarrior) {         numOfWarriors++;    }}

Представленное решение компактно и понятно. К сожалению, на современном оборудовании производительность его невысока.

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

  1. Уменьшив объём данных, которые нужно получать для нашей задачи.
  2. Храня необходимые данные в соседних блоках, чтобы полностью использовать строки кэша.

В приведённом выше примере мы будем считать, что из памяти запрашиваются следующие данные (я предполагаю, что используются compressed oops; поправьте меня, если это не так):

+ 4 байта на ссылку имени
+ 4 байта на ссылку цвета
+ 1 байт на флаг воина
+ 3 байта заполнителя
+ 4 байта на integer возраста
+ 8 байт на заголовки класса
---------------------------------
24 байта на каждый экземпляр муравья


Сколько раз нам нужно обратиться к основной памяти, чтобы подсчитать всех муравьёв-воинов (предполагая, что данные колонии муравьёв ещё не загружены в кэш)?

Если учесть, что в современных процессорах строка кэша имеет размер 64 байта, то мы можем получать не больше 2,6 экземпляра муравьёв на строку кэша. Так как этот пример написан на языке Java, в котором всё это объекты, находящиеся где-то в куче, то мы знаем, что экземпляры муравьёв могут находиться в разных строках кэша. [Если распределить все экземпляры одновременно, один за другим, то есть вероятность, что они будут расположены один за другим и в куче, что ускорит итерации. В общем случае лучше всего заранее распределить все данные при запуске, чтобы экземпляры не разбросало по всей куче, однако если вы работаете с managed-языком, то сложно будет понять, что сделают сборщики мусора в фоновом режиме. Например, JVM-разработчики утверждают, что распределение мелких объектов и отмена распределения сразу после их использования обеспечивает бОльшую производительность, чем хранение пула заранее распределённых объектов. Причина этого в принципах работы сборщиков мусора, учитывающих поколения объектов.]

В наихудшем случае экземпляры муравьёв не распределяются один за другим и мы можем получать только по одному экземпляру на каждую строку кэша. Это значит, что для обработки всей колонии муравьёв нужно обратиться к основной памяти 100 раз, и что из каждой полученной строки кэша (64 байта) мы используем только 1 байт. Другими словами, мы отбрасываем 98% полученных данных. Это довольно неэффективный способ пересчёта муравьёв.

Ускоряем работу


Можем ли мы переписать нашу программу так, чтобы запросы к памяти были более рациональными, и таким образом повысить производительность программы? Да, можем.

Мы используем максимально наивный Data Oriented Design. Вместо моделирования муравьёв по отдельности мы смоделируем целую колонию за раз:

class AntColony {    public int size = 0;    public String[] names = new String[100];    public String[] colors = new String[100];    public int[] ages = new int[100];    public boolean[] warriors = new boolean[100];    // I am aware of the fact that this array could be removed    // by splitting the colony in two (warriors, non warriors),    // but that is not the point of this story.    //     // Yes, you can also sort it and enjoy in an additional     // speedup due to branch predictions.}AntColony antColony_do = new AntColony();// fill the colony with ants and update size counter// count the warrior antslong numOfWarriors = 0;for (int i = 0; i < antColony_do.size; i++) {    boolean isWarrior = antColony_do.warriors[i];    if (isWarrior) {        numOfWarriors++;    }}

Эти два примера алгоритмически эквивалентны (O(n)), но ориентированное на данные решение превосходит по производительности объектно-ориентированное. Почему?

Потому что ориентированный на данные пример получает меньше данных, и эти данные запрашиваются соседними блоками мы получаем 64 флагов воинов за раз и не тратим ресурсы впустую. Так как мы получаем только нужные нам данные, нам достаточно обратиться к памяти всего два раза, а не по разу для каждого муравья (100 раз). Это намного более эффективный способ подсчёта муравьёв-воинов.

Я выполнил бенчмарки производительности при помощи тулкита Java Microbenchmark Harness (JMH), их результаты показаны в таблице ниже (измерения выполнялись на Intel i7-7700HQ с частотой 3,80 ГГц). Чтобы не загромождать таблицу, я не указал доверительные интервалы, но вы можете выполнить собственные бенчмарки, скачав и запустив код бенчмарка.

Задача (размер колонии) ООП DOD Ускорение
countWarriors (100) 10 874 045 операций/с 19 314 177 операций/с 78%
countWarriors (1000) 1 147 493 операций/с 1 842 812 операций/с 61%
countWarriors (10000) 102 630 операций/с 185 486 операций/с 81%

Сменив точку зрения на решение задачи, нам удалось добиться огромного ускорения. Стоит также учесть, что в нашем примере класс муравьёв довольно мал, поэтому данные с большой вероятностью останутся в одном из кэшей процессора и разница между двумя вариантами не так заметна (согласно таблице задержек, доступ к памяти из кэша в 30-100 раз быстрее).

Даже несмотря на то, что представленные выше примеры написаны в свободном стиле всё является публичным, ориентированное на данные проектирование не запрещает использовать корпоративный подход со строгим контролем. Вы можете делать любые поля private и final, создавать геттеры и сеттеры, реализовывать интерфейсы, если вам так нравится.

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

Где есть один, будет несколько.

Майк Эктон

Но постойте! Почему ООП настолько популярно, если имеет такую низкую производительность?

  1. Нагрузка часто зависит от ввода-вывода (по крайней мере, в бэкенде серверов), который примерно в 1000 раз медленнее доступа к памяти. Если вы записываете много данных на жёсткий диск, то улучшения, внесённые в структуру памяти, могут и почти не повлиять на показатели.
  2. Требования к производительности большинства корпоративного ПО чудовищно низки, и с ними справится любой старый код. Это ещё называют синдромом клиент за это не заплатит.
  3. Идеи в нашей отрасли движутся медленно, и сектанты ПО отказываются меняться. Всего 20 лет назад задержки памяти не были особой проблемой, и best practices пока не догнали изменения в оборудовании.
  4. Большинство языков программирования поддерживает такой стиль программирования, а концепцию объектов легко понять.
  5. Ориентированный на данные способ программирования тоже обладает собственным множеством проблем.

Проблемы: допустим, нам нужно найти, сколько в колонии красных муравьёв возрастом старше 1 года. В объектно-ориентированном подходе эта задача решается довольно просто:

long numOfChosenAnts = 0;for (Ant ant : antColony) {    if (ant.age > 1 && "red".equals(ant.color)) {        numOfChosenAnts++;     }}

При ориентированном на данные подходе всё становится чуть более сложным, ведь нам нужно быть аккуратными с тем, как происходит итерация по массивам (нужно использовать элемент, расположенный по тому же индексу):

long numOfChosenAnts = 0;for (int i = 0; i < antColony.size; i++) {    int age = antColony.ages[i];    String color = antColony.colors[i];    if (age > 1 && "red".equals(color)) {        numOfChosenAnts++;    }}

А теперь представьте, что кому-то нужно отсортировать всех муравьёв в колонии на основании их имени, а затем что-то сделать с отсортированными данными (например, посчитать всех красных муравьёв из первых 10% отсортированных данных. У муравьёв могут быть странные правила, не судите их строго). При объектно-ориентированном решении мы можем просто использовать функцию сортировки из стандартной библиотеки. При ориентированном на данные способе придётся сортировать массив имён, но в то же самое время сортировать все остальные массивы на основании того, как перемещаются индексы массива имён (мы предполагаем, что нам важно, какие цвет, возраст и флаг воина связаны с именем муравья). [Также можно скопировать массив имён, отсортировать их и найти соответствующее имя в исходном неотсортированном массиве имён, чтобы получить индекс соответствующего элемента. Получив индекс элемента в массиве, можно делать с ним что угодно, но подобные операции поиска выполнять кропотливо. Кроме того, если массивы большие, то такое решение будет довольно медленным. Понимайте свои данные! Также выше не упомянута проблема вставки или удаления элементов в середине массива. При добавлении или удалении элемента из середины массива обычно требуется копировать весь изменённый массив в новое место в памяти. Копирование данных медленный процесс, и если не быть внимательным при копировании данных, может закончиться память. Если порядок элементов в массивах не важен, можно также заменить удалённый элемент последним элементом массива и уменьшить внутренний счётчик, учитывающий количество активных элементов в группе. При переборе таких элементов в этой ситуации мы, по сути, будем перебирать только активную часть группы. Связанный список не является разумным решением этой задачи, потому что данные не расположены в соседних фрагментах, из-за чего перебор оказывается очень медленным (плохое использование кэша).]

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

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

Best practices


Если вы когда-нибудь работали в энтерпрайзе и засовывали нос в его кодовую базу, то, вероятнее всего, видели огромную кучу классов с множественными полями и интерфейсами. Большинство ПО по-прежнему пишут подобным образом, потому что из-за влияния прошлого в таком стиле программирования достаточно легко разобраться. Кроме того, те, кто работает с большими кодовыми базами естественным образом тяготеют к знакомому стилю, который видят каждый день. [См. также On navigating a large codebase]

Для смены концепций подхода к решению задач требуется больше усилий, чем для подражания: Используй const, и компилятор всё оптимизирует! Никто не знает, что сделает компилятор, и никто не проверяет, что он сделал.

Компилятор это инструмент, а не волшебная палочка!

Майк Эктон

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

Если вы хотите больше узнать об этой теме, то прочитайте книгу Data-Oriented Design и остальные ссылки, которые приведены в статье в квадратных скобках.

[БОНУС] Статья, описывающая проблемы объектно-ориентированного программирования:
Data-Oriented Design (Or Why You Might Be Shooting Yourself in The Foot With OOP).
Подробнее..

Архитектура кода программного обеспечения декорируем стратегией. Рассказ в 10 эпизодах, основанный на реальных событиях

21.05.2021 08:10:50 | Автор: admin

Встречаются два эксперта-консультанта по конструированию программного обеспечения:
- Как написать сложное корпоративное приложение, поддерживать которое будет всегда легко и дешево.
- Могу рассказать...
- Рассказать и я могу! Написать-то как?..

Время чтения: 25 мин.

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

Есть много замечательной доступной литературы с теорией. Найти теорию не проблема; проблема применить найденную теорию на практике. Я являюсь сторонником конструирования исключительно поддерживаемого кода, всегда стараюсь найти новые способствующие этому подходы. К сожалению, часто подобные поиски тщетны. Приходится набираться опыта разработки поддерживаемых приложений самостоятельно, придумывать различные подходы. В этой статье хочу поделиться практическими знаниями о проектировании архитектуры кода программного обеспечения, полученными из опыта.

В самом начале статьи хотел бы заранее попросить прощения у читателя за "много букв". Честно говоря, пробовал выразить свою мысль в более короткой версии статьи всё время казалось, что не хватает важных деталей... Надеюсь, статья будет вам интересна и полезна.

Введение в предметную область

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

При написании своих приложений с непростой бизнес-логикой у нас в Ozon мы так же сталкиваемся с обозначенной проблемой. Чтобы написать программное обеспечение так, что его будет комфортно и недорого поддерживать, нужно нарабатывать соответствующие техники конструирования кода.

В этой статье я хочу предложить технику написания программ, в основе которой лежит два паттерна проектирования ООП: декоратор и стратегия. Я уверен, что основная часть читающих статью наверняка не раз сталкивалась с этими паттернами (возможно, даже на практике). Но чтобы все чувствовали себя "в своей тарелке", обращусь к определениям из "Паттернов проектирования" Эриха Гаммы, Ричарда Хелма, Ральфа Джонсона и Джона Влиссидеса (Банда четырех, Gang of Four, GoF):

  • Декоратор (Decorator, Wrapper) паттерн проектирования, позволяющий динамически добавлять объекту новые обязанности. Является гибкой альтернативой порождению подклассов с целью расширения функциональности.

  • Стратегия (Strategy, Policy) паттерн проектирования, который определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Стратегия позволяет изменять алгоритмы независимо от клиентов, которые ими пользуются.

Подход, который я называю "Декорирование стратегией" и который мы с вами будем рассматривать дальше, предполагает использование этих паттернов совместно друг с другом. Соответственно он не имеет смысла при их использовании порознь.

Декорирование стратегией, на мой взгляд, даёт великую пользу при поддержке приложений на очень большом жизненном цикле программного продукта. Компоненты в коде, написанные с применением данного подхода, соответствуют всем принципам дизайна SOLID из "Чистой архитектуры" Роберта Мартина. Каждый компонент, который мы напишем далее, будет отвечать только за одно действие; после написания нового компонента мы ни разу не модифицируем логику его методов, а лишь будем расширять ее в декорирующих компонентах; в силу паттерна "Декоратор" все расширяемые и расширяющие компоненты соответствуют одному контракту, следовательно их можно заменять друг другом; интерфейсы компонентов не содержат зависимостей, которые не используются; компоненты бизнес-логики ни в коей мере не зависят от деталей.

Я не раз сталкивался в обсуждениях с опытными разработчиками, которые говорят: "А вот всё, что связано с применением принципов SOLID, паттернов ООП на практике это миф!". Любезно обращаясь к скептически настроенным к применению теории разработки в реальных больших корпоративных проектах, хочу сказать: "А вот посмотрим!"

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

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

  • Должная обработка ошибок. В коде мы ограничимся оборачиванием ошибок дополнительным сообщением с помощью пакета "github.com/pkg/errors".

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

  • Комментарии и документирование кода.

  • Всё, что связано, с конкурентным выполнением задач и синхронизацией.

  • Структура файлов и директорий проекта.

  • Стили, линтеры и статический анализ.

  • Покрытие кода тестами.

  • Сквозь методы компонентов рекомендуется с первых этапов разработки "тянуть" context.Context, даже если он в тот момент не будет использоваться. Для упрощения повествования в примерах далее контекст также использоваться не будет.

Перейдём же наконец от скучной теории к занимательной практике!

Пролог. Закладываем фундамент

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

Итак, начнём. Здесь мы с вами высококвалифицированные разработчики программных продуктов. К нам приходит наш первый заказчик от бизнеса и говорит что-то вроде: "Нам нужна функциональность обновления такой-то информации о пользователях нашей платформы". Мы обрабатываем требования, продумываем архитектуру и переходим к конструированию кода.

Первое, что нужно сделать определить интерфейс нашего первого компонента службы, которая будет представлять желаемый use-case SavePersonService. Но для этого нам нужно определить объекты нашей предметной области, а именно структуру данных, содержащую информацию о человеке PersonDetails. Создадим в корне проекта пакет app, далее создадим файл app/person.go, и оставим в нём нашу структуру:

// app/person.gotype PersonDetails struct {    Name string    Age  int}

Данный файл завершён, больше мы к нему в этой статье возвращаться не будем. Далее создаем файл app/save-person.go, и определяем в нём интерфейс нашего use-case:

// app/save-person.gotype SavePersonService interface {    SavePerson(id int, details PersonDetails) error}

Оставим сразу рядом с определением интерфейса его первую реализацию компонент noSavePersonService, который ничего не делает в теле интерфейсного метода:

// app/save-person.go// ... предыдущий код ...type noSavePersonService struct{}func (noSavePersonService) SavePerson(_ int, _ PersonDetails) error { return nil }

Поскольку объекты noSavePersonService не содержат состояния, можно гарантировать, что данный "класс" может иметь только один экземпляр. Напоминает паттерн проектирования Синглтон (Singleton ещё его называют Одиночка, но мне это название по ряду причин не нравится). Предоставим глобальную точку доступа к нему. В Golang легче всего это сделать, определив глобальную переменную:

/ app/save-person.go// ... предыдущий код ...var NoSavePersonService = noSavePersonService{}

Зачем мы написали ничего не делающий компонент? С первого взгляда он очень походит на заглушку. Это не совсем так. Далее поймём.

Эпизод 1. Будем знакомы, Декоратор Стратегией

Перейдём непосредственно к реализации бизнес-логики нашей задачи. Нам нужно в конечном счёте иметь хранилище, в котором содержатся данные о пользователях. С точки зрения выбора технологии мы сразу себе представляем, что будем использовать PostgreSQL, но правильно ли завязываться в коде нашей бизнес-логики на конкретную технологию. Вы правы конечно нет. Определить компонент нашего хранилища нам позволит паттерн Репозиторий (Repository). Создадим пакет с реализациями интерфейса нашего use-case save-person внутри app, и в нём создадим файл app/save-person/saving_into_repository.go реализации нашего use-case, которая обновляет данные в репозитории:

// app/save-person/saving_into_repository.gotype PersonRepository interface {    UpdatePerson(id int, details app.PersonDetails) error}type SavePersonIntoRepositoryService struct {    base app.SavePersonService    repo PersonRepository}func WithSavingPersonIntoRepository(base app.SavePersonService, repo PersonRepository) SavePersonIntoRepositoryService {    return SavePersonIntoRepositoryService{base: base, repo: repo}}func (s SavePersonIntoRepositoryService) SavePerson(id int, details app.PersonDetails) error {    err := s.base.SavePerson(id, details)    if err != nil {        return errors.Wrap(err, "save person in base in save person into repository service")    }    err = s.repo.UpdatePerson(id, details)    if err != nil {        return errors.Wrap(err, "update person in repo")    }    return nil}

В коде выше впервые появляется компонент, который выражает наш подход "Декорирование стратегией". Сам компонент представляет собой декоратор, реализующий интерфейс нашего use-case, который оборачивает любой компонент с таким же интерфейсом. В реализации метода изначально вызывается метод декорируемого объекта s.base; после этого происходит вызов стратегии обновления данных о человеке в хранилище s.repo. По сути, весь подход это конструирование компонентов-декораторов, которые содержат два объекта:

  1. Непосредственно декорируемый объект с таким же интерфейсом.

  2. Стратегия, логику которой мы добавляем в довесок к логике декорируемого объекта.

Структурная схема программы, собранной из декораторов стратегий может выглядеть примерно так:

Компонент сам по себе настолько прост, что самое сложное, пожалуй, это определить, когда следует вызывать метод стратегии до или после вызова метода декорируемого объекта или конкурентно с ним.

Напомню, что бизнес-логика не должна содержать ненужные зависимости, зависимости от деталей и т.п. Другими словами, бизнес-логика должна быть "чистая, как слеза". Где тогда должны находиться зависимости от конкретных реализаций, зависимости от используемых технологий? Ответ в файле main.go. Следуя замечаниям Роберта Мартина, можно сделать умозаключение, что код компонентов файла, содержащего точку входа в программу, является самым "грязным" с точки зрения зависимостей от всего. Обозначим в main.go метод, который нам возвращает клиент к базе данных PostgreSQL. И собственно сборку объекта службы нашего use-case и вызов его метода на условных входных данных:

// main.gofunc NewPostgreSQLDatabaseClient(dsn string) savePerson.PersonRepository {    _ = dsn // TODO implement    panic("not implemented")}func run() error {    userService := savePerson.WithSavingPersonIntoRepository(        app.NoSavePersonService,        NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/users?sslmode=disable"))    err := userService.SavePerson(5, app.PersonDetails{        Name: "Mary",        Age:  17,    })    if err != nil {        return errors.Wrap(err, "save user Mary")    }    return nil}

В коде выше мы можем заметить, что в качестве стратегии репозитория выступает обозначенный конкретный компонент клиента к PostgreSQL. В качестве же декорируемого объекта выступает наша "фиктивная" реализация use-case app.NoSavePersonService, которая по сути ничего не делает. Зачем она нужна? Она ничего полезного ведь не делает? Не легче ли просто вызвать метод клиента к базе данных? Спокойно, звёздный час этой реализации сейчас настанет.

Ссылка на полный код эпизода

Эпизод 2. Магия начинается!

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

// main.go// ... предыдущий код ...func NewMemoryCache() savePerson.PersonRepository {    // TODO implement    panic("not implemented")}// ... последующий код ...

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

// main.go// внутри run()userService := savePerson.WithSavingPersonIntoRepository(    savePerson.WithSavingPersonIntoRepository(        app.NoSavePersonService,        NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/users?sslmode=disable")),    NewMemoryCache(),)err := userService.SavePerson(5, app.PersonDetails{    Name: "Mary",    Age:  17,})if err != nil {    return errors.Wrap(err, "save user Mary")}

Всё, что мы тут делаем в итоге два раза декорируем наш "холостой" сервис обновлениями данных в двух репозиториях разного происхождения. Теперь мы можем добавлять обновление данных в новых репозиториях достаточно быстро и комфортно.

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 3. Рефакторинг для здоровья

В предыдущем листинге кода создание сервиса выглядит достаточно громоздко. Нетрудно догадаться, применяя наш подход, мы продолжим и далее всё больше и больше оборачивать компонент, добавляя к логике новые стратегии. Поэтому мы, как опытные разработчики, замечаем эту потенциальную трудность и производим небольшой рефакторинг когда. Нам поможет паттерн Билдер (Builder опять же мне не очень нравится ещё одно его название Строитель). Это будет отдельный компонент, зона ответственности которого предоставить возможность сборки объекта службы нашего use-case. Файл app/save-person/builder.go:

// app/save-person/builder.gotype Builder struct {    service app.SavePersonService}func BuildIdleService() *Builder {    return &Builder{        service: app.NoSavePersonService,    }}func (b Builder) SavePerson(id int, details app.PersonDetails) error {    return b.service.SavePerson(id, details)}

Компонент Builder должен обязательно реализовывать интерфейс службы нашего use-case, так как именно он будет использоваться в конечном счёте. Поэтому мы добавляем метод SavePerson, который вызывает одноименный метод объекта в приватном поле service. Конструктор данного компонента называется BuildIdleService, потому что создаёт объект, который ничего не будет делать при вызове SavePerson (нетрудно заметить инициализацию поля service объектом app.NoSavePersonService). Зачем нам нужен этот бесполезный компонент? Чтобы получить всю истинную пользу, необходимо обогатить его другими методами. Эти методы будут принимать в параметрах стратегию и декорировать ею объект службы в поле service. Но вначале сделаем конструктор WithSavingPersonIntoRepository в app/save-person/saving_into_repository.go приватным, так как для создания службы мы теперь будем использовать только Builder:

// app/save-person/saving_into_repository.go// ... предыдущий код ...func withSavingPersonIntoRepository(base app.SavePersonService, repo PersonRepository) SavePersonIntoRepositoryService {    return SavePersonIntoRepositoryService{base: base, repo: repo}}// ... последующий код ...

Добавляем соответствующий метод для Builder:

// app/save-person/builder.go// ... предыдущий код ...func (b *Builder) WithSavingPersonIntoRepository(repo PersonRepository) *Builder {    b.service = withSavingPersonIntoRepository(b.service, repo)    return b}

И наконец производим рефакторинг в main.go:

// main.go// ... предыдущий код ...userService := savePerson.BuildIdleService().        WithSavingPersonIntoRepository(NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/platform?sslmode=disable")).        WithSavingPersonIntoRepository(NewMemoryCache())// ... последующий код ...

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 4. Больше заказчиков!

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

// main.go// ... предыдущий код ...func NewMongoDBClient(dsn string) savePerson.PersonRepository {    _ = dsn // TODO implement    panic("not implemented")}// ... последующий код ...

Воспользуемся нашим билдером и просто добавим новый код в main.go под имеющийся фрагмент с userService:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithSavingPersonIntoRepository(NewMemoryCache())err = taxpayerService.SavePerson(1326423, app.PersonDetails{    Name: "Jack",    Age:  37,})if err != nil {    return errors.Wrap(err, "save taxpayer Jack")}

Мы выполнили уже столько поставленных задач, имея небольшой фрагмент кода бизнес-логики. Заметьте, изменения преимущественно вносятся в файл main.go

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 5. Путь в никуда

Проходит ещё время. Заказчик 2 ставит нам такую задачу. Так как все налогоплательщики должны быть совершеннолетними, необходимо в бизнес-логику добавить функциональность проверки возраста человека перед сохранением в хранилище. С этого момента начинаются интересные вещи. Мы можем добавить эту валидацию в метод SavePersonIntoRepositoryService.SavePerson в файле app/save-person/saving_into_repository.go. Но тогда при нескольких декорированиях стратегией сохранения информации в репозиторий эта валидация будет вызываться столько раз, сколько производилось таких декораций. Хотя и все проверки помимо первой никак не влияют на результат напрямую, всё-таки не хочется лишний раз вызывать один и тот же метод.

Мы можем добавить валидацию в Builder.SavePerson. Но есть проблема: заказчику 1 не нужна проверка возраста при сохранении. Придётся добавить if и дополнительный флаг в параметры конструктора, который будет определять необходимость валидации:

// app/save-person/builder.gotype Builder struct {    service           app.SavePersonService    withAgeValidation bool}func BuildIdleService(withAgeValidation bool) *Builder {    return &Builder{        service:           app.NoSavePersonService,        withAgeValidation: withAgeValidation,    }}func (b Builder) SavePerson(id int, details app.PersonDetails) error {    if b.withAgeValidation && details.Age < 18 {        return errors.New("invalid age")    }    return b.service.SavePerson(id, details)}// ... последующий код ...

И тогда в main.go нужно вызывать конструкторы билдера с разными значениями флага withAgeValidation:

// main.go// ... предыдущий код ... userService := savePerson.BuildIdleService(false).// ... код ...taxpayerService := savePerson.BuildIdleService(true).// ... последующий код ...

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

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 6. Путь истины

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

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

// app/save-person/validating.gotype PersonValidator interface {    ValidatePerson(details app.PersonDetails) error}type PreValidatePersonService struct {    base      app.SavePersonService    validator PersonValidator}func withPreValidatingPerson(base app.SavePersonService, validator PersonValidator) PreValidatePersonService {    return PreValidatePersonService{base: base, validator: validator}}func (s PreValidatePersonService) SavePerson(id int, details app.PersonDetails) error {    err := s.validator.ValidatePerson(details)    if err != nil {        return errors.Wrap(err, "validate person")    }    err = s.base.SavePerson(id, details)    if err != nil {        return errors.Wrap(err, "save person in base in pre validate person service")    }    return nil}

Опять ничего нового. PreValidatePersonService это очередной декоратор стратегией валидации перед последующим вызовом декорируемого метода.

Добавим соответствующий метод в Builder:

// app/save-person/builder.go// ... предыдущий код ...func (b *Builder) WithPreValidatingPerson(validator PersonValidator) *Builder {    b.service = withPreValidatingPerson(b.service, validator)    return b}

Добавление каждого нового декоратора стратегией требует добавление нового метода в наш билдер.

Добавим реализацию валидатора, проверяющую возраст человека:

// main.go// ... предыдущий код ...type personAgeValidator struct{}func (personAgeValidator) ValidatePerson(details app.PersonDetails) error {    if details.Age < 18 {        return errors.New("invalid age")    }    return nil}var PersonAgeValidator = personAgeValidator{}// ... последующий код ...

Так как personAgeValidator не имеет состояния, можем сделать для компонента единую точку доступа PersonAgeValidator. Далее просто вызываем новый метод в main.go только для taxpayerService:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithSavingPersonIntoRepository(NewMemoryCache()).    WithPreValidatingPerson(PersonAgeValidator)// ... последующий код ...

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 7. А ну-ка закрепим

Уверен, к данному эпизоду вы поняли смысл подхода "Декорирование стратегией". Чтобы закрепить, давайте добавим ещё один такой компонент. Представим, технический руководитель требует от нас покрыть метриками время выполнения сохранения данных в хранилище. Мы могли бы замерить это время, просто добавив пару строчек кода в SavePersonIntoRepositoryService. Но как бы не так! Мы же не изменяем уже работающий в продакшне код, а можем его только расширить. Давайте же так и сделаем. Добавим новый декоратор стратегией отправки метрики времени:

// app/save-person/sending_metric.gotype MetricSender interface {    SendDurationMetric(metricName string, d time.Duration)}type SendMetricService struct {    base         app.SavePersonService    metricSender MetricSender    metricName   string}func withMetricSending(base app.SavePersonService, metricSender MetricSender, metricName string) SendMetricService {    return SendMetricService{base: base, metricSender: metricSender, metricName: metricName}}func (s SendMetricService) SavePerson(id int, details app.PersonDetails) error {    startTime := time.Now()    err := s.base.SavePerson(id, details)    s.metricSender.SendDurationMetric(s.metricName, time.Since(startTime))    if err != nil {        return errors.Wrap(err, "save person in base in sending metric service")    }    return nil}

Помимо компонента стратегии, отправляющего метрики, мы в конструкторе также передаем название метрики, которую мы хотим замерять. Добавляем новый метод в Builder:

// app/save-person/builder.go// ... предыдущий код ...func (b *Builder) WithMetricSending(metricSender MetricSender, metricName string) *Builder {    b.service = withMetricSending(b.service, metricSender, metricName)    return b}

И наконец обозначаем в main.go функцию, возвращающую savePerson.MetricSender и добавляем вызов нового метода Builder в сборку наших сервисов:

// main.go// ... предыдущий код ...func MetricSender() savePerson.MetricSender {    // TODO implement    panic("not implemented")}// ... код ...userService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewPostgreSQLDatabaseClient("postgres://user:pass@127.0.0.1:5432/platform?sslmode=disable")).    WithMetricSending(MetricSender(), "save-into-postgresql-duration").    WithSavingPersonIntoRepository(NewMemoryCache())// ... код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithMetricSending(MetricSender(), "save-into-mongodb-duration").    WithSavingPersonIntoRepository(NewMemoryCache()).    WithPreValidatingPerson(PersonAgeValidator)// ... последующий код ...

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

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 8. Результаты ясновидения

Проходит время. Заказчик 2 ставит новую задачу. Он желает знать, как долго выполняется сохранение данных о налогоплательщике, но с небольшой оговоркой: учитывать нужно всё, кроме валидации. Похоже на замер времени, который мы недавно реализовали для своих целей, не правда ли? Чтобы решить задачу, всё что нам требуется это добавить вызов метода для новой метрики в main.go:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithMetricSending(MetricSender(), "save-into-mongodb-duration").    WithSavingPersonIntoRepository(NewMemoryCache()).    WithMetricSending(MetricSender(), "save-taxpayer-duration").    WithPreValidatingPerson(PersonAgeValidator)

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 9. Укрощение капризов

Мы вот только недавно произвели релиз последней задачи от заказчика 2, но он захотел изменить начальные требования. Такие изменения часто возникают на стороне заказчика, которые заставляют нас "перелопатить" весь код. Знакомо? На этот раз заказчик желает отказаться от оговорки из предыдущего эпизода и производить замер полного цикла сохранения данных о налогоплательщике вместе с валидацией. Если бы мы конструировали нашу бизнес-логику в виде сценария транзакции (transaction script), то это повлекло бы за собой непосредственное вмешательство в тело метода, copy-paste кода, что требует приложить силы, в том числе в процессе ревью, тестирования и т.п. В нашем же случае нам достаточно просто подвинуть вызов метода WithMetricSending в цепочке методов создания объекта службы в main.go:

// main.go// ... предыдущий код ...taxpayerService := savePerson.BuildIdleService().    WithSavingPersonIntoRepository(NewMongoDBClient("mongodb://user:pass@127.0.0.1:27017/tax_system")).    WithMetricSending(MetricSender(), "save-into-mongodb-duration").    WithSavingPersonIntoRepository(NewMemoryCache()).    WithPreValidatingPerson(PersonAgeValidator).    WithMetricSending(MetricSender(), "save-taxpayer-duration")

В коде выше мы поменяли местами второй WithMetricSending и WithPreValidatingPerson.

Задача от заказчика выглядит надуманной. Но напомню, что цель статьи не придумать качественные задачи заказчиков, а продемонстрировать пользу архитектуры кода при использовании подхода "Декорирование стратегией".

Ссылка на diff эпизода
Ссылка на полный код эпизода

Эпизод 10. Взгляд в будущее

Этот заключительный эпизод всего лишь подчеркивает потенциал дальнейших доработок логики данного кода. Что ещё может пожелать заказчик от бизнеса или с технической стороны? Вариантов более чем достаточно. Может потребоваться функциональность отправки асинхронных событий об изменении информации о человеке (полезно при ведении журнала аудита, коммуникации с другими сервисами и т.д.). Может понадобиться введение механизма гомогенных и даже гетерогенных транзакций. Возможно, потребуется добавить запрос данных к соседнему микросервису. По техническим соображениям возможно будет нужен предохранитель (circuit-breaker) для таких запросов к другим сервисам. Наверняка нужно будет добавлять механизм трассировки (tracing). И многое-многое другое.

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

Эпилог. Подводим итоги

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

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

Но для больших корпоративных приложений наличие подобного подхода просто желательно-обязательно. Если продукт подразумевает длительную поддержку (обычно это условие присутствует всегда), то объектная модель приложения будет иметь значительное преимущество над незамысловатым "полотном" кода сценария транзакции. Я приведу далее график, в основе которого лежит график из "Шаблонов корпоративных приложений" Мартина Фаулера.

Что есть что на этом графике? Почему на осях нет чисел? Всё потому что график абстрактный. Он отражает качественный смысл содержимого, не количественный. По горизонтальной оси у нас время, прошедшее с момента начала разработки продукта. Или если желаете, количество добавлений новой функциональности в изначально разработанный продукт. Меру по вертикальной оси тоже можно выразить различными способами. Это может быть цена добавления новой строчки кода функционала в денежном эквиваленте; может быть время добавления новой функциональности; может быть количество потраченных нервных клеток разработчиком, ревьювером или тестировщиком. Красный график демонстрирует зависимость этих величин для подхода разработки, который называется сценарием транзакции (Transaction Script) последовательно следующие друг за другом инструкции. Синий график показывает эту зависимость для подхода модели предметной области (Domain Model).

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

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

Содержание статьи изложено на основе моего субъективного понимания. Любые замечания с удовольствием готов обсуждать в комментариях. Использовать "Декорирование стратегией" или нет личное решение каждого. Главное, я считаю, нужно помнить о том, что мы как разработчики должны в первую очередь уделять внимание не бизнесу, не пользователю, не выделенным машинным ресурсам, а нашему коллеге такому же разработчику, который через несколько лет будет добавлять в наш код новую функциональность.

Литература

  1. Макконнелл С. Совершенный код. Мастер-класс., 2020.

  2. Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Приемы объектно-ориентированного проектирования. Паттерны проектирования., 2020.

  3. Мартин Р. Чистая архитектура. Искусство разработки программного обеспечения., 2020

  4. Фаулер, Мартин. Шаблоны корпоративных приложений., 2020.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru