Статья
Версия для печати
Обсудить на форуме
Модульное тестирование ПО встроенных систем в среде Unity
Часть 2

(C) Dale, 13.07.2011 — 15.07.2011.

Начало: Часть 1.

В прошлой статье я попытался дать небольшой обзор модульного тестирования с целью подготовить читателя, для которого этот материал в новинку, к восприятию основного материала. Теперь, когда этот минимум выполнен, пришла пора приступить собственно к тестированию.
При написании подобных статей перед автором всегда стоит трудный выбор: какие примеры выбрать для иллюстрирования изложенного материала? Если выбрать чересчур примитивный пример, он будет слишком далек от реальности (никто еще не стал экспертом в программировании, проштудировав программу «Hello World»); если же взять пример из реальной жизни, он наверняка будет изобиловать лишними деталями, которые отвлекают от сути.
Как компромисс в качестве примера я позаимствую небольшой модуль для вычисления площади треугольника из своей предыдущей статьи, посвященной обработке исключений на C. Он несложен, и в то же время, помимо общих ошибок времени выполнения программы, в нем могут возникнуть специфические именно для данного приложения ошибки, наличие которых мы и попытаемся выловить при помощи модульных тестов.
Итак, приступаем.

Подготовка

Запасаемся инструментами

Как упоминалось ранее, теоретически автоматизированное тестирование можно было бы проводить и без инструментов, писать весь необходимый код вручную; однако использование готовых инструментов позволит нам сэкономить массу сил и времени. Поэтому мы отправимся на домашнюю страницу страницу Unity  и скачаем оттуда свежую версию. Продукт этот бесплатен.
В состав Unity, помимо библиотек исходных на C, входит набор скриптов для автоматизации некоторых действий. Эти скрипты написаны на языке Ruby. Язык этот интерпретируемый, и, следовательно, для их выполнения необходим интерпретатор Ruby. Скачать его можно здесь. Этот продукт также относится к бесплатным. После скачивания его следует инсталлировать в своей системе.
Если вы еще не знакомы с Ruby – не беда, для работы с готовыми скриптами это не потребуется. Для выполнения скрипта file.rb достаточно лишь набрать в командной строке

> ruby file.rb

(подразумевается, что путь к самому интерпретатору Ruby прописан в пути поиска исполняемых файлов). Конечно же, знание Ruby не будет лишним, если вы решите написать собственные скрипты автоматизации или захотите воспользоваться функционалом Rake, который превосходит привычный (и архаичный) make.
Если вы еще не обзавелись продуктом для обработки исключений на C под названием CException, самое время сделать это сейчас.
Кроме того, нам потребуется компилятор C, совместимый со спецификациями ANSI. В принципе годится практически любой. Я буду использовать GCC, но, если вы предпочитаете другой, вряд ли это вызовет какие-либо затруднения.

Формулируем задачу

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

Код: (C) errorcode.h
// errorcode.h
enum ERRORCODE_T
{
  // длина стороны треугольника отрицательна или равна нулю
  INVALID_SIDE,
  // невозможно построить треугольник с такими сторонами
  BAD_TRIANGLE
};

Создаем рабочую область проекта

