Правила хорошего кода

Просто о NET | создано: 23.11.2022 | опубликовано: 16.12.2022 | обновлено: 13.01.2024 | просмотров: 840

Несколько правил, которые помогут сделать ваш код более понятным, а значит, более "читабельным". Есть и другие плюсы, но ...

Зачем?

Но мы не будем в этот раз писать про "плюсы", надеюсь, вы их напишите в комментариях. Кстати, если найдет "минусы" - тоже пишите.

Правила написания хорошего кода

В англоязычном сообществе разработчиков "ходит" термин "code smell", то есть "код с запашком", или тот код, который "дурно пахнет". Если использовать русскую терминологию, то правильное слово "гавнокод". Не хочется делать так, чтобы статью находили по этому ключевому слову, поэтому будем писать просто "Правила написания хорошего кода".

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

Вашему вниманию, я хочу представить самые распространённые рекомендации, которые чаще всего встречаются в Интернете и упоминаются в разных книгах о программировании.

Правило 1: только один уровень отступа на метод

"Only One Level Of Indentation Per Method"

Согласен полностью с данным правилом. Не делайте ветвистые if...else или другие конструкции, которые добавляют отступы. Правильнее выделить в отдельный метод, который будет иметь понятное название и может быть даже описание (Summary) того, для чего вы его создали. И, кстати, пусть даже этот метод используется только один раз в одном месте. Читать подобные "выноски" код проще, чем разбираться в иерархии вложенных конструкций.

Правило 2: не использовать конструкцию else 

"Don't Use The Else Keyword"

Согласен полностью с данным правилом. Всегда или почти всегда можно написать конструкцию кода так, чтобы исключить использование else. Никогда не задумывались, почему, например, у строки есть метод:

string.IsNullOrEmpty()

Хотя, наверное, можно было бы назвать string.IsNotNullOrEmpty() и не ставить в начале if конструкции знак отрицания (!). Но почему именно такая конструкция используется. Полагаю, что применяется правило "Fail First", то есть "падай как можно быстрее".

if (string.IsNullOrEmpty(userName))
{
    return "Not valid username";
}

А дальше идет обычная логика и не нужен никакой else.

Правило 3: оборачивать примитивы в объекты

"Wrap All Primitives And Strings"

Опять же согласен с данным правилом. Не даром есть анти паттерн "primitive obsession", который говорит о злоупотреблении примитивами. Примитивы - убивают ООП. Это моё сугубо личное мнение, основанное на опыте.

Правило 4: коллекция первого класса

"Use first-class collections"

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

public class Person 
{
	public List<Address> Addresses { get; set; }	
	
	// other members skipped
}

public class Address
{
	public int MyProperty { get; set; }	
	
	// other members skipped
}

Представьте, как вы будете наполнять коллекцию адресов для пользователя:

void Main(object[] args)
{
	var person = new Person();
	var address1 = new Address();
	var address2 = new Address();
	var address3 = new Address();
	
	person.Addresses.Add(address1);
	person.Addresses.Add(address2);	
	person.Addresses.Add(address3);	
}

Кажется, что всё хорошо, и выглядит красиво, но это не так. Как только вы начнете добавлять какую-нибудь логику при обработке адресов, вы начнете плодить строчки кода, которые проще и правильнее всего положить в отдельный класс. Например, проверка адреса перед его добавлением в коллекцию:

void Main(object[] args)
{
	var person = new Person();
	var address1 = new Address();
	var address2 = new Address();
	var address3 = new Address();
	
	if (!person.Addresses.Contains(address1))
	{
		person.Addresses.Add(address1);
	}

	if (!person.Addresses.Contains(address2))
	{
		person.Addresses.Add(address2);
	}
	
	if (!person.Addresses.Contains(address2))
	{
		person.Addresses.Add(address2);
	}
}

А дальше - больше!

Можно добавить методы по обработке списка, методы добавления в список, методы формирования списка и т.д. и т.п. Но почему бы сразу не сделать класс AddressList (или AdddressCollection, или даже просто Addresses) и не реализовать уже в нем всю логику по обратке этой коллекции? Например, так:

