Pull to refresh

Чистый код для TypeScript — Часть 3

Reading time 9 min
Views 9.4K
Original author: labs42io

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



Тестирование


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


Нет никакого оправдания, чтобы не писать тесты. Есть много хороших фреймворков для тестирования на JS с поддержкой типов для TypeScript, так что вы найдите тот который понравится вашей команде. Когда вы найдете тот, который работает для вашей команды, тогда стремитесь всегда писать тесты для каждой новой фичи/модуля, которую вы пишете. Если вы предпочитаете метод тест-ориентированной разработки (TDD), это замечательно, но главное — просто убедиться, что вы достигли своих целей покрытия, прежде чем запускать какую-либо функцию или реорганизовать существующую.


Три закона TDD


  1. Новый рабочий код пишется только после того, как будет написан модульный тест, который не проходит.
  2. Вы пишете ровно такой объем кода модульного теста, какой необходим для того, чтобы этот тест не проходил (если код теста не компилируется, считается, что он не проходит).
  3. Вы пишете ровно такой объем рабочего кода, какой необходим для прохождения модульного теста, который в данный момент не проходит.

Правила F.I.R.S.T.


Чистые тесты должны следовать правилам:


  • Быстрота(Fast) Тесты должны выполняться быстро. Все мы знаем, что разработчики люди, а люди ленивы, поскольку эти выражения являются “транзитивными”, то можно сделать вывод, что люди тоже ленивы. А ленивый человек не захочет запускать тесты при каждом изменении кода, если они будут долго выполняться.
  • Независимость(Independent) Тесты не должны зависеть друг от друга. Они должны обеспечивать одинаковые выходные данные независимо от того, выполняются ли они независимо или все вместе в любом порядке.
  • Повторяемость(Repeatable) Тесты должны выполняться в любой среде, и не должно быть никаких оправданий тому, почему они провалились.
  • Очевидность(Self-Validating) Тест должен отвечать либо Passed, либо Failed. Вам не нужно сравнивать файлы логов, для чтобы ответить, что тест пройден.
  • Своевременность(Timely) Юнит тесты должны быть написаны перед производственным кодом. Если вы пишете тесты после производственного кода, то вам может показаться, что писать тесты слишком сложно.

Один кейс на тест


Тесты также должны соответствовать Принципу единой ответственности(SPP). Делайте только одно утверждение за единицу теста.(ps. не пренебрегайте этим правилом)


import { assert } from 'chai';

describe('AwesomeDate', () => {
  it('handles date boundaries', () => {
    let date: AwesomeDate;

    date = new AwesomeDate('1/1/2015');
    assert.equal('1/31/2015', date.addDays(30));

    date = new AwesomeDate('2/1/2016');
    assert.equal('2/29/2016', date.addDays(28));

    date = new AwesomeDate('2/1/2015');
    assert.equal('3/1/2015', date.addDays(28));
  });
});

Хорошо:


import { assert } from 'chai';

describe('AwesomeDate', () => {
  it('handles 30-day months', () => {
    const date = new AwesomeDate('1/1/2015');
    assert.equal('1/31/2015', date.addDays(30));
  });

  it('handles leap year', () => {
    const date = new AwesomeDate('2/1/2016');
    assert.equal('2/29/2016', date.addDays(28));
  });

  it('handles non-leap year', () => {
    const date = new AwesomeDate('2/1/2015');
    assert.equal('3/1/2015', date.addDays(28));
  });
});

Асинхронность


Используйте promises а не callbacks


Callback-функции ухудшают читаемость и приводят к чрезмерному количеству вложенности (ад обратных вызовов(callback hell)). Существуют утилиты, которые преобразуют существующие функции, используя стиль callback-ов, в версию, которая возвращает промисы (для Node.js смотрите util.promisify, для общего назначения смотрите pify, es6-promisify)


Плохо:


import { get } from 'request';
import { writeFile } from 'fs';

function downloadPage(url: string, saveTo: string, callback: (error: Error, content?: string) => void) {
  get(url, (error, response) => {
    if (error) {
      callback(error);
    } else {
      writeFile(saveTo, response.body, (error) => {
        if (error) {
          callback(error);
        } else {
          callback(null, response.body);
        }
      });
    }
  });
}

downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => {
  if (error) {
    console.error(error);
  } else {
    console.log(content);
  }
});

Хорошо:


import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';

const write = promisify(writeFile);

function downloadPage(url: string, saveTo: string): Promise<string> {
  return get(url)
    .then(response => write(saveTo, response));
}

downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html')
  .then(content => console.log(content))
  .catch(error => console.error(error));  

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


Методы Описание
Promise.resolve(value) Преобразуйте значение в решенный промис.
Promise.reject(error) Преобразуйте ошибку в отклоненный промис.
Promise.all(promises) Возвращает новый промис, который выполняется с массивом значений выполнения для переданных промисов или отклоняется по причине первого промиса, который выполняется с ошибкой.
Promise.race(promises) Возвращает новый промис, который выполнен/отклонен с результатом/ошибкой первого выполненного промиса из массива переданных промисов.

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


Обработка ошибок


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


Всегда используйте ошибки для отклонений(reject)


JavaScript и TypeScript позволяют вам делать throw любым объектом. Промис также может быть отклонен с любым объектом причины. Рекомендуется использовать синтаксис throw с типом Error. Это потому что ваша ошибка может быть поймана в более высоком уровне кода с синтаксисом catch. Было бы очень странно поймать там строковое сообщение и сделать отладку более болезненной. По той же причине вы должны отклонять промисы с типами Error.


Плохо:


function calculateTotal(items: Item[]): number {
  throw 'Not implemented.';
}