Создадим директорию для нашего проекта. Я назвал ее TestedTriangle.
Для пущей правдоподобности создадим в ней поддиректорию для нашего модуля. Хотя наш учебный проект совсем крошечный и состоит из единственного модуля, будем считать, что вскоре появятся и другие, использующие его сервис. Назовем эту поддиректорию TriangleArea. В ней, соответственно, заведем отдельные поддиректории для исходного кода (src) и для тестов (test).
Разумеется, как профессионалы мы подход к работе основательно, поэтому мы всегда используем в работе систему управления версиями файлов (Version Control System, VCS). Я использую для этой цели SVN, но вообще выбор конкретного продукта не принципиален, главное, что используется какая-либо VCS.
Если проект находится под управлением VCS, то любой участник проекта, имеющий доступ к репозиторию кода, может загрузить на свой компьютер все рабочие файлы проекта и произвести его полную сборку. Если в проекте используется код сторонних разработчиков (как в нашем случае Unity), хорошим тоном считается включать его в специальную поддиректорию проекта со стандартным названием vendor. Во-первых, это позволит вашим менее продвинутым коллегам, которые еще не установили у себя Unity, загрузить к себе наш проект, откомпилировать его и собрать без ошибок, поскольку все необходимое имеется в репозитории, и им не придется впопыхах разбираться, чего же не хватает на их компьютере, и устанавливать недостающие компоненты. Во-вторых, жизнь – непредсказуемая штука, и порой приходится возвращаться к проектам, о которых мы порой едва можем вспомнить (лично у меня были реальные случаи, когда проект совершенно неожиданно вдруг воскресал спустя 10 лет забвения, и большой проблемой было даже найти и восстановить среду программирования, в которой он был сделан). Не исключено, что через некоторое время ваши инструменты либо будет вовсе невозможно найти, либо их текущие версии окажутся несовместимыми с прошлыми, и проект придется перерабатывать. Поэтому идея хранить все вместе представляется мне весьма здравой и практичной, благо терабайтные объемы современных жестких дисков позволяют такую роскошь. В особо ответственных случаях вместе с проектом хранят даже инструментарий, с помощью которого он был реализован: компиляторы, IDE, вспомогательные утилиты; впрочем, мы прислушаемся к нашему чувству меры и воздержимся от столь решительных действий.
Итак, создаем в нашем проекте поддиректорию vendor и копируем в нее Unity, которую мы столь благоразумно припасли заранее. Туда же добавляем CException.
«Нулевой цикл» проекта подготовлен. Наша рабочая директория выглядит так:
Рис. 1. Структура рабочей директории.

Генерируем модуль

Пришло время наполнить нашу заготовку проекта содержимым. Прежде всего скопируем в директорию src файл errorcode.h, полученный нами от заказчика вместе со спецификациями модуля.
Сам модуль можно было бы создать вручную, но разработчики Unity любезно предоставили нам скрипт по имени generate_module.rb, который выполнит часть работы за нас. Находится этот скрипт в поддиректории auto корневой директории Unity. Если вызвать его с опцией –h, мы увидим подсказку по его использованию:

> ruby generate_module.rb –h

GENERATE MODULE
-------- ------

Usage: ruby generate_module [options] module_name
  -i"include" sets the path to output headers to 'include' (DEFAULT ../src)
  -s"../src"  sets the path to output source to '../src'   (DEFAULT ../src)
  -t"C:/test" sets the path to output source to 'C:/test'  (DEFAULT ../test)
  -p"MCH"     sets the output pattern to MCH.
              dh  - driver hardware.
              dih - driver interrupt hardware.
              mch - model conductor hardware.
              mvp - model view presenter.
              src - just a single source module. (DEFAULT)
  -d          destroy module instead of creating it.
  -u          update subversion too (requires subversion command line)
  -y"my.yml"  selects a different yaml config file for module generation

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

> ruby generate_module.rb -i"../../../TriangleArea/src" -s"../../../TriangleArea/src" -t"../../../TriangleArea/test" TriangleArea
File ../../../TriangleArea/src/TriangleArea.c created
File ../../../TriangleArea/src/TriangleArea.h created
File ../../../TriangleArea/test/TestTriangleArea.c created
Generate Complete

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

Код: (C) TriangleArea.h
// TriangleArea.h

#ifndef _TRIANGLEAREA_H
#define _TRIANGLEAREA_H

#endif // _TRIANGLEAREA_H

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

Код: (C) TriangleArea.c
// TriangleArea.c

#include "TriangleArea.h"

Тоже пока в общем-то ничего особенно впечатляющего. Хотя, конечно, минута там, минута сям – и в целом на дурной работе какое-то время будет сэкономлено; но революции в информатике такими средствами, конечно, не сделать.
Перейдем в test:

Код: (C) TestTriangleArea.c
// TestTriangleArea.c

#include "unity.h"
#include "TriangleArea.h"

void setUp(void)
{
}

void tearDown(void)
{
}

void test_TriangleArea_NeedToImplement(void)
{
    TEST_IGNORE();
}