public class AddressCollection
{
	public AddressCollection()
	{
		Addresses = new Collection<Address>();
	}
	
	public Collection<Address> Addresses { get; }


	public void Append(Address address)
	{
		if (!Addresses.Contains(address))
		{
			Addresses.Add(address);
		}
	}

	public void Remove(Address address)
	{
		if (Addresses.Contains(address))
		{
			Addresses.Remove(address);
		}
	}
	
	// other members skipped
}

Соответственно, класс Person нужно обновить, чтобы он использовал AddressCollection, то есть проделать своего рода композицию:

public class Person
{
	public AddressCollection Addresses { get; set; }
	
	// other members skipped
}

Тогда использовать можно таким образом:

void Main(object[] args)
{
	var person = new Person();
	var address1 = new Address();
	var address2 = new Address();
	var address3 = new Address();
	
	person.Addresses.Append(address1);
	person.Addresses.Append(address2);
	person.Addresses.Append(address3);
}

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

Правило 5: одна точка на строку

"One Dot Per Line"

Это хорошее правило и с точки зрения опыта его применения, могу сказать, что это вошло в привычку и помогает читать код более быстро. А при наличии в Visual Studio специальной быстрой команды перемещения строки на строку выше или ниже помогает легко менять порядок выполнения. А также, возможность закомментировать строку с точкой тоже никто не отменял. Очень удобно и читабельно.

Правило 6: не использовать аббревиатуры 

"Don't Abbreviate"

Но тут мне нечего сказать. Всё, итак, прекрасно известно. Сокращения и аббревиатуры — это зло! Особенно в долгосрочной перспективе.

Правило 7: не "надувайте" сущности

Keep All Entities Small

Правило так или иначе коррелирует с Single Responsibility Principle (SRP), которые гласит, что каждый "класс должен иметь одну и только одну причину на изменение". Данное утверждение инвариантно!

Правило 8: не используйте в классах более двух переменных

"No Cases With Mode Than Two Variables"

Сомнительна положительная сторона данного правила. Чтобы обосновать его жизнеспособность потребуется призадуматься. Конечно же всё хорошо в меру, и когда у вас в классе 100 или 500 переменных, читать такой класс будет просто невозможно. Но когда из-за невозможности добавить третью или четвертую переменную вы будете создавать новый класс — это тоже не выход из ситуации. Я придерживаюсь разумных параметров для данного правила. Стремлюсь держать количество переменных не более двух. Но если их у меня пять, я смотрю по обстоятельствам в конкретном классе.

Правило 9: избегайте использование getter and setter

"Avoid getters and setters whenever possible"

Это правило отсылает к принципам, описанных в Rich Domain Model и Anemic Domain Model. Если учесть, что концепция Anemic Domain Model гораздо более распространена в кругах людей, которые пишут код, а Rich Domain Model это вектор движения в сторону Domain Driven Design, то следовать этому правилу хорошая практика.

Заключение

В качестве заключения хотелось бы сказать следующее. Я полагаю, что все паттерны, правила, рекомендации, архитектуры и прочие штуки, которые придумывают разработчики (обычно разработчики со стажем) так или иначе, призваны предупредить молодых разработчиков не совершать ошибок, которые могут повлечь за собой нежелательные последствия. Отсюда и ряд архитектур, дополняющих и развивающих друг друга, и правила именования и стили написания кода, и паттерны, и правила, и еще куча всего, что даже может быть еще не известно (мне). Самое главное понимать, что не существует универсального успешного рецепта, который решит все проблемы в ваших проектах и или превратит ваш код в искусство. Всё хорошо в меру и к месту. В этой связи я вывел свои правила высокого уровня по отношению ко всем перечисленным выше штукам:

  1. Не следует слепо следовать.
  2. Следует прислушаться и следовать только тем, что понятны и/или подтверждены опытом.

Какие правила вы могли бы добавить к перечисленным? Какие правила соблюдаете, а какие нет?