function get(): Promise<Item[]> {
  return Promise.reject('Not implemented.');
}

Хорошо:


function calculateTotal(items: Item[]): number {
  throw new Error('Not implemented.');
}

function get(): Promise<Item[]> {
  return Promise.reject(new Error('Not implemented.'));
}

// or equivalent to:

async function get(): Promise<Item[]> {
  throw new Error('Not implemented.');
}

Преимущество использования типов Error заключается в том, что они поддерживается синтаксисом try/catch/finally и неявно всеми ошибками и имеют свойство stack, которое является очень мощным для отладки. Есть и другие альтернативы: не использовать синтаксис throw и вместо этого всегда возвращать пользовательские объекты ошибок. TypeScript делает это еще проще.
Рассмотрим следующий пример:


type Result<R> = { isError: false, value: R };
type Failure<E> = { isError: true, error: E };
type Failable<R, E> = Result<R> | Failure<E>;

function calculateTotal(items: Item[]): Failable<number, 'empty'> {
  if (items.length === 0) {
    return { isError: true, error: 'empty' };
  }

  // ...
  return { isError: false, value: 42 };
}

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


Не игнорируйте отловленные ошибки


Игнорирование пойманной ошибки не дает вам возможности исправить или каким-либо образом отреагировать на ее появление. Логирование ошибок в консоль (console.log) не намного лучше, так как зачастую оно может потеряться в море консольных записей. Оборачивание куска кода в try/catch означает, что вы предполагаете возможность появления ошибки и имеете на этот случай четкий план.


Плохо:


try {
  functionThatMightThrow();
} catch (error) {
  console.log(error);
}

// or even worse

try {
  functionThatMightThrow();
} catch (error) {
  // ignore error
}

Хорошо:


import { logger } from './logging'

try {
  functionThatMightThrow();
} catch (error) {
  logger.log(error);
}

Не игнорируйте ошибки, возникшие в промисах


Вы не должны игнорировать ошибки в промисах по той же причине, что и в try/catch.


Плохо:


getUser()
  .then((user: User) => {
    return sendEmail(user.email, 'Welcome!');
  })
  .catch((error) => {
    console.log(error);
  });

Хорошо:


import { logger } from './logging'

getUser()
  .then((user: User) => {
    return sendEmail(user.email, 'Welcome!');
  })
  .catch((error) => {
    logger.log(error);
  });

// or using the async/await syntax:

try {
  const user = await getUser();
  await sendEmail(user.email, 'Welcome!');
} catch (error) {
  logger.log(error);
}

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


Форматирование носит субъективный характер. Как и во многом собранном здесь, в вопросе форматирования нет жестких правил, которым вы обязаны следовать. Главное — НЕ СПОРИТЬ по поводу форматирования. Есть множество инструментов для автоматизации этого. Используйте один! Это трата времени и денег когда инженеры спорят о форматировании. Общее правило, которому стоит следовать соблюдайте правила форматирования принятые в команде


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



Обратитесь также к этому великому TypeScript StyleGuide and Coding Conventions источнику.


Используйте один вариант именования


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


Плохо:


const DAYS_IN_WEEK = 7;
const daysInMonth = 30;

const songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restore_database() {}

type animal = { /* ... */ }
type Container = { /* ... */ }

Хорошо:


const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;

const SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
const ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restoreDatabase() {}

type Animal = { /* ... */ }
type Container = { /* ... */ }

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


Организация импортов


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


  • Операторы импорта должны быть в алфавитном порядке и сгруппированы.
  • Неиспользованный импорт должен быть удален.
  • Именованные импорты должны быть в алфавитном порядке (т.е. import {A, B, C} from 'foo';)
  • Источники импорта должны быть в алфавитном порядке в группах, т.е.: import * as foo from 'a'; import * as bar from 'b';
  • Группы импорта обозначены пустыми строками.
  • Группы должны соблюдать следующий порядок:
    • Полифилы (т.е. import 'reflect-metadata';)
    • Модули сборки Node (т.е. import fs from 'fs';)
    • Внешние модули (т.е. import { query } from 'itiriri';)
    • Внутренние модули (т.е. import { UserService } from 'src/services/userService';)
    • Модули из родительского каталога (т.е. import foo from '../foo'; import qux from '../../foo/qux';)
    • Модули из того же или родственного каталога (т.е. import bar from './bar'; import baz from './bar/baz';)

Плохо:


import { TypeDefinition } from '../types/typeDefinition';
import { AttributeTypes } from '../model/attribute';
import { ApiCredentials, Adapters } from './common/api/authorization';
import fs from 'fs';
import { ConfigPlugin } from './plugins/config/configPlugin';
import { BindingScopeEnum, Container } from 'inversify';
import 'reflect-metadata';

Хорошо:


import 'reflect-metadata';

import fs from 'fs';
import { BindingScopeEnum, Container } from 'inversify';

import { AttributeTypes } from '../model/attribute';
import { TypeDefinition } from '../types/typeDefinition';

import { ApiCredentials, Adapters } from './common/api/authorization';
import { ConfigPlugin } from './plugins/config/configPlugin';

Используйте typescript алиасы


Создайте более симпатичный импорт, определив пути и свойства baseUrl в разделе compilerOptions в tsconfig.json
Это позволит избежать длинных относительных путей при импорте.


Плохо:


import { UserService } from '../../../services/UserService';

Хорошо:


import { UserService } from '@services/UserService';

// tsconfig.json
...
  "compilerOptions": {
    ...
    "baseUrl": "src",
    "paths": {
      "@services": ["services/*"]
    }
    ...
  }
...

P.S.


Первая часть
Вторая часть
Полный перевод

Tags:
Hubs:
+6
Comments 7
Comments Comments 7

Articles