Генератор зачем-то включил в наш тест две пустые функции без параметров с именами setUp() и tearDown(). Обратившись к документации (она находится в поддиректории docs корневой директории Unity), мы выясним, что эти функции имеют специальное назначение. Инфраструктура Unity будет вызывать setUp() перед выполнением каждого теста; соответственно, после выполнения каждого теста будет вызвана tearDown(). Самое время припомнить четырехфазную структуру теста из первой части статьи, и все становится понятным: задача первой функции – помочь подготовить среду для выполнения теста, задача второй – прибраться после его завершения.
Еще одна функция, test_TriangleArea_NeedToImplement(), включена в качестве образца для будущих тестов. Ее название намекает, что неплохо было бы эту функцию реализовать. Тело функции состоит из единственного макроса TEST_IGNORE(), который, как и следовало ожидать, приводит к игнорированию данного теста инфраструктурой (то есть он попросту не будет вызван).
В принципе тестовые функции можно называть как угодно. Однако, если имя функции будет начинаться с test, это даст нам важное преимущество: такие имена распознаются специальным инструментом, о котором речь пойдет позже. Использование этого инструмента опять же позволит сэкономить драгоценные минуты: он способен автоматически сгенерировать некоторый код, который в принципе можно было бы написать и вручную. Кроме того, такое соглашение об именовании функций позволит нам легко визуально отличать тесты от вспомогательных функций-хелперов, которые также могут (и, как мы увидим впоследствии, скорее всего, будут) присутствовать в тестах.

Готовим код для тестирования

Нам осталось подготовить код, который мы намерены протестировать. Заполним «пустышки», сгенерированные в поддиректории для исходных текстов проекта (src):

Код: (C) TriangleArea.h
// TriangleArea.h

#ifndef _TRIANGLEAREA_H
#define _TRIANGLEAREA_H

double triangleArea(double a, double b, double c);

#endif // _TRIANGLEAREA_H

Код: (C) TriangleArea.c
// TriangleArea.c

#include <math.h>
#include <stdlib.h>
#include "CException.h"
#include "ErrorCode.h"
#include "TriangleArea.h"

double triangleArea(double a, double b, double c)
{
  if (   (a <= 0.0)
      || (b <= 0.0)
      || (c <= 0.0)
      )
      Throw(NEGATIVE_SIDE);
       
  if (   (a <= fabs(b - c))
      || (a >= (b + c))
      )
      Throw(BAD_TRIANGLE);
         
  double p = (a + b + c) / 2.0;
  double result = sqrt(p * (p - a) * (p - b) * (p - c));
  return result;
}

На этом наш этап подготовки заканчивается, и мы приступаем к нашей главной задаче – тестированию модуля TriangleArea.

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

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

Код: (C) TestTriangleArea.c
// TestTriangleArea.c

#include "unity.h"


void test_FailExpected(void)
{
    TEST_FAIL();
}

Макрос TEST_FAIL() завершает тест по ошибке. Проверим, что он работает. Но сначала нам придется сгенерировать еще один модуль, который необходим для запуска тестов.
Как известно, в любой программе на языке C должна быть функция main(), с которой и начинается выполнение (особо дотошные читатели могут заметить, что в принципе есть возможность использовать и другую функцию в этом качестве, но мы их попросим не умничать и не мешать). Тестовый набор – это тоже программа, которая поочередно запускает все тесты из данного набора. Для ее генерации предназначен скрипт generate_test_runner.rb. Он гораздо проще своего предшественника в использовании и имеет лишь единственный обязательный параметр – спецификацию файла, содержащего тесты. Можно указать также второй параметр, который укажет спецификацию сгенерированного файла, если по каким-то причинам значение по умолчанию вас не устраивает. Я этого делать сейчас не буду:

> ruby generate_test_runner.rb ../../../TriangleArea/test/TestTriangleArea.c

Скрипт выполнился молча, однако в поддиректории test появился новый файл – TestTriangleArea_Runner.c. Посмотрим, как он устроен внутри:

Код: (C) TestTriangleArea_Runner.c
/* AUTOGENERATED FILE. DO NOT EDIT. */

