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

Чистый код

Из песочницы Преимущества интерфейсов в GO

06.07.2020 20:09:26 | Автор: admin

Преимущества интерфейсов в GO


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


В чем особенность интерфейсов в GO?


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


Какие преимущество дает эта особенность?


Приватный интерфейс


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


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


Рассмотрим пример.


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


package authimport (   "gitlab.com/excercice_detection/backend")type userRepository interface {   FindUserByEmail(email string) (backend.User, error)   AddUser(backend.User) (userID int, err error)   AddToken(userID int, token string) error   TokenExists(userID int, token string) bool}// Auth сервис авторизацииtype Auth struct {   repository userRepository   logger     backend.Logger}// NewAuth создает объект авторизацииfunc NewAuth(repository userRepository, logger backend.Logger) *Auth {   return &Auth{repository, logger}}// Autentificate Проверяет существование токена пользователяfunc (auth Auth) Autentificate(userID int, token string) bool {   return auth.repository.TokenExists(userID, token)}

Для примера я показал как используется один из методов, на самом деле они используются все.


В главном методе main создается и используется объект авторизации:


package mainimport (   "gitlab.com/excercice_detection/backend/auth"   "gitlab.com/excercice_detection/backend/mysql")func main() {    logger := newLogger()    userRepository := mysql.NewUserRepository(logger)    err := userRepository.Connect()    authService := auth.NewAuth(userRepository, logger)...

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


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


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


Такой интерфейс удобно расширять


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


Тесты


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


Пример мока:


type userRepositoryMock struct {   user         backend.User   findUserErr  error   addUserError error   addUserID    int   addTokenErr  error   tokenExists  bool}func (repository userRepositoryMock) FindUserByEmail(email string) (backend.User, error) {   return repository.user, repository.findUserErr}func (repository userRepositoryMock) AddUser(backend.User) (userID int, err error) {   return repository.addUserID, repository.addUserError}func (repository userRepositoryMock) AddToken(userID int, token string) error {   return repository.addTokenErr}func (repository userRepositoryMock) TokenExists(userID int, token string) bool {   return repository.tokenExists}

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


Как понять, кто на самом деле реализует метод, используемый из интерфейса?


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


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


Как найти места, где используются реализованные методы, если они закрыты интерфейсом?


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


Заключение


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


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

Подробнее..

Перевод Руководство Google по форматированию кода на Java

02.08.2020 16:06:23 | Автор: admin
image

1 Введение


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

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

1.1 Терминология


В рамках этого руководства определены следующие термины:

  1. Термин класс используется для обозначения обычного класса, перечислений, интерфейса или типа аннотации (@interface)
  2. Термин член класса используется для обозначения вложенного класса, поля, метода или конструктора, то есть для всех элементов класса высокого уровня, кроме блоков инициализации и комментариев
  3. Термин комментарий всегда относится к комментариям реализации. Мы не используем словосочетание комментарии к документации, а вместо этого используем термин Javadoc

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

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


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

2 Исходный файл. Основы


2.1 Имя файла


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

2.2 Кодировка файла: UTF-8


Для файлов с кодом используется кодировка UTF-8.

2.3 Специальные символы


2.3.1 Символы пробела


Помимо последовательности символов конца строки, горизонтальный символ пробела ASCII (020) является единственным символом-разделителем, встречающимся в исходном файле. Это означает, что:

  1. Все прочие пробельные символы в символьных и строковых литералах экранируются
  2. Символы табуляции не используются для отступов

2.3.2 Специальные экранирующие последовательности


Для каждого символа, для которого существует специальная экранирующая последовательность (\b, \t, \n, \f, \r, \", \' и \\), предпочтительнее использовать именно ее вместо соответствующего ей восьмеричного значения (например, \012) или кода Unicode (например, \u000a).

2.3.3 Символы не из таблицы ASCII


Для символов не из таблицы ASCII используется символ Unicode (например, ) или эквивалентная экранирующая последовательность (например, \u221e). Выбор отдается в пользу тех символов, которые делают код более понятным и читаемым.

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

Пример Примечание
String unitAbbrev = "s" Отлично: прекрасно воспринимается даже без комментария
String unitAbbrev = "\u03bcs" // "s" Допускается, но нет причин так делать
String unitAbbrev = "\u03bcs" // Греческая буква mu, s Допускается, но неуклюже и может привести к ошибкам
String unitAbbrev = "\u03bcs" Плохо: читающий понятия не имеет, что это такое
return '\ufeff' + content // знак порядка байтов Хорошо: используйте экранирование для непечатаемых символов и комментируйте при необходимости

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

3 Структура исходного файла


Исходный файл состоит из следующих элементов (в указанном порядке):

  1. Информация о лицензии или авторских правах, если имеется
  2. Объявление пакета
  3. Объявление импортов
  4. Объявление класса

Ровно одна пустая строка разделяет каждый присутствующий раздел.

3.1 Информация о лицензии или авторских правах, если имеется


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

3.2 Объявление пакета


Пакет объявляется без переноса строки. Ограничение на ширину строки (Раздел 4.4) на объявление пакета не распространяется.

3.3 Объявления импортов


3.3.1 Символ подстановки при объявлении импортов


Символ * (подстановки) при объявлении импортов, статических или нет, не используется.

3.3.2 Перенос строки


Импорты объявляются без переноса строки. Ограничение на ширину строки на них не распространяется.

3.3.3 Упорядочивание и интервал


Импорты упорядочиваются следующим образом:

  1. Все статические импорты размещаются и группируются в одном блоке
  2. Все не статические импорты размещаются в другом блоке

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

Внутри каждого блока импортируемые классы следуют в порядке сортировки ASCII-символов.

3.3.4 Статический импорт вложенных классов


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

3.4 Объявление класса


3.4.1 Объявляется ровно один класс верхнего уровня


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

3.4.2 Упорядочивание содержимого класса


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

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

3.4.2.1 Перегруженный код не должен быть разделен


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

4 Форматирование


Терминология


Тело класса, метода или конструктора относится к блочной конструкции.

Обратите внимание, что согласно Разделу 4.8.3.1, любой инициализатор массива также может рассматриваться как блочная конструкция.

4.1 Фигурные скобки


4.1.1 Фигурные скобки используются везде, где они могут быть использованы


Фигурные скобки используются в if, else, for, while и do-while, даже если тело выражения пустое или содержит лишь одну строку кода.

4.1.2 Непустые блоки: стиль K & R


Фигурные скобки ставятся согласно стилю Кернигана и Ритчи (Египетские скобки) для непустых блоков и блочных конструкций (для наглядности мы решили добавить немного кода, демонстрирующего данные правила примечание переводчика):

  • Перед открывающейся скобкой переход на новую строку не делается:

// правильноif (true) { // неправильноif (true){

  • Переход на новую строку делается после открывающей скобки:

// правильноwhile (true) {  // code// неправильноwhile (true) { // code

  • Переход на новую строку делается перед закрывающей скобкой:

// правильноfor () {  // code}// неправильноwhile (true) { /* code */ }// неправильноif (true) {  /* code */ }

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

Примеры правильно выполненных правил:

return () -> {  while (condition()) {    method();  }};return new MyClass() {  @Override public void method() {    if (condition()) {      try {        something();      } catch (ProblemException e) {        recover();      }    } else if (otherCondition()) {      somethingElse();    } else {      lastThing();    }  }};

Некоторые исключения для перечислений приведены в Разделе 4.8.1

4.1.3 Пустые блоки могут быть сжатыми


Пустой блок или пустая блочная конструкция может следовать стилю K & R (как описано в Разделе 4.1.2). Также возможно, чтобы такой блок был закрыт сразу же после открытия, без символов или разрыва строки внутри {}. Это правило не относится к случаю, когда блок является частью многоблочного выражения, которое содержит if-else или try-catch-finally.

Примеры:

// Приемлемоvoid doNothing() {}// Также приемлемоvoid doNothingElse() {}// Неприемлемо: нельзя использовать пустые блоки в многоблочном выраженииtry {  doSomething();} catch (Exception e) {}

4.2 Два пробела для отступа


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

4.3 Одно выражение на каждую строку


Каждое выражение завершается переходом на новую строку.

4.4 Ограничение ширины строки в 100 символов


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

Исключения:

  1. Строки, в которых соблюдение ограничения по ширине невозможно (например, длинная ссылка URL в Javadoc или длинная JSNI-ссылка на метод)
  2. Объявления package и import (см. Разделы 3.2 и 3.3)
  3. Строки с командами в комментариях, которые могут быть скопированы и вставлены для выполнения в терминале

4.5 Перенос строки


Терминология


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

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

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

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

4.5.1 Где делать перенос


Первое указание по переносу строки гласит: предпочтительнее делать перенос на более высоком синтаксическом уровне. Также:

1. Когда строка разрывается не на операторе присваивания, разрыв делается перед символом.

Это правило также применимо к следующим оператороподобным символам:

  • разделяющая точка .
  • двойное двоеточие ссылочного метода ::
  • амперсанд в скобках дженерика <T extends Foo & Bar>
  • разделитель в catch-блоке catch (FooException | BarException e)

2. Когда строка переносится на операторе присваивания, перенос обычно делается после символа, но приемлемо и другое решение

Это также применимо к двоеточию для цикла for-each.

3. Имя метода или конструктора при переносе строки остается присоединенным к открывающей скобке (

4. Запятая , при переносе строки остается с элементом, который ей предшествует

5. Строка никогда не переносится непосредственно у стрелки лямбда-выражения, кроме случаев, когда его тело состоит из одного выражения без фигурных скобок:

MyLambda<String, Long, Object> lambda =    (String label, Long value, Object obj) -> {        ...    };Predicate<String> predicate = str ->    longExpressionInvolving(str);

Главная цель переноса строки добиться ясности кода, но не обязательно наименьшего количества строк

4.5.2 Смещение продолжения строки на 4 и более пробелов


При переносе строки каждая следующая ее подстрока (каждое продолжение строки) смещается как минимум на 4 пробела относительно предыдущей.

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

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

4.6 Пробелы и отступы


4.6.1 Отступы


Одна пустая строка всегда ставится:

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

  • исключение: пустая строка между двумя последовательными полями (без кода между ними) используется опционально. При необходимости пустые строки используются для логического группирования полей
  • исключение: пустые строки между константами класса enum (см. Раздел 4.8.1)

2. В соответствии с другими разделами данного документа (например, с Разделом 3 и Разделом 3.3)

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

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

4.6.2 Пробелы


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

1. При разделении любого зарезервированного слова, такого как if, for или catch, и открывающей круглой скобки (, которая следует за ним

2. При разделении любого зарезервированного слова, такого как else или catch, и закрывающей фигурной скобки }, которая следует за ним

3. Перед любой открывающей фигурной скобкой {, за исключением двух ситуаций:

  • &commat;SomeAnnotation({a, b})
  • String[][] x = {{foo}}; пробел между {{ не нужен согласно п. 8 ниже

4. По обе стороны от любого бинарного или тернарного оператора

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

  • амперсанд внутри угловых скобок: <T extends Foo & Bar>
  • разделитель в блоке catch, содержащий несколько исключений: catch (FooException | BarException e)
  • двоеточие : в for-each
  • стрелка в лямбда-выражении: (String str) -> str.length()

Но это правило не применимо к операторам:

  • двойное двоеточие :: ссылочного метода, которое пишется как Object::toString
  • разделяющая точка ., которая пишется как object.toString()

5. После ,:; или закрывающей круглой скобки ")" при приведении типа

6. По обе стороны от двойной косой черты // при создании комментария в той же строке кода. Здесь разрешены, но не необходимы несколько пробелов

7. Между объявлением типа и именем переменной:
List<String> list

8. Опционально: внутри скобок инициализатора массива
new int[] {5, 6} и new int[] { 5, 6 } оба варианта верные

9. Между аннотацией типа и [] или

Это правило не требует наличия или отсутствия пробелов в начале или конце строки; оно относится лишь к внутренним пробелам.

4.6.3 Горизонтальное выравнивание не требуется никогда


Терминология


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

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

Пример с выравниванием и без него:

private int x; // хорошоprivate Color color; // и это тожеprivate int   x;      // разрешено, но при редактировании в будущемprivate Color color;  // можно оставить без выравнивания

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

4.7 Группирующие скобки рекомендованы


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

4.8 Особые конструкции


4.8.1 Классы-перечисления


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

private enum Answer {  YES {    @Override public String toString() {      return "yes";    }  },  NO,  MAYBE}

Код класса-перечисления без методов и комментариев, описывающих его константы, может быть представлен, как инициализатор массива (см. Раздел 4.8.3.1):

private enum Suit { CLUBS, HEARTS, SPADES, DIAMONDS }

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

4.8.2 Объявления переменных


4.8.2.1 Одна переменная на одно объявление


Каждая переменная (поле или локальная) объявляется только по одной за раз: такие объявления, как int a, b; не используются.

Исключение: Множественные объявления переменных допускаются в заголовке цикла for.

4.8.2.2 Объявляйте переменные тогда, когда они нужны


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

4.8.3 Массивы


4.8.3.1 Инициализаторы массивов могут быть блочными


Любой массив может быть инициализирован так, как если бы он был блочной конструкцией. К примеру, весь следующий код допустим (список примеров не полный):

new int[] {  0, 1, 2, 3}                        new int[] {  0, 1,  2, 3}new int[]    {0, 1, 2, 3}new int[] {  0,  1,  2,  3}

4.8.3.2 Никаких объявлений массивов в стиле языка С


Квадратные скобки ставятся после типа, а не после переменной: String[] args, а не String args[].

4.8.4 Оператор switch


Терминология


Внутри блока switch располагается одна или более групп операторов. Каждая группа состоит из одной или более меток (как case FOO:, так и default:), за которыми следует один или более операторов (или, в случае последней группы, ни одного или более).

4.8.4.1 Смещение


Как и в случае с любым другим блоком, содержимое блока switch смещается на 2 пробела.

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

4.8.4.2 Сквозной проход комментируется


Внутри блока каждая группа операторов либо завершает досрочно switch (с помощью break, continue, return или выбросом исключения), либо помечается комментарием, чтобы обозначить, что выполнение кода будет или может быть продолжено в следующей группе. Достаточно любого комментария, передающего идею сквозного прохода (обычно // fall through). Такой комментарий не требуется в последней группе блока switch. Пример:

switch (input) {  case 1:  case 2:    prepareOneOrTwo();    // fall through  case 3:    handleOneTwoOrThree();    break;  default:    handleLargeNumber(input);}

Обратите внимание, что комментарий ставится не после case 1, а лишь в конце группы операторов.

4.8.4.3 Всегда используйте default


Оператор switch должен содержать метку default, даже если в ней не присутствует код.

Исключение: блок switch для типа enum может не использовать default, если он содержит явные случаи, охватывающие все возможные значения этого типа. Это позволяет среде разработки или другим инструментам статического анализа выдавать предупреждение о том, что некоторые случаи не охвачены.

4.8.5 Аннотации


Аннотации, применяемые к классу, методу или конструктору, следуют непосредственно после блока документации. Каждая аннотация указывается на собственной строке (то есть по одной аннотации на строку). Эти разрывы строки не являются переносами строки (см. Раздел 4.5), поэтому уровень отступа не увеличивается. Пример:

@Override@Nullablepublic String getNameIfPresent() { ... }

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

@Override public int hashCode() { ... }

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

@Partial @Mock DataLoader loader;

Не существует особых правил для форматирования аннотаций к параметрам, локальным переменным или типам.

4.8.6 Комментарии


Этот раздел посвящен комментариям реализации. Javadoc рассматривается отдельно в Разделе 7.

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

4.8.6.1 Стиль блочного комментария


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

/* * This is          // And so           /* Or you can * okay.            // is this.          * even do this. */ */

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

При написании многострочных комментариев используйте стиль /* */, если хотите, чтобы средства автоматического форматирования кода делали перенос строки при необходимости (в стиле параграфов). Большинство средств форматирования не могут делать этого с блоками однострочных комментариев //

4.8.7 Модификаторы


Модификаторы класса и полей, если они присутствуют, отображаются в порядке, рекомендованном спецификацией языка Java:

public protected private abstract default static final transient volatile synchronized native strictfp

4.8.8 Числовые литералы


Тип long использует прописную букву L, а не строчную (чтобы не перепутать с цифрой 1). Например, 300_000_000L вместо 300_000_000l.

5 Именование


5.1 Общие правила для всех идентификаторов


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

Таким образом, каждому действительному имени идентификатора соответствует регулярное выражение \w+ (буквенно-цифровой символ, встречающийся один или более раз).

Стилю данного руководства не соответствуют имена, использующие специальные суффиксы или префиксы, например: name_, mName, s_name или kName.

5.2 Правила для разных типов идентификаторов


5.2.1 Имена пакетов


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

Правильно: com.example.deepspace
Неправильно: com.example.deepSpace или com.example.deep_space

5.2.2 Имена классов


Имена классов пишутся в стиле UpperCamelCase (с заглавной первой буквой).

Имена классов обычно являются существительными или словосочетаниями с существительными. Например, Character или ImmutableList.

Имена интерфейсов также могут быть существительными или словосочетаниями с существительными (например, List), но иногда могут быть и прилагательными или сочетаниями прилагательных (например, Readable).

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

Тестовые классы носят имя, которое начинается с имени класса, который они тестируют, и заканчивается словом Test. Например, HashTest или HashIntegrationTest.

5.2.3 Имена методов


Имена методов пишутся в стиле lowerCamelCase.

Имена методов обычно являются глаголами или словосочетаниями с глаголами. Например, sendMessage или stop.

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

<methodUnderTest>_<state>, например, pop_emptyStack

Не существует единственно верного пути в именовании тестовых методов.

5.2.4 Имена констант


Константы именуются в стиле CONSTANT_CASE: все буквы в верхнем регистре, каждое слово отделено от следующего подчеркиванием. Но что именно является константой?

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

Примеры:

// Константыstatic final int NUMBER = 5;static final ImmutableList<String> NAMES = ImmutableList.of("Ed", "Ann");static final ImmutableMap<String, Integer> AGES = ImmutableMap.of("Ed", 35, "Ann", 32);static final Joiner COMMA_JOINER = Joiner.on(','); // because Joiner is immutablestatic final SomeMutableType[] EMPTY_ARRAY = {};enum SomeEnum { ENUM_CONSTANT }// Не константыstatic String nonFinal = "non-final";final String nonStatic = "non-static";static final Set<String> mutableCollection = new HashSet<String>();static final ImmutableSet<SomeMutableType> mutableElements = ImmutableSet.of(mutable);static final ImmutableMap<String, SomeMutableType> mutableValues =    ImmutableMap.of("Ed", mutableInstance, "Ann", mutableInstance2);static final Logger logger = Logger.getLogger(MyClass.getName());static final String[] nonEmptyArray = {"these", "can", "change"};

Имена констант обычно являются существительными или словосочетаниями с существительными.

5.2.5 Имена не константных полей


Имена полей, не являющихся константами (статических или нет), пишутся в стиле lowerCamelCase.

Имена таких полей обычно являются существительными или словосочетаниями с существительными. Например, computedValues или index.

5.2.6 Имена параметров


Имена параметров пишутся в стиле lowerCamelCase.

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

5.2.7 Имена локальных переменных


Имена локальных переменных пишутся в стиле lowerCamelCase.

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

5.2.8 Имена переменных типа


Каждая переменная типа именуется согласно одному из двух стилей:

  • Одиночная заглавная буква, за которой может следовать обычное число (например, E, T, X, T2)
  • Имя в виде имени класса (см. Раздел 5.2.2), за которым следует заглавная буква T (примеры: RequestT, FooBarT).

5.3 Верблюжий стиль (camelCase)


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

Для повышения предсказуемости, данное руководство задает следующую (примерную) схему.

Начинаем с исходной формы имени:

1. Преобразуйте фразу в обычный ASCII и удалите все апострофы. Например, Mller's algorithm можно преобразовать в Muellers algorithm

2. Разделите полученный результат на слова, отбрасывая пробелы и оставшиеся знаки пунктуации (обычно дефисы):

  • рекомендация: если какое-либо слово уже имеет общепринятую форму в обычном верблюжьем стиле, разделите его на составные части (например, AdWords преобразуется в ad words). Обратите внимание, что такое слово, как iOS, на самом деле не совсем в верблюжьем стиле; оно не соответствует каким-либо соглашениям, поэтому данная рекомендация не применяется.

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

  • в каждом слове, чтобы достичь стиля UpperCamelCase, или
  • в каждом слове, кроме первого, чтобы достичь стиля lowerCamelCase

4. Наконец, соедините все слова в одиночный идентификатор

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

Примеры:
Исходная форма Правильно Неправильно
XML HTTP request XmlHttpRequest XMLHTTPRequest
new customer ID newCustomerId newCustomerID
inner stopwatch innerStopwatch innerStopWatch
supports IPv6 on iOS? supportsIpv6OnIos supportsIPv6OnIOS
YouTube importer YouTubeImporter
YoutubeImporter*

*Допускается, но не рекомендуется.

Примечание: некоторые слова в английском языке используют дефис неоднозначно: например, оба слова nonempty и non-empty являются правильными, поэтому имена методов checkNonempty и checkNonEmpty также являются правильными.

6 Практика программирования


6.1 Используйте всегда аннотацию &commat;Override


Метод помечается аннотацией &commat;Override всякий раз, когда он действительно переопределяется. Это относится как к методу класса-потомка, переопределяющему метод класса-родителя, так и к методу интерфейса, переопределяющему метод супер-интерфейса.

Исключение: аннотация может быть опущена, если родительский метод помечен аннотацией &commat;Deprecated.

6.2 Не игнорируйте пойманные исключения


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

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

try {  int i = Integer.parseInt(response);  return handleNumericResponse(i);} catch (NumberFormatException ok) {  // it's not numeric; that's fine, just continue}return handleTextResponse(response);

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

try {  emptyStack.pop();  fail();} catch (NoSuchElementException expected) {}

6.3 Для статических членов используйте имя класса


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

Foo aFoo = ...;Foo.aStaticMethod(); // хорошоaFoo.aStaticMethod(); // плохоsomethingThatYieldsAFoo().aStaticMethod(); // очень плохо

6.4 Не используйте финализаторы


Крайней редко необходимо переопределить метод Object.finalize.

Подсказка:

Не делайте этого. Если вам действительно это нужно, сначала прочитайте и очень тщательно осмыслите Effective Java Item 7, Avoid Finalizers, а затем не делайте этого.

7. Javadoc


7.1 Форматирование


7.1.1 Основная форма


Простое форматирование блоков Javadoc соответствует данному примеру:

/** * Multiple lines of Javadoc text are written here, * wrapped normally... */public int method(String p1) { ... }

или в одну строку:

/** An especially short bit of Javadoc. */

Простая форма применима всегда. Однострочная форма может быть применена, когда весь блок Javadoc (включая маркеры комментариев) может поместиться на одной строке. Обратите внимание, что это применимо лишь тогда, когда в блоке нет таких тэгов, как &commat;return.

7.1.2 Параграфы


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

7.1.3 Блочные тэги


Все блочные тэги следуют в таком порядке: &commat;param, &commat;return, &commat;throws, &commat;deprecated, и эти четыре типа никогда не присутствуют с пустым описанием. Если тег блока не помещается на одной строке, строки продолжения имеют отступ в четыре (или более) пробела от @.

7.2 Итоговый фрагмент


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

Этот фрагмент словосочетание с существительным или глаголом, а не с полным предложением. Он не начинается с A {&commat;code Foo} is a или This method returns, и не образует законченного утвердительного предложения, как например Save the record. Однако этот фрагмент пишется с заглавной буквы, и в нем проставляются знаки пунктуации, как если бы это было законченное предложение.

Подсказка: распространенной ошибкой является написание простого Javadoc в виде /** &commat;return the customer ID */. Это неверно и должно быть исправлено на /** Returns the customer ID. */.

7.3 Когда применяется Javadoc


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

7.3.1 Исключение: методы, описывающие себя


Javadoc опционален для простых и очевидных методов, таких как getFoo, в тех случаях, когда действительно нельзя сказать ничего более, чем Возвращает foo.

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

Например, для метода с названием getCanonicalName не опускайте документацию (с тем основанием, что имя метода говорит лишь /** Возвращает каноническое имя. */), если обычный человек, читающий код, может и не подозревать, что означает термин каноническое имя!

7.3.2 Исключение: переопределение

Javadoc не всегда сопровождает метод, который переопределяет метод из супер-класса (или супер-интерфейса).

7.3.4 Необязательный Javadoc


Прочие классы и члены сопровождаются Javadoc по необходимости или по желанию.

Всякий раз, когда комментарий реализации будет использоваться для определения общей цели или поведения класса или члена, этот комментарий записывается как Javadoc (с использованием /**).

Необязательный Javadoc не обязательно должен следовать правилам форматирования Разделов 7.1.2, 7.1.3 и 7.2, хотя это, конечно, рекомендуется.

Данный перевод так же доступен в нашем блоге
Подробнее..

Перевод Чистый код пять ключевых моментов из обязательной к прочтению книги для программистов

08.09.2020 10:16:23 | Автор: admin
Недавно я написал о Пять книг, которые изменили то как я кодирую. В комментариях несколько читателей рекомендовали Чистый код Роберта С. Мартина. В результате я прочитал книгу и нашел ее достойной углубленного обзора.


О книге


Чистый код был опубликован в 2008 году, и в последние годы он неизменно входит в пятерку самых продаваемых книг на Amazon. Автор, которого ласково называют Дядя Боб, был одним из первых авторов Agile Manifesto и имеет некоторые серьезные полномочия. Книга получила средний рейтинг 4,4 на Goodreads из более чем 13 000 оценок. Достаточно сказать, что это одна из тех книг, которую должен прочитать каждый программист.

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

1. Программирование это прикладное искусство


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

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

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

Хороший код. Источник: xkcd

Но как перейти от простого написания кода к искусству программирования?

По словам Мартина, основными инструментами, которыми мы располагаем, являются непрерывный рефакторинг и разработка на основе тестирования (TDD). Они неотделимы друг от друга, как две стороны медали. Вот некоторые определения.

Рефакторинг это процесс реструктуризации существующего программного кода без изменения его внешнего поведения.

Разработка через тестирование это процесс, в котором требования превращаются в конкретные тестовые сценарии, а затем пишется код и проводится успешное тестирование.

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

  1. Напишите тесты на еще не разработанную функциональность. Естественно, они не будут работать.
  2. Реализуйте функциональность так, чтобы тесты выполнялись успешно. Пусть даже код сначала будет не самым хорошим.
  3. Итеративно проводите рефакторинг кода не ломая тесты. С каждым разом ваш код будет становится все более чистым.

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

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

2. Функции должны быть короткими!


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

Роберт С. Мартин

По словам Мартина, это означает две вещи.

  1. Функции должны быть короткими не длиннее 20 строк и в большинстве случаев менее 10 строк.
  2. Функции должны иметь как можно меньше аргументов, желательно ни одного.

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

Автор книги делает аналогичное замечание о классах. По идее класс должен отвечать только за одну вещь. Это известно как принцип единственной ответственности (SRP).

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

Делай код самодокументирующимся


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

Роберт С. Мартин

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


После рефакторинга:


Примечания:

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

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

Вот они.

  • Используйте имена, раскрывающие намерения например, int elapsedTimeInDays, а не int days
  • Используйте произносимые имена например, Customer, а не DtaRcrd102
  • Избегайте кодировок не используйте префикс m_ и не используйте венгерскую нотацию.
  • Выберите одно слово для каждой концепции не используйте разные наименования типа fetch, retrieve, get для одной и той же операции по сути.

4. Абстракция важна


Абстракция. Источник: Abstruse Goose

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

Мартин иллюстрирует это следующим примером из FitNesse:


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


Примечания:

  • Функция render () теперь отвечает только за создание тега hr.
  • Низкоуровневые детали построения тега теперь делегируются модулю HtmlTag.
  • Форматирование размера абстрагировано в отдельную функцию.

По словам Мартина:

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

Теперь я буду уделять этому большее внимание при написании кода в будущем.

5. Чистый код это тяжелая работа и соблюдение принципов


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

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

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

Роберт С. Мартин

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

Заключение


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

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

P.s. Спасибо Zack Shapiro.


В ЛАНИТ есть вакансии в области разработки. Те, кто ценит эту книгу, милости просим к нам.

Подробнее..

Перевод Творческое использование методов расширения в C

12.09.2020 10:23:52 | Автор: admin
Привет, Хабр!

Продолжая исследование темы C#, мы перевели для вас следующую небольшую статью, касающуюся оригинального использования extension methods. Рекомендуем обратить особое внимание на последний раздел, касающийся интерфейсов, а также на профиль автора.




Уверен, что любой, хотя бы немного имевший дело с C#, знает о существовании методов расширений (extension methods). Это приятная фича, позволяющая разработчикам расширять имеющиеся типы новыми методами.

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

Добавление методов к перечислениям

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

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

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

public enum FileFormat{    PlainText,    OfficeWord,    Markdown}


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

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

public static class FileFormatExtensions{    public static string GetFileExtension(this FileFormat self)    {        if (self == FileFormat.PlainText)            return "txt";        if (self == FileFormat.OfficeWord)            return "docx";        if (self == FileFormat.Markdown)            return "md";        // Будет выброшено, если мы забудем новый формат файла,        // но забудем добавить соответствующее расширение файла        throw new ArgumentOutOfRangeException(nameof(self));    }}


Что, в свою очередь, позволяет нам поступить так:

var format = FileFormat.Markdown;var fileExt = format.GetFileExtension(); // "md"var fileName = $"output.{fileExt}"; // "output.md"


Рефакторинг классов моделей

Бывает, что вы не хотите добавлять метод непосредственно к классу, например, если работаете с анемичной моделью.

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

Итак, рассмотрим следующий пример с двумя моделями: одна представляет закрытый список титров, а другая отдельную строку титров:

public class ClosedCaption{    // Отображаемый текст    public string Text { get; }    // Когда он отображается относительно начала трека     public TimeSpan Offset { get; }    // Как долго текст остается на экране     public TimeSpan Duration { get; }    public ClosedCaption(string text, TimeSpan offset, TimeSpan duration)    {        Text = text;        Offset = offset;        Duration = duration;    }}public class ClosedCaptionTrack{    // Язык, на котором написаны субтитры    public string Language { get; }    // Коллекция закрытых надписей    public IReadOnlyList<ClosedCaption> Captions { get; }    public ClosedCaptionTrack(string language, IReadOnlyList<ClosedCaption> captions)    {        Language = language;        Captions = captions;    }}


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

var time = TimeSpan.FromSeconds(67); // 1:07var caption = track.Captions    .FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);


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

public static class ClosedCaptionTrackExtensions{    public static ClosedCaption GetByTime(this ClosedCaptionTrack self, TimeSpan time) =>        self.Captions.FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);}


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

  1. Понятно, что этот метод работает только с публичными членами класса и не изменяет его приватного состояния каким-нибудь таинственным образом.
  2. Очевидно, что этот метод просто позволяет срезать угол и предусмотрен здесь только для удобства.
  3. Этот метод относится к совершенно отдельному классу (или даже сборке), назначение которых отделять данные от логики.


В целом, при использовании подхода с методами расширений удобно проводить линию между необходимым и полезным.

Как сделать интерфейсы разностороннее

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

Если вам это кажется нонсенсом, рассмотрим типичный интерфейс, сохраняющий модель в файл:

public interface IExportService{    FileInfo SaveToFile(Model model, string filePath);}


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

Таким образом, чтобы выполнить это требование, мы добавляем к контракту новый метод:

public interface IExportService{    FileInfo SaveToFile(Model model, string filePath);    byte[] SaveToMemory(Model model);}


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

Но, чтобы не делать всего этого, мы могли с самого начала спроектировать интерфейс немного иначе:

public interface IExportService{    void Save(Model model, Stream output);}


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

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

К счастью, этот недостаток полностью обнуляется при использовании методов расширений:

public static class ExportServiceExtensions{    public static FileInfo SaveToFile(this IExportService self, Model model, string filePath)    {        using (var output = File.Create(filePath))        {            self.Save(model, output);            return new FileInfo(filePath);        }    }    public static byte[] SaveToMemory(this IExportService self, Model model)    {        using (var output = new MemoryStream())        {            self.Save(model, output);            return output.ToArray();        }    }}


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

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

Перевод Наследование реализации в С. Реальная история

24.10.2020 14:21:16 | Автор: admin
Привет, Хабр!

В поисках вдохновения, чем бы пополнить портфель издательства на тему С++, мы набрели на возникший словно из ниоткуда блог Артура О'Дуайера, кстати, уже написавшего одну книгу по C++. Сегодняшняя публикация посвящена теме чистого кода. Надеемся, что вам будут интересны как сам кейс, так и автор.


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

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

Этап 1: Транзакции



Допустим, наша предметная область банковские транзакции. У нас классическая полиморфная иерархия интерфейсов.

class Txn { ... };class DepositTxn : public Txn { ... };class WithdrawalTxn : public Txn { ... };class TransferTxn : public Txn { ... };


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

class Txn {public:    AccountNumber account() const;    std::string name_on_account() const;    Money amount() const;private:    // виртуальная всячина};class DepositTxn : public Txn {public:    std::string name_of_customer() const;};class TransferTxn : public Txn {public:    AccountNumber source_account() const;};


Этап 2: Фильтры транзакций



Но на самом деле наша программа не исполняет транзакции, а отслеживает их, чтобы помечать подозрительные транзакции. Человек-оператор может выставить фильтры, соответствующие определенным критериям, например, помечать все транзакции на сумму свыше $ 10 000 или помечать все транзакции, выполненные от лица людей, занесенных в контрольный список W. Внутри программы мы представляем различные типы фильтров, конфигурируемые оператором, как классическую полиморфную иерархию.

class Filter { ... };class AmountGtFilter : public Filter { ... };class NameWatchlistFilter : public Filter { ... };class AccountWatchlistFilter : public Filter { ... };class DifferentCustomerFilter : public Filter { ... };class AndFilter : public Filter { ... };class OrFilter : public Filter { ... };Все фильтры имеют ровно один и тот же публичный API.class Filter {public:    bool matches(const Txn& txn) const {        return do_matches(txn);    }private:    virtual bool do_matches(const Txn&) const = 0;};


Вот пример простого фильтра:

class AmountGtFilter : public Filter {public:    explicit AmountGtFilter(Money x) : amount_(x) { }private:    bool do_matches(const Txn& txn) const override {        return txn.amount() > amount_;    }    Money amount_;};


Этап 3: Оступились в первый раз



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

class DifferentCustomerFilter : public Filter {    bool do_matches(const Txn& txn) const override {        if (auto *dtxn = dynamic_cast<const DepositTxn*>(&txn)) {            return dtxn->name_of_customer() != dtxn->name_on_account();        } else {            return false;        }    }};


Это классическое злоупотребление dynamic_cast! (Классическое потому что встречается сплошь и рядом). Чтобы поправить этот код, можно было бы попытаться применить способ из Classically polymorphic visit (2020-09-29), но, к сожалению, никаких улучшений не наблюдается:

class DifferentCustomerFilter : public Filter {    bool do_matches(const Txn& txn) const override {        my::visit<DepositTxn>(txn, [](const auto& dtxn) {            return dtxn.name_of_customer() != dtxn.name_on_account();        }, [](const auto&) {            return false;        });    }};


Поэтому автора кода осенила (сарказм!) идея. Давайте реализуем в некоторых фильтрах чувствительность к регистру. Перепишем базовый класс Filter вот так:

class Filter {public:    bool matches(const Txn& txn) const {        return my::visit<DepositTxn, WithdrawalTxn, TransferTxn>(txn, [](const auto& txn) {            return do_generic(txn) && do_casewise(txn);        });    }private:    virtual bool do_generic(const Txn&) const { return true; }    virtual bool do_casewise(const DepositTxn&) const { return true; }    virtual bool do_casewise(const WithdrawalTxn&) const { return true; }    virtual bool do_casewise(const TransferTxn&) const { return true; }};class LargeAmountFilter : public Filter {    bool do_generic(const Txn& txn) const override {        return txn.amount() > Money::from_dollars(10'000);    }};class DifferentCustomerFilter : public Filter {    bool do_casewise(const DepositTxn& dtxn) const override {        return dtxn.name_of_customer() != dtxn.name_on_account();    }    bool do_casewise(const WithdrawalTxn&) const override { return false; }    bool do_casewise(const TransferTxn&) const override { return false; }};


Благодаря такой умной тактике уменьшается объем кода, который необходимо написать в DifferentCustomerFilter. Но мы нарушаем один из принципов ООП, а именно, запрет на наследование реализации. Функция Filter::do_generic(const Txn&) ни чиста, ни финальна. Это нам еще аукнется.

Этап 4: Оступились во второй раз

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

class NameWatchlistFilter : public Filter {protected:    bool is_flagged(std::string_view name) const {        for (const auto& list : watchlists_) {            if (std::find(list.begin(), list.end(), name) != list.end()) {                return true;            }        }        return false;    }private:    bool do_generic(const Txn& txn) const override {        return is_flagged(txn.name_on_account());    }    std::vector<std::list<std::string>> watchlists_;};


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

class WideNetFilter : public NameWatchlistFilter {    bool do_generic(const Txn& txn) const override {        return true;    }    bool do_casewise(const DepositTxn& txn) const override {        return is_flagged(txn.name_on_account()) || is_flagged(txn.name_of_customer());    }    bool do_casewise(const WithdrawalTxn& txn) const override {        return is_flagged(txn.name_on_account());    }    bool do_casewise(const TransferTxn& txn) const override {        return is_flagged(txn.name_on_account());    }};


Обратите внимание, как жутко перепутана получившаяся у нас архитектура. NameWatchlistFilter переопределяет do_generic, чтобы только проверить фамилию владельца счета, затем WideNetFilter переопределяет его обратно к исходному виду. (Если бы WideNetFilter не переопределил его обратно, то WideNetFilter неверно срабатывал бы при любой транзакции по внесению средств, где name_on_account() не помечен, а name_of_customer() помечен.) Это путано, но работает пока.

Этап 5: Ряд неприятных событий



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

class FeeTxn : public Txn { ... };class Filter {public:    bool matches(const Txn& txn) const {        return my::visit<DepositTxn, WithdrawalTxn, TransferTxn, FeeTxn>(txn, [](const auto& txn) {            return do_generic(txn) && do_casewise(txn);        });    }private:    virtual bool do_generic(const Txn&) const { return true; }    virtual bool do_casewise(const DepositTxn&) const { return true; }    virtual bool do_casewise(const WithdrawalTxn&) const { return true; }    virtual bool do_casewise(const TransferTxn&) const { return true; }    virtual bool do_casewise(const FeeTxn&) const { return true; }};


Важнейший шаг: мы забыли обновить WideNetFilter, добавить к нему переопределитель для WideNetFilter::do_casewise(const FeeTxn&) const. В данном игрушечном примере такая ошибка, возможно, сразу кажется непростительной, но в реальном коде, где от одного переопределителя до другого десятки строк кода, а идиома невиртуального интертфейса не слишком ревностно соблюдается думаю, не составит труда встретить class WideNetFilter : public NameWatchlistFilter, переопределяющий как do_generic, так и do_casewise, и подумать: о, что-то здесь запутано. Вернусь к этому позже (и никогда к этому не вернуться).

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

Делаем рефакторинг, избавляемся от наследования реализации. Шаг 1

Чтобы избавиться от наследования реализации в Filter::do_casewise, применим постулат Скотта Майерса о том, что любой виртуальный метод должен быть либо чистым, либо (логически) финальным. В данном случае это компромисс, так как здесь же мы нарушаем правило о том, что иерархии должны быть неглубокими. Вводим промежуточный класс:

class Filter {public:    bool matches(const Txn& txn) const {        return do_generic(txn);    }private:    virtual bool do_generic(const Txn&) const = 0;};class CasewiseFilter : public Filter {    bool do_generic(const Txn&) const final {        return my::visit<DepositTxn, WithdrawalTxn, TransferTxn>(txn, [](const auto& txn) {            return do_casewise(txn);        });    }    virtual bool do_casewise(const DepositTxn&) const = 0;    virtual bool do_casewise(const WithdrawalTxn&) const = 0;    virtual bool do_casewise(const TransferTxn&) const = 0;};


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

class LargeAmountFilter : public Filter {    bool do_generic(const Txn& txn) const override {        return txn.amount() > Money::from_dollars(10'000);    }};class DifferentCustomerFilter : public CasewiseFilter {    bool do_casewise(const DepositTxn& dtxn) const override {        return dtxn.name_of_customer() != dtxn.name_on_account();    }    bool do_casewise(const WithdrawalTxn&) const override { return false; }    bool do_casewise(const TransferTxn&) const override { return false; }};


Если кто-то добавит новый тип транзакции и изменит CasewiseFilter так, чтобы включить в него новую перегрузку do_casewise, то мы увидим, что DifferentCustomerFilter стал абстрактным классом: нам придется иметь дело с транзакцией нового типа. Теперь компилятор помогает соблюдать правила нашей архитектуры, там, где ранее тихонько скрывал наши ошибки.

Также обратите внимание, что теперь невозможно определить WideNetFilter в терминах NameWatchlistFilter.

Делаем рефакторинг, избавляемся от наследования реализации. Шаг 2



Чтобы избавиться от наследования реализации в WideNetFilter, применим принцип единственной ответственности. На данный момент NameWatchlistFilter решает две задачи: работает в качестве полноценного фильтра и владеет возможностью is_flagged. Давайте выделим is_flagged в самостоятельный класс WatchlistGroup, которому не требуется соответствовать API class Filter, так как он не фильтр, а просто полезный вспомогательный класс.

class WatchlistGroup {public:    bool is_flagged(std::string_view name) const {        for (const auto& list : watchlists_) {            if (std::find(list.begin(), list.end(), name) != list.end()) {                return true;            }        }        return false;    }private:    std::vector<std::list<std::string>> watchlists_;};


Теперь WideNetFilter может использовать is_flagged, не наследуя NameWatchlistFilter. В обоих фильтрах можно следовать известной рекомендации и предпочитать композицию наследованию, так как композиция это инструмент для переиспользования кода, а наследование нет.

class NameWatchlistFilter : public Filter {    bool do_generic(const Txn& txn) const override {        return wg_.is_flagged(txn.name_on_account());    }    WatchlistGroup wg_;};class WideNetFilter : public CasewiseFilter {    bool do_casewise(const DepositTxn& txn) const override {        return wg_.is_flagged(txn.name_on_account()) || wg_.is_flagged(txn.name_of_customer());    }    bool do_casewise(const WithdrawalTxn& txn) const override {        return wg_.is_flagged(txn.name_on_account());    }    bool do_casewise(const TransferTxn& txn) const override {        return wg_.is_flagged(txn.name_on_account());    }    WatchlistGroup wg_;};


Опять же, если кто-то добавит новый тип транзакций и модифицирует CasewiseFilter, чтобы включить в него новую перегрузку do_casewise, то мы убедимся, что WideNetFilter стал абстрактным классом: нам приходится иметь дело с транзакциями нового типа в WideNetFilter (но не в NameWatchlistFilter). Словно компилятор сам ведет для нас список дел!

Выводы



Я анонимизировал и исключительно упростил этот пример по сравнению с тем кодом, с которым мне пришлось работать. Но общие очертания иерархии классов были именно такими, равно как и хлипкая логика do_generic(txn) && do_casewise(txn) из оригинального кода. Думаю, из изложенного не так легко понять, насколько незаметно в старой структуре прятался баг FeeTxn. Теперь, когда я изложил перед вами эту упрощенную версию (буквально разжевал ее вам!), я уже и сам практически удивляюсь, а так ли плоха была исходная версия кода? Может и нет в конце концов, этот код же работал какое-то время.
Но, надеюсь, вы согласитесь, что версия после рефакторинга, использующая CasewiseFilter и особенно WatchlistGroup, получилась гораздо лучше. Если бы мне пришлось выбирать, с какой из двух этих баз кода работать, то я без колебаний взял бы вторую.
Подробнее..

Перевод О C и объектно-ориентированном программировании

16.11.2020 10:18:38 | Автор: admin
Привет, Хабр!

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


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

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

Об объектно-ориентированном программировании (ООП) как инструменте



Хотя C++ и описывается как мультипарадигмальный язык программирования, на практике большинство программистов используют C++ сугубо как объектно-ориентированный язык (обобщенное программирование используется для дополнения ООП).

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

Об энтропии как тайной силе, подпитывающей разработку ПО



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

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

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

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

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

В жизненном цикле любого ООП-проекта рано или поздно наступает такой момент, после которого поддерживать его невозможно. Как правило, в такой момент следует предпринять одно из двух действий:

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


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

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

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

О легкости удаления кода как о принципе проектирования



В любой системе, построенной по принципу ООП, именно объектам в составе созвездия уделяется основное внимание. Но я считаю, что взаимосвязи между объектами важны не менее, если не более, чем сами объекты.

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

О производительности по определению



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

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

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

В данном случае наиболее бросается в глаза, какую кучу кода генерируют компиляторы, даже после оптимизации. Затем, присмотревшись, можно заметить, как затратно и при этом бесполезно такое сопровождение: когда программе передается ненулевое количество аргументов, код все равно выделяет память (вызов new), загружает адреса vtable обоих объектов, загружает адрес функции Work() для ImplB и перескакивает к ней, чтобы затем сразу же вернуться, так как делать там нечего. Наконец, вызывается delete, чтобы высвободить выделенную память.

Ни одна из этих операций совершенно не была необходимой, но процессор исправно исполнил их все.

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

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

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

О борьбе со стереотипами



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

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

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

Правда, за годы работы я выделил для себя три основных аргумента, из-за которых люди не готовы дать шанс другой стороне:

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


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

Заметки о codestyle

10.02.2021 14:12:50 | Автор: admin

Довольно часто сталкиваюсь с вопросом касательно качества кода: "Почему написано именно так, а не так?". И я объясняю каждый раз. Именно поэтому решил составить эдакую заметку с некоторыми примерами и объяснениями.

Второй возникающий вопрос: "Где научился так красиво писать?". Ответ на который будет к концу статьи.

Приведу три примера, с которыми сталкиваюсь чаще всего.

Пример 1

use App\Models\Payment;use Illuminate\Database\Eloquent\Model;class Foo extends Model {  public function user(){    return $this->hasOne('App\Models\User');  }  public function payments(){    return $this->hasMany(Payment::class);  }}

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

Начнём с того, что открывающиеся фигурные скобки лучше переносить на новую строку. Это позволит сделать код более приятным в чтении (Привет, PSR-12).

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

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

class Foo{    public function user()    {        //    }}

То у другого он может быть таким:

class Foo{        public function user()        {                //        }}

Или таким:

class Foo{  public function user()  {    //  }}

Согласитесь, разница ощутима. А символ "пробела" он и в Африке символ "пробела", отображающийся на всех устройствах одинаково.

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

Итак, после ревью код будет выглядеть так:

use App\Models\Payment;use App\Models\User;use Illuminate\Database\Eloquent\Model;class Foo extends Model{    public function user()    {        return $this->hasOne(User::class);    }    public function payments()    {        return $this->hasMany(Payment::class);    }}

Пример 2

Недавно встретил такой участок кода (вставлю картинкой, чтобы не нарушить его вид):

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

Данный участок очень плохо читается. Приходится вычитывать каждое слово с целью понять что в нём происходит и не пропустил ли чего при чтении.

Проведём код-ревью:

class NewsService{    public function sendNews(EmployerNews $employerNews)    {        $this->dispatch($employerNews, WorkPositionIdsJob::class);        $this->dispatch($employerNews, WorkPositionTypesJob::class);        $this->dispatch($employerNews, EmployerIdsJob::class);    }    protected function dispatch(EmployerNews $news, string $class): void    {        $job   = $this->job($news, $class);        $delay = $this->delay();        dispatch($job)->delay($delay);    }    protected function job(EmployerNews $news, string $job): AbstractJob    {        return new $job($news->getAttributes());    }    protected function delay(): Carbon    {        return Carbon::now()->addSecond();    }}

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

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

То же самое касается и примерно таких проблемных участков, как:

switch($value){    case 'foo':        //        break;    case 'bar':        //        break;}
if ($employerPriceCategory->default) {    return false;}$defaultCategory = EmployerPriceCategory::where('default', true)    ->first(['id']);Employer::query()    ->where('employer_price_category_id', $employerPriceCategory->id)    ->update([        'employer_price_category_id' => $defaultCategory->id,    ]);
$districts = $this->getDistricts();$this->output->writeln('districts: ' . sizeof($districts));$period = $this->getPeriod();foreach ($period as $start) {    $end = $start->copy()->endOfDay();    $this->output->writeln('date = ' . $start->format('Y-m-d'));    //}

Это же просто ужас! Как такое вообще можно читать? А писать?!

Давайте отрефакторим их!

switch($value){    case 'foo':        //        break;    case 'bar':        //        break;}
if ($employerPriceCategory->default) {    return false;}$defaultCategory = EmployerPriceCategory::query()    ->select(['id'])    ->where('default', true)    ->first();Employer::query()    ->where('employer_price_category_id', $employerPriceCategory->id)    ->update(['employer_price_category_id' => $defaultCategory->id]);

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

Пример 3

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

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

Для примера возьмём следующий участок:

$salaryFirstBaseEmployer = $employer->salaryBases->first()->salary_base;$salaryLastBaseEmployer = $employer->salaryBases->last()->salary_base;$begin = Carbon::parse('2020-05-01')->startOfMonth()->startOfDay();$end = $begin->clone()->endOfMonth()->endOfDay();$endMonth = $begin->clone()->endOfMonth();$startPeriod = new CarbonPeriod($begin, $end);$endPeriod = new CarbonPeriod($begin, $end);

В нём мы видим непонятный массив текста. Отформатируем его, разбив на логические блоки:

$salaryFirstBaseEmployer = $employer->salaryBases->first()->salary_base;$salaryLastBaseEmployer  = $employer->salaryBases->last()->salary_base;$begin = Carbon::parse('2020-05-01')->startOfMonth()->startOfDay();$end      = $begin->clone()->endOfMonth()->endOfDay();$endMonth = $begin->clone()->endOfMonth();$startPeriod = new CarbonPeriod($begin, $end);$endPeriod   = new CarbonPeriod($begin, $end);

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

Заключение

Очень давно мне дали дельный совет:

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

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

Подробнее..

Сколько методов должно быть в классе?

15.06.2020 14:21:28 | Автор: admin

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

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



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

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

Каждый раз, начиная писать сервис, я долго думаю над названием Бессмысленное AnotherEntityService снова и снова. А что если не должно быть тут никакого существительного? Как название сервиса отражает собранную внутри солянку запросов? Кто вообще пользуется этими методами одним разом?

Меня долго не отпускает пример рефакторинга от дядюшки Боба [Чистый Код, стр. 177]:

class Sql {    constructor(private table: string, private columns: Column[]) {}    public create(): string;    public insert(fields: object[]): string;    public selectAll(): string;    public select(criteria: Criteria): string;    // ...}


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


Вот результат преобразования.

abstract class Sql {    constructor(private table: string, private columns: Column[]) {}    public abstract generate(): string;}class CreateSql extends Sql {    public generate(): string;}class SelectSql extends Sql {    public generate(): string;}// ...


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

Что не менее важно, когда придёт время добавления команды Update, вам не придётся изменять ни один из существующих классов!


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

Я задумался, поскольку от разбиения столько плюсов, почему в реальности преобладают раздутые классы? Потому что мы раздробим классы до уровня функций и станет сложнее понимать общую картину? (А ФПшники по этому поводу комплексуют?) Или потому что из наших толстяков-сервисов инкапсуляция посыплется наружу? Мне кажется, существует темная сторона, которая хранит тайну больших классов.

Что даёт класс в плане программной языковой конструкции, если отбросить романтику? У класса есть светлая сторона публичные методы, которые автоматически формируют интерфейс объекта. Интерфейс образует собой абстракцию, которая должна упрощать программу. Это романтика.

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

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

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

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

Давайте рассмотрим пример из официальной документации Angular.

@Injectable({ providedIn: 'root' })export class HeroService { private heroesUrl = 'api/heroes'; private httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) };  constructor(private http: HttpClient, private messageService: MessageService) {}  getHero(id: number): Observable<Hero> {   const url = `${this.heroesUrl}/${id}`;   return this.http.get<Hero>(url).pipe(     tap((_) => this.log(`fetched hero id=${id}`)),     catchError(this.handleError<Hero>(`getHero id=${id}`)),   ); }  addHero(hero: Hero): Observable<Hero> {   return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(     tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),     catchError(this.handleError<Hero>('addHero')),   ); }  // ...  private handleError<T>(operation = 'operation', result?: T) {   return (error: any): Observable<T> => {     // TODO: send the error to remote logging infrastructure     console.error(error); // log to console instead     // TODO: better job of transforming error for user consumption     this.log(`${operation} failed: ${error.message}`);     return of(result as T);   }; }}


HeroService полностью
@Injectable({ providedIn: 'root' })export class HeroService {  private heroesUrl = 'api/heroes';  // URL to web api  httpOptions = {   headers: new HttpHeaders({ 'Content-Type': 'application/json' }) };  constructor(   private http: HttpClient,   private messageService: MessageService) { }  /** GET heroes from the server */ getHeroes(): Observable<Hero[]> {   return this.http.get<Hero[]>(this.heroesUrl)     .pipe(       tap(_ => this.log('fetched heroes')),       catchError(this.handleError<Hero[]>('getHeroes', []))     ); }  /** GET hero by id. Will 404 if id not found */ getHero(id: number): Observable<Hero> {   const url = `${this.heroesUrl}/${id}`;   return this.http.get<Hero>(url).pipe(     tap(_ => this.log(`fetched hero id=${id}`)),     catchError(this.handleError<Hero>(`getHero id=${id}`))   ); }  /* GET heroes whose name contains search term */ searchHeroes(term: string): Observable<Hero[]> {   if (!term.trim()) {     // if not search term, return empty hero array.     return of([]);   }   return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(     tap(x => x.length ?        this.log(`found heroes matching "${term}"`) :        this.log(`no heroes matching "${term}"`)),     catchError(this.handleError<Hero[]>('searchHeroes', []))   ); }  //////// Save methods //////////  /** POST: add a new hero to the server */ addHero(hero: Hero): Observable<Hero> {   return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(     tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),     catchError(this.handleError<Hero>('addHero'))   ); }  /** DELETE: delete the hero from the server */ deleteHero(hero: Hero | number): Observable<Hero> {   const id = typeof hero === 'number' ? hero : hero.id;   const url = `${this.heroesUrl}/${id}`;    return this.http.delete<Hero>(url, this.httpOptions).pipe(     tap(_ => this.log(`deleted hero id=${id}`)),     catchError(this.handleError<Hero>('deleteHero'))   ); }  /** PUT: update the hero on the server */ updateHero(hero: Hero): Observable<any> {   return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(     tap(_ => this.log(`updated hero id=${hero.id}`)),     catchError(this.handleError<any>('updateHero'))   ); }  /**  * Handle Http operation that failed.  * Let the app continue.  * @param operation - name of the operation that failed  * @param result - optional value to return as the observable result  */ private handleError<T>(operation = 'operation', result?: T) {   return (error: any): Observable<T> => {      // TODO: send the error to remote logging infrastructure     console.error(error); // log to console instead      // TODO: better job of transforming error for user consumption     this.log(`${operation} failed: ${error.message}`);      // Let the app keep running by returning an empty result.     return of(result as T);   }; }  /** Log a HeroService message with the MessageService */ private log(message: string) {   this.messageService.add(`HeroService: ${message}`); }}



Перед нами типичный сервис. (Я бы даже сказал бест практикс). И на первый взгляд всё с ним нормально. HeroService работает с Hero сущностями, ничего лишнего. Много лишнего! Ни один клиент не использует интерфейс согласовано только частями.

Что делать, когда появится новый АПИ метод для Hero? Когда нужно остановиться складывать всё в одно, и разбить класс? Уверен, что каждый задавался этими вопросами. Но само наличие этого сервиса в проекте только смущает разработчиков, подталкивая их к написанию кода через изменение, а не расширение.

Давайте разберёмся, что делает похожими методы, собранные в HeroService, и как это разбивается.

  • Все методы пользуются общим url решается вынесением в константу BASE_HERO_API_URL.
  • Часть методов пользуется одинаковыми http-заголовками решается вынесением в интерсептор. Или можно выделить специальный HttpClient, который обернёт методы get, post, put, delete необходимыми опциями.
  • handleError решается вынесением в отдельный класс, который отлично выступит на роли ответственного по обработке ошибок.


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

Покажи мне код!
@Injectable({ providedIn: 'root' })export class GetHeroesOperation { constructor(   private http: HeroHttpClient,   private messageService: MessageService,   private errorHandler: ErrorHandler, ) {}  execute(): Observable<Hero[]> {   return this.http.get<Hero[]>().pipe(     tap((_) => this.messageService.add('fetched heroes')),     catchError(this.errorHandler.handleError<Hero[]>('getHeroes', [])),   ); }} @Injectable({ providedIn: 'root' })export class AddHeroOperation { constructor(   private http: HeroHttpClient,   private messageService: MessageService,   private errorHandler: ErrorHandler, ) {}  execute(hero: Hero): Observable<Hero> {   return this.http.post<Hero>('', hero).pipe(     tap((newHero: Hero) => this.logAddedHero(newHero)),     catchError(this.errorHandler.handleError<Hero>('addHero')),   ); }  private logAddedHero(newHero: Hero): void {   return this.messageService.add(`AddHeroOperation: added hero w/ id=${newHero.id}`); }} @Injectable({ providedIn: 'root' })export class ErrorHandler { constructor(private messageService: MessageService) {}  handleError<T>(operation: string, result?: T) {   return (error: any): Observable<T> => {     // TODO: send the error to remote logging infrastructure     console.error(error); // log to console instead      // TODO: better job of transforming error for user consumption     this.messageService.add(`${operation} failed: ${error.message}`);      // Let the app keep running by returning an empty result.     return of(result as T);   }; }} @Injectable({ providedIn: 'root' })export class HeroHttpClient { private httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) };  constructor(@Inject(BASE_HERO_API_URL) private baseUrl: string, private http: HttpClient) {}  get<T>(endpoint = ''): Observable<T> {   return this.http.get<T>(this.getUrl(endpoint)); }  private getUrl(endpoint: string): string {   return Location.joinWithSlash(this.baseUrl, endpoint); }  post<T>(endpoint = '', data: unknown): Observable<T> {   return this.http.post<T>(this.getUrl(endpoint), data, this.httpOptions); }  put<T>(endpoint = '', data: unknown): Observable<T> {   return this.http.put<T>(this.getUrl(endpoint), data, this.httpOptions); }  delete<T>(endpoint = ''): Observable<T> {   return this.http.delete<T>(this.getUrl(endpoint), this.httpOptions); }} 



Я не пытаюсь (и не хочу) делать заявлений, типа: У класса не может быть больше 1-го метода!. Конечно может, когда у объекта есть состояние, в силу вступают другие правила. Так же я не против включения в класс методов, которые должны использоваться согласованно: isExecutable, validateParams, execute.

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

Класс не должен содержать в себе набор методов только по причине их схожей реализации.

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

Надеюсь, вам понравилось. Спасибо! Пока!
Подробнее..

Анализ кода систем повышенной надежности

21.07.2020 14:09:04 | Автор: admin
Привет Хабр! В этой статье я хочу поговорить о достаточно мало рассматриваемой теме анализа кода систем повышенной надежности. На хабре много статей о том, что такое хороший статический анализ, но в этой статье я бы хотел рассказать о том, что такое формальная верификация кода, а также объяснить опасность бездумного применения статических анализаторов и стандартов кодирования.

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

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

Опасная трясина MISRA и подобных


Стандарты кодирования предназначены для того, чтобы ограничить использование конструкций языка программирования, которые могут быть потенциально опасны. По идее, это должно повышать качество кода, верно? Да, это обеспечивает качество кода, но всегда важно помнить, что 100% соответствие правилам кодирования не является самоцелью. Если код на 100% соответствует правилам какой-нибудь MISRA, то это совсем не значит, что он хороший и правильный. Можно потратить кучу времени на рефакторинг, вычищение нарушений стандарта кодирования, но все это будет впустую если код в итоге будет работать неправильно или содержать ошибки времени исполнения. Тем более, что правила из MISRA или CERT это обычно только часть стандарта кодирования, принятого на предприятии.

Статический анализ не панацея


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

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

Формальная верификация кода


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

Прежде всего требуется понять, что же это за зверь? Формальная верификация это доказательство безошибочности кода при помощи формальных методов. Звучит страшно, но на самом деле это как доказательство теоремы из матана. Никакой магии тут нет. Данный метод отличается от традиционного статического анализа, так как используется абстрактная интерпретация, а не эвристики. Это дает нам следующее: мы можем доказать, что в коде нет определенных ошибок времени исполнения. Что это за ошибки? Это всякие выходы за границы массива, деление на ноль, переполнение целых и так далее. Их подлость заключается в том, что компилятор соберет код, содержащий такие ошибки (так как такой код синтаксически корректен), но зато при запуске этого кода они проявятся.

Посмотрим на пример. Ниже в спойлерах представлен код для простого ПИ-регулятора:

Посмотреть код
pictrl.c
#include "pi_control.h"/* Global variable definitions */float inp_volt[2];float integral_state;float duty_cycle;float direction;float normalized_error;/* Static functions */static void pi_alg(float Kp, float Ki);static void process_inputs(void);/* control_task implements a PI controller algorithm that ../  *  * - reads inputs from hardware on actual and desired position  * - determines error between actual and desired position  * - obtains controller gains  * - calculates direction and duty cycle of PWM output using PI control algorithm  * - sets PWM output to hardware  *  */void control_task(void){  float Ki;  float Kp;  /* Read inputs from hardware */  read_inputs();  /* Convert ADC values to their respective voltages provided read failure did not occur, otherwise do not update input values */  if (!read_failure)  {    inp_volt[0] = 0.0048828125F * (float) inp_val[0];    inp_volt[1] = 0.0048828125F * (float) inp_val[1];  }    /* Determine error */  process_inputs();    /* Determine integral and proprortional controller gains */  get_control_gains(&Kp,&Ki);    /* PI control algorithm */  pi_alg(Kp, Ki);  /* Set output pins on hardware */  set_outputs();}/* process_inputs  computes the error between the actual and desired position by  * normalizing the input values using lookup tables and then taking the difference */static void process_inputs(void){  /* local variables */  float rtb_AngleNormalization;  float rtb_PositionNormalization;  /* Normalize voltage values */  look_up_even( &(rtb_AngleNormalization), inp_volt[1], angle_norm_map, angle_norm_vals);   look_up_even( &(rtb_PositionNormalization), inp_volt[0], pos_norm_map, pos_norm_vals);   /* Compute error */  normalized_error = rtb_PositionNormalization - rtb_AngleNormalization;}/* look_up_even provides a lookup table algorithm that works for evenly spaced values.  *   * Inputs to the function are...  *     pY - pointer to the output value  *     u - input value  *     map - structure containing the static lookup table data...  *         valueLo - minimum independent axis value  *         uSpacing - increment size of evenly spaced independent axis  *         iHi - number of increments available in pYData  *         pYData - pointer to array of values that make up dependent axis of lookup table   *   */void look_up_even( float *pY, float u, map_data map, float *pYData){  /* If input is below range of lookup table, output is minimum value of lookup table (pYData) */  if (u <= map.valueLo )   {    pY[1] = pYData[1];  }   else   {    /* Determine index of output into pYData based on input and uSpacing */    float uAdjusted = u - map.valueLo;    unsigned int iLeft = uAdjusted / map.uSpacing;/* If input is above range of lookup table, output is maximum value of lookup table (pYData) */    if (iLeft >= map.iHi ) {      (*pY) = pYData[map.iHi];    } /* If input is in range of lookup table, output will interpolate between lookup values */else {      {        float lambda;  // fractional part of difference between input and nearest lower table value        {          float num = uAdjusted - ( iLeft * map.uSpacing );          lambda = num / map.uSpacing;        }        {          float yLeftCast;  // table value that is just lower than input          float yRghtCast;  // table value that is just higher than input          yLeftCast = pYData[iLeft];          yRghtCast = pYData[((iLeft)+1)];          if (lambda != 0) {            yLeftCast += lambda * ( yRghtCast - yLeftCast );          }          (*pY) = yLeftCast;        }      }    }  }}static void pi_alg(float Kp, float Ki){  {    float control_output;float abs_control_output;    /*  y = integral_state + Kp*error   */    control_output = Kp * normalized_error + integral_state;/* Determine direction of torque based on sign of control_output */    if (control_output >= 0.0F) {      direction = TRUE;    } else {      direction = FALSE;    }/* Absolute value of control_output */    if (control_output < 0.0F) {      abs_control_output = -control_output;    } else if (control_output > 0.0F) {  abs_control_output = control_output;}    /* Saturate duty cycle to be less than 1 */    if (abs_control_output > 1.0F) {  duty_cycle = 1.0F;} else {  duty_cycle = abs_control_output;}    /* integral_state = integral_state + Ki*Ts*error */    integral_state = Ki * normalized_error * 1.0e-002F + integral_state;    }}


pi_control.h
/* Lookup table structure */typedef struct {  float valueLo;  unsigned int iHi;  float uSpacing;} map_data;/* Macro definitions */#define TRUE 1#define FALSE 0/* Global variable declarations */extern unsigned short inp_val[];extern map_data angle_norm_map;extern float angle_norm_vals[11];extern map_data pos_norm_map;extern float pos_norm_vals[11];extern float inp_volt[2];extern float integral_state;extern float duty_cycle;extern float direction;extern float normalized_error;extern unsigned char read_failure;/* Function declarations */void control_task(void);void look_up_even( float *pY, float u, map_data map, float *pYData);extern void read_inputs(void);extern void set_outputs(void);extern void get_control_gains(float* c_prop, float* c_int);



Запустим проверку при помощи Polyspace Bug Finder, сертифицируемого и квалифицируемого статического анализатора и получим такие результаты:



Для удобства, сведем результаты в таблицу:

Посмотреть результаты
Дефект
Описание
Строка
Non-initialized variable
Local variable 'abs_control_output' may be read before being initialized.
159
Float division by zero
Divisor is 0.0.
99
Array access out of bounds
Attempt to access element out of the array bounds.
Valid index range starts at 0.
38
Array access out of bounds
Attempt to access element out of the array bounds.
Valid index range starts at 0.
39
Pointer access out of bounds
Attempt to dereference pointer outside of the pointed object at offset 1.
93


А теперь верифицируем этот же код при помощи инструмента формальной верификации Polyspace Code Prover:


Зеленый цвет в результатах это код, для которого отсутствие ошибок времени выполнения было доказано. Красный доказана ошибка. Оранжевый инструменту не хватило данных. Результаты, помеченные зеленым цветом самые интересные. Если для части кода доказано отсутствие ошибки времени выполнения, то для этой части кода можно значительно сократить объем тестирования (например, тестирование на робастность уже можно не проводить) А теперь, посмотрим на сводную таблицу потенциальных и доказанных ошибок:

Посмотреть результаты
Проверка
Строка
Описание
Out of bounds array index
38
Warning: array index may be outside bounds: [array size undefined]
Out of bounds array index
39
Warning: array index may be outside bounds: [array size undefined]
Overflow
70
Warning: operation [-] on float may overflow (on MIN or MAX bounds of FLOAT32)
Illegally dereferenced pointer
93
Error: pointer is outside its bounds
Overflow
98
Warning: operation [-] on float may overflow (result strictly greater than MAX FLOAT32)
Division by zero
99
Warning: float division by zero may occur
Overflow
99
Warning: operation [conversion from float32 to unsigned int32] on scalar may overflow (on MIN or MAX bounds of UINT32)
Overflow
99
Warning: operation [/] on float may overflow (on MIN or MAX bounds of FLOAT32)
Illegally dereferenced pointer
104
Warning: pointer may be outside its bounds
Overflow
114
Warning: operation [-] on float may overflow (result strictly greater than MAX FLOAT32)
Overflow
114
Warning: operation [*] on float may overflow (on MIN or MAX bounds of FLOAT32)
Overflow
115
Warning: operation [/] on float may overflow (on MIN or MAX bounds of FLOAT32)
Illegally dereferenced pointer
121
Warning: pointer may be outside its bounds
Illegally dereferenced pointer
122
Warning: pointer may be outside its bounds
Overflow
124
Warning: operation [+] on float may overflow (on MIN or MAX bounds of FLOAT32)
Overflow
124
Warning: operation [*] on float may overflow (on MIN or MAX bounds of FLOAT32)
Overflow
124
Warning: operation [-] on float may overflow (on MIN or MAX bounds of FLOAT32)
Overflow
142
Warning: operation [*] on float may overflow (on MIN or MAX bounds of FLOAT32)
Overflow
142
Warning: operation [+] on float may overflow (on MIN or MAX bounds of FLOAT32)
Non-uninitialized local variable
159
Warning: local variable may be non-initialized (type: float 32)
Overflow
166
Warning: operation [*] on float may overflow (on MIN or MAX bounds of FLOAT32)
Overflow
166
Warning: operation [+] on float may overflow (on MIN or MAX bounds of FLOAT32)



Эта таблица говорит мне о следующем:

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

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


Доверие к инструментам


Когда вы имеете дело с отраслевыми стандартами, типа КТ-178С или ГОСТ Р ИСО 26262, то вы постоянно сталкиваетесь с такими штуками как доверие к инструменту или квалификация инструмента. Что же это такое? Это такой процесс, в ходе которого вы показываете, что результатам работы инструментов разработки или тестирования, которые были использованы в проекте можно доверять и их ошибки задокументированы. Этот процесс тема отдельной статьи, так как не все очевидно. Главное здесь следующее: инструменты, применяющиеся в индустрии всегда идут вместе с набором документов и тестов которые помогают в этом процессе.

Итоги


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

К слову, если вам интересно, можно сделать отдельную статью про сертификацию инструментов. Напишите в комментариях, нужна ли такая статья.
Подробнее..

Не нарушайте мою приватность

29.12.2020 02:22:15 | Автор: admin

Добрый день! Статья будет про один совершенно незначительный момент в кодировании.
Например, многие мои коллеги сказали - Doch, das ist ganz normal! "А, чё, все норм, нашел к чему придраться!" Но, тем не менее.

Есть несколько вещей в написании кода которые меня бесят. Ну, на самом деле не несколько. :), чем старше становлюсь, тем больше и больше таких вещей, характер не лучшает. Но в топе - когда девелоперы пишут тесты к private методам. А, нет! Когда они ради этого убирают слово private у метода и метод становится package-private.

Вот, ты такой ревьювишь класс, строк этак на 3К, написанный в лучших традициях процедурного программирования, а там 10 методов public, 10 private, а еще 15 package-private. И про интерфейсы мы не слышали - это ж сервис, давай так прям так и инжектить! Ок, думаешь ты, сейчас рукава засучим, вынесем все public методы в интерфейс, helper-ы, их в отдельный package - короче распилим это безумство на части согласно SRP. Но тут же обнаруживаешь, что package-private методы давно дергаются из других мест. Дык у нас все классы в один package свалены! If something can be misused, it will be misused for sure, как говорится.

Да-да, все знают, писать тесты к private методам не надо, если ты это делаешь, то что-то не так с дизайном, и так далее. Но, блин, люди все равно это делают, и надо сказать, не без причин. Представте, что упомянутый выше класс вообще был с zero test coverage. А рефакторить надо. Вот так и появляются package-private методы. Но как же нам быть - вон и Whitebox из Mockito выпилили, конечно в PowerMock он остался, но ради этого тащить PowerMock в проект, да и синтаксис не лучше прямого вызова через reflection. Можно конечно свой велосипед написать, но... И так и так не айс получается.

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

Есть класс:

public class ObjectWithPrivates {    private final AtomicInteger count = new AtomicInteger(0);    private String name;    private String methodToTest(String in) {        return name + in + count.incrementAndGet();    }}

Тест:

interface TestPrivates {    void setName(String name);    String methodToTest(String in);    AtomicInteger getCount();}@Testvoid testPrivates() {    ObjectWithPrivates obj = new ObjectWithPrivates();    TestPrivates objWithPrivates = API.lookupPrivatesIn(obj)                                      .usingInterface(TestPrivates.class);    objWithPrivates.setName("Andromeda");    String in = objWithPrivates.methodToTest("in");    AtomicInteger count = objWithPrivates.getCount();    Assertions.assertEquals("Andromedain1", in);    Assertions.assertEquals(1, count.get());}

Как видите, с помощью интерфейса мы можем дергать private методы, и получать доступ к private полям. Ну и по коду сразу можно сказать, что происходит. Имейте ввиду, что все манипуляции происходят с obj и можно, например, засетить все private поля у obj и потом дернуть public метод.

Можно получить доступ к private полям/методам родительского класса:

TestPrivates accessToPrivates = API.lookupPrivatesIn(obj)                                   .lookupInSuperclass()                                   .usingInterface(TestPrivates.class);

А можно и статические методы дергать:

TestPrivates accessToPrivates = lookupPrivatesIn(SomePOJOClass.class)                                .usingInterface(TestPrivates.class);accessToPrivate.someStaticPrivateMethod();

А можно и объект создать через private конструктор.

Под капотом там стандартный reflection и Dynamic Proxy, никаких дополнительных зависимостей.

Вот ссылка на проект testprivates. Использовать только для тестов.

Всех с наступающим, не болейте!

Подробнее..
Категории: Чистый код , Java , Unit testing

Категории

Последние комментарии

  • Имя: Макс
    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