Некоторое время назад, вернувшись после полугодового отпуска в функциональном мире, назад в ООП, я в который раз наступил на привычные грабли: случайно изменил состояние.
private double fBm(Vector2D v, int y)
{
double result = 0f;
double freq = Frequency;
for (int i = 0; i < Octaves; ++i)
{
result += NoiseFn(permutation, v * freq) * Amplitude;
freq *= Lacunarity;
Amplitude *= Gain; // <-- Вот тут.
}
return result;
}
В ФП нужно особо постараться чтобы получить такой баг, а в некоторый языках невозможно в принципе. Салат из полезной работы и состояния класса не радовал, простор для ошибок даже в этой четверке строк слишком широк. Я стал думать как можно уменьшить площадь этих грабель и вывел следующее:
Во-первых нужно сделать this
обязательным. Поставить поле не с той стороны выражения слишком легко. Если бы я везде явно указал контекст, скорее всего сразу бы заметил, что ступил. К сожалению, компилятор C# нельзя заставить требовать this
, а в статических проверках студии есть только правило которое работает наоборот, то есть заставляет убирать this
. Так что, видимо придется писать свое.
Во-вторых нужно исключить использование полей класса из тела метода, а все изменения записывать в конце метода, если они есть. У нас получается три абзаца. В первом мы объявляем все что нам понадобится, во втором делаем полезную работу, в третьем сохраняем состояние.
public void DoAThing(int input)
{
// Первый абзац
int a = this.A;
int b = this.B;
int result = 0;
// Второй абзац
for (int i = 0; i < input; ++i)
result += (a + b) * i;
// Третий абзац
this.Thing = result;
}
В первом абзаце все состояние которое нужно мы сохраняем локально.
Во втором, this
не фигурирует.
В третьем this
всегда должен быть первым словом. Тут же будут вызываться другие методы которые меняют состояние.
return
можно добавить в любое место, на поведение метода он не повлияет.
Плюсы
- Багов со случайным изменением состояния станет меньше.
- Отсутствие третьего абзаца делает метод чистой функцией, а значит его можно запускать параллельно.
Минусы
- Дисциплина. Фу-фу-фу. Без автоматических проверок, легко все испортить.
- В некоторых случаях этот подход будет работать медленней.
- Может выглядеть странно или глупо. Вот к примеру:
{
int a = this.A;
int b = this.B;
return a + b;
}
Вместо заключения
Как думаете? В этом есть смысл или я парюсь? Если вы уже делаете подобное или натягиваете другие части ФП на ООП, пожалуйста поделитесь.