//=======Test Runner Used To Run Each Test Below=====
#define RUN_TEST(TestFunc, TestLineNum) \
{ \
  Unity.CurrentTestName = #TestFunc; \
  Unity.CurrentTestLineNumber = TestLineNum; \
  Unity.NumberOfTests++; \
  if (TEST_PROTECT()) \
  { \
      setUp(); \
      TestFunc(); \
  } \
  if (TEST_PROTECT() && !TEST_IS_IGNORED) \
  { \
    tearDown(); \
  } \
  UnityConcludeTest(); \
}


//=======Automagically Detected Files To Include=====
#include "unity.h"
#include <setjmp.h>
#include <stdio.h>

//=======External Functions This Runner Calls=====
extern void setUp(void);
extern void tearDown(void);
extern void test_FailExpected(void);


//=======Test Reset Option=====
void resetTest()
{
  tearDown();
  setUp();
}


//=======MAIN=====
int main(void)
{
  Unity.TestFile = "../../../TriangleArea/test/TestTriangleArea.c";
  UnityBegin();
  RUN_TEST(test_FailExpected, 14);

  return (UnityEnd());
}

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

> TriangleTest.exe
../../../TriangleArea/test/TestTriangleArea.c:16:test_FailExpected:FAIL
-----------------------
1 Tests 1 Failures 0 Ignored
FAIL

Пока все идет по плану. Как и ожидалось, наш  первый тест завершился с ошибкой. Unity любезно сообщила нам, что тест test_FailExpected потерпел крах в 16-й строке исходного файла ../../../TriangleArea/test/TestTriangleArea.c.
Ну все, на этом заканчиваем баловство и переходим непосредственно к делу.

Первый тест (для нормального режима работы)

Прежде всего удостоверимся, что площадь треугольника считается правильно в случае корректных данных. Возьмем треугольник со сторонами 3, 4, 5. Как известно, он прямоугольный (сомневающиеся могут легко проверить этот факт по теореме Пифагора). Следовательно, его площадь легко посчитать в уме без всяких формул, она составляет 6 квадратных единиц. Проверим, работает ли наш модуль для данного набора входных значений.
Выбросим за ненадобностью test_FailExpected, который был нам нужен лишь для разминки с генератором тестовой программы, и заменим его настоящим тестом:

Код: (C) TestTriangleArea.c
// TestTriangleArea.c



void test_TriangleArea_3_4_5_Equals_6(void)
{
     // фаза 1 - подготовка
     double const expected = 6.0;
     // фаза 2 - выполнение
     double actual = triangleArea(3.0, 4.0, 5.0);
     // фаза 3 - оценка
     TEST_ASSERT_EQUAL_FLOAT(expected, actual);
     // фаза 4 - вырожденная
}

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

> TriangleTest.exe
../../../TriangleArea/test/TestTriangleArea.c:14:test_TriangleArea_3_4_5_Equals_6:PASS
-----------------------
1 Tests 0 Failures 0 Ignored

OK

Наш первый реальный тест прошел успешно, результат совпал с ожидаемым.

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

Тестируем обработку ошибок

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

Отрицательные длины сторон

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

Код: (C) TestTriangleArea.c
// TestTriangleArea.c

#include "unity.h"
#include "TriangleArea.h"
#include "CException.h"
#include "ErrorCode.h"



void test_Side_1_isLessThan_0_Exception(void)
{
     // фаза 1 - подготовка
     CEXCEPTION_T e;
     // фаза 2 - выполнение
     Try
     {
             double actual = triangleArea(-3.0, 4.0, 5.0);
             // фаза 3 - оценка
             // если мы попали в эту точку, то обработка ошибок не работает
             TEST_FAIL_MESSAGE("No exception was thrown");
     }
     Catch(e)
     {
             // проверяем, что исключение "правильное"
             TEST_ASSERT_EQUAL_UINT((unsigned int)NEGATIVE_SIDE, (unsigned int)e);
     }
}

Выполняем:

../../../TriangleArea/test/TestTriangleArea.c:16:test_TriangleArea_3_4_5_Equals_6:PASS
../../../TriangleArea/test/TestTriangleArea.c:27:test_Side_1_isLessThan_0_Exception:PASS
-----------------------
2 Tests 0 Failures 0 Ignored
OK

