2. Анализ кода (code review)

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

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

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

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

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

2.1 Критерии, которые можно использовать при анализе кода

  1. Наличие организации проекта в виде классов.
  2. Наличие значимых комментариев к интерфейсу класса.
  3. Наличие комментариев к реализации методов.
  4. Не должно быть закомментированных участков кода в проекте, который передается на просмотр другим.
  5. Следование кода какому-то определенному соглашению о коде (code convention). Важно, чтобы код был написан единообразно, чтобы классы, методы и переменные назывались согласно единому стилю (например, чтобы не встречалось переменных variable, Variable, variableName, variable_name, VariableName, var, v одновременно в одном проекте). При этом, важно не следование какому-то конкретному стилю, а то, что стиль кода во всем проекте у разработчика одинаков.
  6. Код должен быть удобочитаемым.
  7. Наименования классов, полей, методов, параметров должны быть «говорящими», т.е. их имена должны понятно говорить, что за что отвечает. Код должен описывать себя.
  8. Недопустимо использовать краткие и не значащие ничего названия (a, z1, class1). Краткие названия переменных допустимо использовать только для индексов циклов.
  9. Не должно быть дублирования кода.
  10. Не должно быть дублирования кода с небольшими изменениями. Ведь можно использовать функции, методы, наследование, параметризацию и т.д.
  11. Классы не должны быть слишком сильно связаны.
  12. Не должны использоваться магические константы. Даже разработчик через месяц не сможет вспомнить, что значит 44, 536 и т.д. в коде. Константы следует объявлять и называть.
  13. Не должны использоваться глобальные переменные (может быть, исключая использования классов-одиночек).
  14. Методы не должны быть сильно большого размера. Если код метода слишком большой, его целесообразно поделить на несколько более коротких приватных методов.
  15. Поля класса и внутренние методы должны иметь правильные модификаторы доступа.
  16. Классы не должны быть слишком раздуты.
  17. Реализация методов должна быть эффективной и использовать стандартные структуры данных. Например, для коллекции объектов, в которую часто вставляются и из которой часто удаляются данные, не следует использовать массивы, эффективнее при этом использовать список, очередь и т.д.
  18. По возможности, должны использоваться разработанные и принятые сообществом сторонние компоненты. Не следует «изобретать» велосипед во многих случаях.
  19. Должна быть документация к коду (диаграммы классов, спецификация, диаграммы, поясняющие работу системы и алгоритмов, текстовая документация).
  20. Код С++ не должен использовать макросы С и по возможности, не использовать стандартную библиотеку С.
  21. Не рекомендуется использовать RTTI (динамическое определение типа).
  22. Не рекомендуется использовать множественное наследование.
  23. Графический интерфейс должен быть отделен от методов, реализующих логику системы.
  24. Не должны использоваться сгенерированные методы и названия объектов (вроде button1, button1_onclick()).
  25. Операции ввода-вывода должны быть отделены от логики.
  26. Субъективная оценка кода (нравится-не нравится, 5 звезд, 3 звезды и т.д.).

2.2 Пример анализа студенческого кода другими студентами

  1. Используются неинформативные имена переменных;
