Софтуерно Инженерство
Loading...
+ Нов въпрос
Wanker avatar Wanker 15 Точки

Валидация в конструктор?

Въпрос към Жоро или който може да даде съвет. :)

Имаме примерен клас:

class Person
{
private:
	std::string name;
	int age;
	
public:
	Person(std::string name, int age)
		: name(name), age(age)
	{
	}

	std::string GetName() const
	{
		return this->name;
	}

	void SetName(std::string name)
	{
		if (name.empty())
		{
			throw "Person name cannot be empty.";
		}

		this->name = name;
	}

	int GetAge() const
	{
		return this->age;
	}

	void SetAge(int age)
	{
		if (age <= 0)
		{
			throw "Invalid person age.";
		}

		this->age = age;
	}
}

Имаме дадена валидация в сетърите, но в конструктора чрез инициализиращия лист няма как да направим тази валидация.

От C# знаем, че се минава през пропъртитата в конструктора, дали това може да се приложи и тук? Например:

Person(std::string name, int age)
{
	this->SetName(name);
	this->SetAge(age);
}

Но така пък не се използва инициализиращия лист...

Въобще как е добрата практика в такъв случай?

Благодаря.

0
C++ Programming
georgi.stef.georgiev avatar georgi.stef.georgiev 904 Точки
Best Answer

Здравей,

Хубав въпрос, заслужава си малко по-голямо обяснение. Ще разделя отговора на две части:

1. АКО ползваме initializer list на конструктора (защото ни се налага заради членове, базов клас или нещо друго, или защото такъв стил сме избрали:

- Изнасяме си валидиращата логика в отделни функции. Това така или иначе е добра практика, защото изчистваме setter-ите от валидационния код, което ги прави по-лесни за четене (общо взето е очевидно какво правят)

void setAge(int age) {
    this->age = verifyAge(age);
}

// тук спокойно може параметърът и return типа да са const int&, но за int променливи дали ще предаваш по референция или по копие общо взето е еднакво бързо
int verifyAge(int age) {
    if(age < 0) {
        throw "Invalid age";
    }

    return age;
}

- Обърни внимание, че verifyAge връща int. Това на практика за setter-а не ни е нужно (можеше просто на 1 ред да verify-нем, а на следващия да присвоим). Обаче понеже искаме да го ползваме в initializer list-а, се принуждаваме да имаме return на стойността, ако е валидна:

Person(int age) :
    age(verifyAge(age)) {
}

- Така първо се изпълнява валидацията, която е във verifyAge, и ако тя мине, тогава получаваме стойност в age полето, която сме сигурни, че е вярна

- Между другото, в конкретния случай бих направил verifyAge да е static метод, защото не е свързан с текущия обект (не ползва this по никакъв начин), а е проверка която е еднаква за всички обекти от този клас. Но, може да имаш валидационна логика, която Е свързана с текущото състояние на обекта, в които случаи не може да е static. Също така verify може би трябва да е validate, ама вече съм тръгнал така да го пиша :D...

- Обърни внимание, че тези валидации има смисъл да се правят САМО за членове, които НЕ СА обекти на КЛАСОВЕ. Това е защото всеки член, който е обект от клас би следвало да си има собствен конструктор, който прави валидация сам за себе си. 

2. Може и да не ползваме initializer list изобщо, не е лош стил, на много места ще срещнеш код, който не ползва. Най-общо казано, ако инициализираш членове, които са примитивни типове данни е едно и също дали ще го направиш с initializer list или като извикаш setter в тялото на конструктора.

- Както казахме и преди малко, валидации има смисъл да правиш само когато съответния член е примитивен тип, така че съвсем спокойно можеш примитивните членове да ги инициализираш със setters в тялото на конструктора, а тези, които са обекти, да ги инициализираш в initializer list-а

- Въпреки това на много места кодът за инициализацията не ползва initializer list, независимо, че членовете са обекти - това най-често се случва под формата на init() методи. Тези init() методи най-често се пишат за да може няколко различни конструктора да преизползват една и съща логика (до преди C++11 не можеше от един конструктор да викаш друг и има много legacy код, който е писан като за C++03...) и се викат от тялото на конструктора. Съответно init() методите също могат да си викат каквито setters си искат

Така де, най-близкото нещо до правило, което бих ти дал, е: членовете, които са класове - в initializer list-а и без валидация, защото техните конструктори трябва да ги валидират, а членовете, които са просто примитивни типове (int, float...) - в тялото на конструктора през setter методи, които валидират. Това поне бих направил аз, но - отново - в C++ няма "правилни" и "грешни" начини, така че не го приемай за нещо, което не търпи промяна, в определени ситуации може да е по-подходящ друг подход. Важното е да разбираш кое какво прави, а как го правиш винаги ще е въпрос на стил.

Поздрави,

Жоро

1
01/04/2017 01:19:50
Wanker avatar Wanker 15 Точки

Много изчерпателен отговор. Не остави място за допълнителни въпроси. :)

Мерси, Жоро!

0