Pull to refresh

Comments 25

Хорошие советы по написанию хорошего кода, по большому счёту. По-моему, testability и maintainability сильно коррелируют.
В чём проводилась оценка покрываемости кода? Я использую lconv, но мне кажется, что он не настолько информативный вывод даёт, как ваш
lcov тоже. А приведен вывод genhtml. Я добавлю команды построения отчета в статью
На самом деле половина советов сводится к тому, что «используйте dependency injection». Собственно, так оно и есть, потому что DI позволяет избавится от лишней связанности классов и четко контролировать создание объекто, в нужный момент заменяя все это моками. Благо, на плюсах теперь есть из чего выбирать в плане DI (я, например, использую Hypodermic C++, но есть и куча другого).
Тут еще один момент есть. Время кодинга хоть и возрастает, но, по моему ощущению, время разработки таки сокращается (это при TDD).
Т.е. необходимость отладки практически пропадает. Но это только при TDD, при просто UnitTesting эффект существенно слабее.
ИМХО, тут все просто. Если можно применить TDD — есть ясное понимание задачи. Ну, более ясное, чем в ситуации, когда TDD применить трудно. Вы удивлены, что при лучшем понимании задачи разработка идет быстрее?
Скорее наоборот — нет ясного понимания задачи, тогда TDD еще удобнее, т.к. дает возможность легче вносить изменения и архитектура выстраивается почти сама из имеющихся требований.
И как же Вы собираетесь писать тесты, если не знаете, что будете тестировать?
К сожалению, за надёжность разработки приходится расплачиваться тем, что код становится менее удобным для изучения, когда хочется посмотреть не что функция делает, а как:

1. Увеличивается количество сущностей: вместо прямого вызова new конкретного класса вызывается абстрактная фабрика, возвращающая абстрактный класс: +2 интерфейса, +1 класс.

2. Усложняется навигация по коду: перейти по определению класса становится невозможно.

3. Может упасть производительность из-за виртуальных вызовов в вычилистельных задачах.

Ну и общие соображения:

4. В C++ нет интерфейсов. Их можно пытаться эмулировать абстрактными классами и множественным наследованием, но получить тот же функционал, что в C# и Java (композиция интерфейсов), все равно не получится.
но получить тот же функционал, что в C# и Java (композиция интерфейсов), все равно не получится

Почему?

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

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

Оно вылезет, если один интерфейс наследуется от другого или нескольких.
Например, IList, который наследуется от ICollection и IEnumerable.

Пример C#:

interface IA {}
interface IB {}
interface IC {}
interface IAB: IA, IB {}

class CA: IA {}
class CB: CA, IAB, IC {}

Аналогичный код на C++ будет выглядеть так:

class IA { public: virtual ~IA() {} };
class IB { public: virtual ~IB() {} };
class IC { public: virtual ~IC() {} };
class IAB: virtual public IA, virtual public IB { public: virtual ~IAB() {} };

class CA: virtual public IA {};
class CB: public CA, virtual public IAB, virtual public IC {};

При этом на 64-битной аритектуре объект C# будет занимать 24 байта вне зависимости от числа интерфейсов, тогда как C++ — 40 байт, и каждый последующий интерфейс будет добавлять ещё по 8 байт.
Виртуальное наследование может быть опасно только если в базовых классах имеются какие-либо данные. Если интерфейсные классы не содержат данных, то никаких проблем возникнуть не должно, можно виртуальное наследование вообще не использовать.
Виртуальное наследование нужно использовать для возможности множественного включения одного и того же интерфейса (см. мой пример выше).
Можно и не использовать
struct I0 {
    virtual ~I0() = default;
    virtual void base() = 0;
};

struct I1 : I0 {
    virtual ~I1() = default;
    virtual void foo() = 0;
};

struct I2 : I0 {
    virtual ~I2() = default;
    virtual void bar() = 0;
};

struct I12 : I1, I2, I0 {
    ~I12() override { printf("~I12\n"); }

    void base() override { printf("base\n"); }

    void foo() override { printf("foo\n"); }

    void bar() override { printf("bar\n"); }
};

int main() {
    I0* i0 = new I12;
    i0->base();
    delete i0;

    printf("\n");

    I1* i1 = new I12;
    i1->base();
    i1->foo();
    delete i1;

    printf("\n");

    I2* i2 = new I12;
    i2->base();
    i2->bar();
    delete i2;

    printf("\n");
    printf("Size0: %llu\n", sizeof(I0));
    printf("Size1: %llu\n", sizeof(I1));
    printf("Size2: %llu\n", sizeof(I2));
    printf("Size12: %llu\n", sizeof(I12));

    return 0;
}

И получаем вполне закономерную ошибку:

error: 'I0' is an ambiguous base of 'I12'
Вышеприведённый вариант работает в VS2015. Для переносимости можно сделать так:
struct I12 : I1, I2 {
...
};

int main() {
    I0* i0 = static_cast<I1*>(new I12);
...
Да, при миграции проекта с VS2015 на g++ я с кучей проблем, связанных с нестрогим пониманием стандарта, связывался.

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

А вот на такое и VS2015 ругнётся
struct I0 {
	virtual ~I0() {};
	virtual void base() = 0;
};

struct I1 : I0 {
	virtual ~I1() {};
	virtual void foo() = 0;
};

struct I2 : I1, I0 {
	virtual ~I2() {};
	virtual void bar() = 0;
};

struct I12 : I1, I2, I0 {
	~I12() override { printf("~I12\n"); }

	void base() override { printf("base\n"); }

	void foo() override { printf("foo\n"); }

	void bar() override { printf("bar\n"); }
};


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

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

Я лишь хотел указать на то, что слабая связанность в виде использования интерфейсов замедляет навигацию по коду, иногда существенно.
Sign up to leave a comment.

Articles