В современном программировании код достаточно сложного проекта обычно пишется в команде. Полезным упражнением, которое может повысить качество кода и уровень разработчика, является анализ чужого кода. При этом другая пара глаз может найти ошибки в организации проекта, ошибки как в реализации алгоритмов на нижнем уровне, так и в проектировании всей системы на уровне взаимодействия между классами, т.е. на высоком уровне.
В компаниях промышленной разработки ПО такой просмотр кода является обычной практикой при приеме кандидата на работу и оценке его навыков, а также для контроля работы разработчиков на проектах лидерами команд, и как средство борьбы с ошибками.
При начале знакомства студентов с архитектурным проектированием собственный анализ чужого кода и получения анализа (ревью) кода от других студентов группы на какой-то предыдущий проект (курсовую работу) позволит будущему разработчику увидеть свои и чужие ошибки и не писать таким образом код далее.
Если использовать систему контроля версий и общий репозиторий, то можно увидеть основные ошибки всех других студентов и их анализ.
Процесс анализа кода тесно связан с рефакторингом и является его движущей частью. Внутренний рефакторинг – это улучшения внутренней структуры кода классов без изменения их внешних интерфейсов.
2.1 Критерии, которые можно использовать при анализе кода
- Наличие организации проекта в виде классов.
- Наличие значимых комментариев к интерфейсу класса.
- Наличие комментариев к реализации методов.
- Не должно быть закомментированных участков кода в проекте, который передается на просмотр другим.
- Следование кода какому-то определенному соглашению о коде (code convention). Важно, чтобы код был написан единообразно, чтобы классы, методы и переменные назывались согласно единому стилю (например, чтобы не встречалось переменных variable, Variable, variableName, variable_name, VariableName, var, v одновременно в одном проекте). При этом, важно не следование какому-то конкретному стилю, а то, что стиль кода во всем проекте у разработчика одинаков.
- Код должен быть удобочитаемым.
- Наименования классов, полей, методов, параметров должны быть «говорящими», т.е. их имена должны понятно говорить, что за что отвечает. Код должен описывать себя.
- Недопустимо использовать краткие и не значащие ничего названия (a, z1, class1). Краткие названия переменных допустимо использовать только для индексов циклов.
- Не должно быть дублирования кода.
- Не должно быть дублирования кода с небольшими изменениями. Ведь можно использовать функции, методы, наследование, параметризацию и т.д.
- Классы не должны быть слишком сильно связаны.
- Не должны использоваться магические константы. Даже разработчик через месяц не сможет вспомнить, что значит 44, 536 и т.д. в коде. Константы следует объявлять и называть.
- Не должны использоваться глобальные переменные (может быть, исключая использования классов-одиночек).
- Методы не должны быть сильно большого размера. Если код метода слишком большой, его целесообразно поделить на несколько более коротких приватных методов.
- Поля класса и внутренние методы должны иметь правильные модификаторы доступа.
- Классы не должны быть слишком раздуты.
- Реализация методов должна быть эффективной и использовать стандартные структуры данных. Например, для коллекции объектов, в которую часто вставляются и из которой часто удаляются данные, не следует использовать массивы, эффективнее при этом использовать список, очередь и т.д.
- По возможности, должны использоваться разработанные и принятые сообществом сторонние компоненты. Не следует «изобретать» велосипед во многих случаях.
- Должна быть документация к коду (диаграммы классов, спецификация, диаграммы, поясняющие работу системы и алгоритмов, текстовая документация).
- Код С++ не должен использовать макросы С и по возможности, не использовать стандартную библиотеку С.
- Не рекомендуется использовать RTTI (динамическое определение типа).
- Не рекомендуется использовать множественное наследование.
- Графический интерфейс должен быть отделен от методов, реализующих логику системы.
- Не должны использоваться сгенерированные методы и названия объектов (вроде button1, button1_onclick()).
- Операции ввода-вывода должны быть отделены от логики.
- Субъективная оценка кода (нравится-не нравится, 5 звезд, 3 звезды и т.д.).
2.2 Пример анализа студенческого кода другими студентами
- Используются неинформативные имена переменных;
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
- Для работы необходимо разделится на команды, и послать их список преподавателю.
- Команда должна выбрать код прежнего готового проекта (например, курсовой работы по ООП) и нарисовать диаграмму классов для этого кода.
- Далее необходимо закачать код проекта команды и диаграмму классов на общий репозиторий (адрес узнать у преподавателя). Как это делается? Пример для системы контроля версий 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.
- Дождаться кода и диаграмм от других команд.
- Результаты анализа кода записать в файл со своими фамилиями (doc, pdf) в папке чужого кода и добавить его в репозиторий согласно описанному алгоритму.