Кто проверит проверяющего?

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

Код: (C) TriangleArea.c
// TriangleArea.c

#include <math.h>
#include <stdlib.h>
#include "CException.h"
#include "ErrorCode.h"
#include "TriangleArea.h"

double (double a, double b, double c)
{ /*
  if (   (a <= 0.0)
      || (b <= 0.0)
      || (c <= 0.0)
      )
      Throw(NEGATIVE_SIDE);
       
  if (   (a <= fabs(b - c))
      || (a >= (b + c))
      )
      Throw(BAD_TRIANGLE);
         
  double p = (a + b + c) / 2.0;
  double result = sqrt(p * (p - a) * (p - b) * (p - c));
  return result; */

}

Запускаем:

../../../TriangleArea/test/TestTriangleArea.c:49:test_Side_1_isLessThan_0_Exception:FAIL: No exception was thrown
-----------------------
1 Tests 1 Failures 0 Ignored
FAIL

Тест провалился. Unity бдит. Посмотрим, что будет, если модуль все-таки выбросит исключение, но не то, которое должен по контракту:

Код: (C) TriangleArea.c
// TriangleArea.c

#include <math.h>
#include <stdlib.h>
#include "CException.h"
#include "ErrorCode.h"
#include "TriangleArea.h"

double triangleArea(double a, double b, double c)
{
  if (   (a <= 0.0)
      || (b <= 0.0)
      || (c <= 0.0)
      )
      Throw(BAD_TRIANGLE);
      //Throw(NEGATIVE_SIDE);
       
  if (   (a <= fabs(b - c))
      || (a >= (b + c))
      )
      Throw(BAD_TRIANGLE);
         
  double p = (a + b + c) / 2.0;
  double result = sqrt(p * (p - a) * (p - b) * (p - c));
  return result;
}

Результат выполнения:

../../../TriangleArea/test/TestTriangleArea.c:42:test_Side_1_isLessThan_0_Exception:FAIL: Expected 0 Was 1
-----------------------
1 Tests 1 Failures 0 Ignored
FAIL

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

Примечание. Выполнение "нормального" теста с "испорченной" функцией не проводилось ввиду бессмысленности.

Второй тест

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

Код: (C) TriangleArea.c
// TestTriangleArea.c

#include "unity.h"
#include "TriangleArea.h"
#include "CException.h"
#include "ErrorCode.h"



void test_Side_2_isLessThan_0_Exception(void)
{
     // фаза 1 - подготовка
     CEXCEPTION_T e;
     // фаза 2 - выполнение
     Try
     {
             double actual = triangleArea(3.0, -4.0, 5.0);
             // фаза 3 - оценка
             // если мы попали в эту точку, то обработка ошибок не работает
             TEST_FAIL_MESSAGE("No exception was thrown");
     }
     Catch(e)
     {
             // проверяем, что исключение "правильное"
             TEST_ASSERT_EQUAL_UINT((unsigned int)NEGATIVE_SIDE, (unsigned int)e);
     }
}

Мы, как обычно, на высоте:

../../../TriangleArea/test/TestTriangleArea.c:16:test_TriangleArea_3_4_5_Equals_6:PASS
../../../TriangleArea/test/TestTriangleArea.c:27:test_Side_1_isLessThan_0_Exception:PASS
../../../TriangleArea/test/TestTriangleArea.c:46:test_Side_2_isLessThan_0_Exception:PASS
-----------------------
3 Tests 0 Failures 0 Ignored
OK

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

Примечание. Те, кто профессионально занимается программированием, знакомы с понятием «запаха кода» по работам Мартина Фаулера и его последователей. Для тех, кому этот термин в новинку, краткое пояснение: «запах кода» – это весьма неприятное явление (хороших «запахов» у кода не бывает, это всегда признак низкого качества). Все, кто заинтересован в создании высококачественного кода, обязательно должны изучить хотя бы основополагающую книгу Фаулера «Рефакторинг. Улучшение существующего кода».

(Окончание).
Версия для печати
Обсудить на форуме