class Bus : public Truck
{
 int xStop;
 int yStop;
 int TimeStop;
 bool isStop;
Car::Car(int s,int l,int dx,int dy,int ml):Vehicle(s,l,dx,dy)
{
 setMaxLength(ml);
}

2. Применяются непонятные константы;

void SUV::setSpeed(int s)
{
 (isRoad) ? Vehicle::setSpeed(s) : Vehicle::setSpeed(5);
}
void SUV::setLength(int l)
{
 (l > 0 ) ? Vehicle::setLength(l) : Vehicle::setLength(45);
}

3. Присутствуют неинформативные имена control’ов на форме;

private: System::Void button2_Click(System::Object^ sender,
System::EventArgs^ e) {
 this->Close();
 }
private: System::Void button1_Click(System::Object^ sender,
System::EventArgs^ e) {
 button1->Enabled = false;
 TimerNewVehicle->Enabled = false;
 TimerVehicleGo->Enabled = false;
 StartRoad->Enabled = true;
 TimerSort->Enabled = false;
 Show();
 }

4. Создан особый файл GlobalVars.h. По сути, его содержимое можно спокойно распределить между стандартным stdafx.h и полями формы Form1.h;

#pragma once
#include <vector>
using namespace std;
vector<Vehicle*> road;
vector<SUV*> outRoad;
int LenRoad;
int MaxSpeed;

5. Неоптимальная структура операторов if/else с нагромождением скобок и сложными условиями. К примеру, ниже оба вложенных условия совпадают во всём, кроме знака. Их можно вынести в две переменные типа int или double и проверять уже их. Также явно просится else if без лишних скобок между;

if(radioButton4->Checked)
{
 if((data[j]->getX() + data[j]->getLength())/2 > (data[j+1]-
>getX() + data[j]->getLength())/2 )
 {
 Vehicle* tmp = data[j];
 data[j] = data[j+1];
 data[j+1] = tmp;
 }
}
else
{
 if((data[j]->getX() + (data[j]->getLength()/2)) < (data[j+1]-
>getX() + (data[j]->getLength())/2) )
 {
 Vehicle* tmp = data[j];
 data[j] = data[j+1];
 data[j+1] = tmp;
 }
}

6. Странный способ использования оператора switch. Кавычки можно убрать без ущерба работоспособности, тем самым улучшив читаемость;

switch(type)
{
case 0:
 {
 Car* car = new Car(s,l,x,y,30);
 road.push_back(car);
 }break;
case 1:
 {
 Truck* truck = new Truck(s,l,x,y,70);
 road.push_back(truck);
 }break;
case 2:
 {
 if ( l < 65 ) l = 65;
 Bus* bus = new Bus(s,l,x,y,70,400,y);
 road.push_back(bus);
 }break;
case 3:
 {
 Taxi* taxi = new Taxi(s,l,x,y,30,xs,y);
 road.push_back(taxi);
 }break;
case 4:
 {
 SUV* suv = new SUV(s,l,x,y);
 Truck* truck = dynamic_cast<Truck*>(suv);
 road.push_back(truck);
 }break;
}

7. Множественное наследование классов, впоследствии приводящее к трудно отслеживаемым проблемам;

#pragma once
#include "Truck.h"
#include "Car.h"
#include "Vehicle.h"
class SUV : virtual public Truck , public Car
{
 int isRoad;// 0 - на обочине, 1 - на дороге.

8. При каждом вызове метода прорисовки ему всегда передаётся ссылка на один и тот же объект. Логичней было бы осуществить передачу всего один раз (в конструкторе или setter’ом, предварительно объявив в классе ссылку на объект);

virtual void draw(Graphics^ g , bool cmd);

9. Отсутствуют пробелы после запятых;

bool isStop;
public:
 Taxi(void);
 Taxi(int s,int l,int x,int y,int ml,int xs,int ys);
 //
 //get

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

int xStop;
 int yStop;
 int TimeStop;
 bool isStop;
 bool isStop;
public:
 Taxi(void);
 Taxi(int s,int l,int x,int y,int ml,int xs,int ys);
 //
 //get
 //
 virtual int getSX(void);
 virtual int getSY(void);
 virtual int getTimeStop(void);

11. Переносы длинных строк отсутствуют, приходится использовать прокрутку;

12. Классы, по сути своей, представляют собой лишь набор getter’ов и setter’ов, логика в них практически не реализуется;

class Taxi : public Car
{
 int xStop;
 int yStop;
 int TimeStop;
 bool isStop;
public:
 Taxi(void);
 Taxi(int s,int l,int x,int y,int ml,int xs,int ys);
 //
 //get
 //
 virtual int getSX(void);
 virtual int getSY(void);
 virtual int getTimeStop(void);
 virtual bool getIsStop(void);
 //
 //set
 //
 virtual void setTimeStop(int ts);
 virtual void setIsStop(bool is);
 //
 //interface
 //
 virtual void draw(Graphics^ g,bool cmd);
 ~Taxi(void);
};

13. Вся логика фактически расположена внутри формы;

private: System::Void TimerNewVehicle_Tick(System::Object^ sender,
System::EventArgs^ e) {
 Random^ rand = gcnew Random();
 int x = -10;
 int y = 10;
 int s = rand->Next(40,MaxSpeed);
 int l = rand->Next(50,80);
 int type = rand->Next(0,5);
 int xs = rand->Next(250,800);

 switch(type)
 {
 <Здесь был код switch’а>
 }
 }

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

2.3 Задания к разделу 2

  1. Для работы необходимо разделится на команды, и послать их список преподавателю.
  2. Команда должна выбрать код прежнего готового проекта (например, курсовой работы по ООП) и нарисовать диаграмму классов для этого кода.
  3. Далее необходимо закачать код проекта команды и диаграмму классов на общий репозиторий (адрес узнать у преподавателя). Как это делается? Пример для системы контроля версий Git:
    • Скачать консольный Git.
    • Поставить, указать, чтобы он был в путях ОС. Это позволит вводить команды в командной строке far или cmd.
    • Склонировать себе репозиторий (преподаватель должен создать репозиторий на хостинге вроде github и пользователя, который может писать туда).
    • Создать какую-либо папку, зайти в нее и там в командной строке написать
      git clone http://<пользователь>@<репозиторий>
      (доступ по протоколу http, чтобы не иметь дело с ключами, по-хорошему надо по защищенному каналу (для этого нужно использовать ключи, которые генерируются в профиле пользователя)).
    • После того, как данные закачаются на локальный компьютер, в репозитории необходимо создать папку (просто создается там средствами ОС и потом добавляется в git):
      git add <папка>
      здесь <папка> — имя папки, необходимо задать 3 фамилии латиницей подряд.
      Если все ок, то необходимо скопировать туда код и диаграмму классов.
    • И добавить в git:
      git add <папка>/*
      (надо быть на уровень выше для этого примера).
    • После добавления всех файлов необходимо сделать локальный коммит:
      git commit -a -m "message"
      где message — сообщение, о том, что сделано в коммите, будет видно в логах.
    • Так как git система распределенная, надо послать все изменения на сервер. Пользователь должен быть с правами на запись, поэтому следует написать:
      git push
      и ввести пароль пользователя
    • В дальнейшем, изменения других можно подхватить к себе, набрав команду
      git pull
      в папке под системой git.
  4. Дождаться кода и диаграмм от других команд.
  5. Результаты анализа кода записать в файл со своими фамилиями (doc, pdf) в папке чужого кода и добавить его в репозиторий согласно описанному алгоритму.
Создайте подобный сайт на WordPress.com
Начало работы