Полный курс C#: от новичка до профессионала

Этот курс проведет вас через все этапы изучения C#, начиная с базового синтаксиса и заканчивая сложными архитектурными паттернами. Вы освоите платформу .NET, научитесь писать чистый код и создавать масштабируемые приложения.

1. Основы языка C#: синтаксис, типы данных и управляющие конструкции

Основы языка C#: синтаксис, типы данных и управляющие конструкции

Добро пожаловать в первую статью курса «Полный курс C#: от новичка до профессионала». Если вы читаете эти строки, значит, вы решили освоить один из самых мощных, востребованных и элегантных языков программирования в мире. C# (произносится как «си-шарп») — это универсальный язык, созданный компанией Microsoft, который позволяет разрабатывать всё: от простых консольных приложений до сложных веб-сервисов, мобильных игр и систем искусственного интеллекта.

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

Структура программы на C#

Любой язык, будь то русский, английский или C#, имеет свои правила грамматики и пунктуации. В программировании это называется синтаксисом. Если вы нарушите правила, компилятор (программа-переводчик, превращающая ваш код в понятный машине язык) выдаст ошибку.

Давайте взглянем на минимальную структуру программы:

!Структура вложенности элементов в программе на C#

Разберем этот код по кирпичикам:

  • using System; — это подключение библиотеки. Представьте, что вы хотите использовать сложные инструменты, например, молоток. Вместо того чтобы создавать молоток с нуля, вы просто открываете ящик с инструментами. System — это стандартный ящик инструментов C#.
  • namespace MyFirstApp — пространство имен. Это как фамилия для вашего кода, чтобы он не перепутался с чужим кодом, если классы будут называться одинаково.
  • class Program — класс. В C# весь код живет внутри классов. Это контейнер для данных и методов.
  • static void Main(string[] args) — это точка входа. Когда вы запускаете программу, компьютер ищет именно метод Main и начинает выполнение команд с него.
  • { } (фигурные скобки) — они обозначают начало и конец блока кода. Все, что внутри, относится к заголовку перед скобкой.
  • ; (точка с запятой) — это как точка в конце предложения. Каждая команда должна заканчиваться этим символом.
  • > C# — регистрозависимый язык. Это значит, что Main, main и MAIN — это три разных слова. Если вы напишете console.writeline вместо Console.WriteLine, программа не заработает.

    Переменные и типы данных

    Программам нужно где-то хранить информацию: возраст пользователя, цену товара, текст сообщения. Для этого используются переменные.

    Переменная — это именованная область в памяти компьютера. Представьте себе коробку, на которой написано название (имя переменной), и в которую можно положить определенный предмет (значение).

    C# — язык со строгой типизацией. Это значит, что каждая коробка предназначена для определенного типа вещей. Вы не можете положить кота в коробку для обуви, а текст — в переменную для целых чисел.

    !Метафора переменных как коробок для разных типов данных

    Основные типы данных

    Вот самые часто используемые типы, которые вам понадобятся в 99% случаев:

    * int (от integer) — целые числа. Например: 10, -5, 42000. * double — дробные числа (числа с плавающей точкой). Например: 3.14, -0.001, 5.0. * string — строка (текст). Текст всегда заключается в двойные кавычки. Например: "Привет", "C# - это круто". * char — один символ. Заключается в одинарные кавычки. Например: 'A', '7', '!'. * bool (от boolean) — логический тип. Имеет всего два значения: true (истина) или false (ложь).

    Объявление и инициализация

    Чтобы создать переменную, мы сначала пишем тип, потом имя, а затем (необязательно) присваиваем значение через знак равно =. Знак = в программировании — это не равенство, а команда «присвоить».

    Арифметика

    С числами можно производить математические операции. Основные операторы:

    * + (сложение) * - (вычитание) (умножение) * / (деление) * % (остаток от деления)

    Важный нюанс с делением: если вы делите два целых числа (int), результат тоже будет целым. Дробная часть просто отбрасывается.

    Управляющие конструкции: Ветвление

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

    Представьте, что вы подходите к развилке на дороге. Если горит зеленый свет — идем, иначе — стоим.

    !Блок-схема работы условного оператора if-else

    Синтаксис выглядит так:

    Операторы сравнения

    Чтобы задать условие внутри скобок if (...), мы используем операторы сравнения:

    * == — равно (два знака равно, так как один — это присваивание!) * != — не равно * > — больше * < — меньше * >= — больше или равно * <= — меньше или равно

    Также можно объединять условия с помощью логических операторов: * && (И) — оба условия должны быть верны. * || (ИЛИ) — хотя бы одно условие должно быть верно.

    Пример:

    Циклы: Повторение действий

    Часто нам нужно выполнить одно и то же действие много раз. Например, вывести на экран фразу «Я не буду болтать на уроках» 100 раз. Писать 100 строк кода глупо. Для этого есть циклы.

    Цикл for

    Используется, когда мы точно знаем, сколько раз нужно повторить действие.

    Разберем заголовок цикла:

  • int i = 0 — создаем счетчик i и ставим его в 0.
  • i < 5 — условие: пока i меньше 5, цикл работает.
  • i++ — шаг: после каждого выполнения увеличиваем i на 1 (запись i++ аналогична i = i + 1).
  • Цикл while

    Используется, когда мы не знаем точное число повторений, но знаем условие, при котором нужно продолжать.

    !Циклический процесс выполнения программы

    Ввод данных с консоли

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

    Важно помнить: Console.ReadLine() всегда возвращает строку (string). Если вы хотите ввести число, вам нужно превратить строку в число (распарсить её).

    Заключение

    Сегодня мы заложили первый камень в фундамент вашего мастерства C#. Мы узнали:

  • Как выглядит скелет программы.
  • Что такое переменные и почему важно соблюдать типы данных.
  • Как научить программу думать с помощью if.
  • Как избавить себя от рутины с помощью циклов for и while.
  • Это база, без которой невозможно написать даже самую простую игру или приложение. В следующей статье мы углубимся в более сложные концепции и научимся создавать свои собственные методы, чтобы наш код стал еще чище и профессиональнее.

    2. Объектно-ориентированное программирование: классы, наследование, интерфейсы и полиморфизм

    Объектно-ориентированное программирование: классы, наследование, интерфейсы и полиморфизм

    Приветствую вас во второй статье курса «Полный курс C#: от новичка до профессионала». В прошлый раз мы научились писать простые инструкции: объявлять переменные, складывать числа и использовать циклы. Это похоже на то, как если бы мы выучили слова и научились строить простые предложения.

    Но представьте, что вам нужно написать роман или спроектировать небоскреб. Простых предложений и кирпичей недостаточно. Вам нужна структура. В программировании такой структурой является Объектно-ориентированное программирование (ООП).

    ООП — это методология, которая позволяет нам моделировать реальный мир в коде. Вместо того чтобы писать тысячи строк разрозненных команд, мы создаем «объекты», которые взаимодействуют друг с другом. Сегодня мы разберем четыре кита, на которых держится C#: классы, инкапсуляция, наследование и полиморфизм.

    Классы и объекты: Чертеж и Здание

    Самая важная концепция в C# — это различие между классом и объектом.

    * Класс — это шаблон, чертеж или описание. Он не существует в физическом мире (в памяти компьютера как данные), пока мы его не используем. Это просто инструкция. * Объект (или экземпляр класса) — это конкретная вещь, созданная по этому чертежу.

    !Иллюстрация различия между абстрактным классом (чертеж) и конкретным объектом (автомобиль)

    Создание класса

    Давайте создадим класс Car (Автомобиль). У любого автомобиля есть характеристики (цвет, модель) и действия (ехать, гудеть).

    Создание объекта

    Теперь, когда у нас есть чертеж, мы можем создать конкретный автомобиль в методе Main.

    Ключевое слово new — это команда строителям: «Возьми чертеж Car и выдели память под реальный объект».

    Инкапсуляция: Защита данных

    Вы заметили слово public в примере выше? Это модификатор доступа. Он говорит о том, что любой может изменить модель или цвет машины.

    Однако в реальном мире мы не позволяем кому попало копаться в двигателе. Мы прячем сложные механизмы под капот, оставляя водителю только руль и педали. Это и есть инкапсуляция.

    В C# мы используем модификатор private, чтобы скрыть данные, и публичные методы (свойства), чтобы дать к ним контролируемый доступ.

    Если бы balance был публичным, кто угодно мог бы написать account.balance = -1000000;. Инкапсуляция защищает нас от таких ошибок.

    Наследование: Не повторяй себя

    Представьте, что вы создаете игру. У вас есть Warrior (Воин), Mage (Маг) и Archer (Лучник). У всех них есть имя, уровень здоровья и метод Move (Двигаться).

    Писать один и тот же код три раза — плохая практика. Вместо этого мы создаем общий класс (родительский) и наследуем от него другие классы.

    !Схема наследования, показывающая передачу общих свойств от родительского класса к дочерним

    Теперь объект класса Mage автоматически имеет Name, Health и умеет делать Move(), хотя мы не писали это внутри самого класса Mage.

    Полиморфизм: Один интерфейс, множество форм

    Полиморфизм — самое сложное слово, но очень простая концепция. Оно означает «много форм». Это способность объектов с разной внутренней реализацией вести себя одинаково для внешнего наблюдателя.

    Допустим, мы хотим посчитать площадь разных фигур. Формулы разные, но действие одно — «Посчитать площадь».

    Для демонстрации давайте вспомним немного математики. Например, площадь круга вычисляется по формуле:

    где — это площадь круга, — математическая константа (примерно 3.14), а — радиус круга.

    В коде мы можем использовать ключевое слово virtual в базовом классе (разрешение на изменение) и override в наследнике (само изменение).

    Теперь самое интересное. Мы можем создать список фигур и пройтись по нему, не зная, какая именно фигура лежит в переменной.

    Программа сама поймет: если это круг — используй формулу с , если квадрат — просто перемножь стороны.

    Интерфейсы: Контракты

    Иногда наследования недостаточно. Что общего у Bird (Птица) и Airplane (Самолет)? Они оба летают. Но наследовать самолет от птицы (или наоборот) — глупо. У них нет общего предка, кроме физического объекта.

    Здесь на помощь приходят интерфейсы. Интерфейс — это контракт. Он не говорит как делать, он говорит что должно быть сделано.

    В C# названия интерфейсов принято начинать с буквы I.

    Главное отличие от классов: класс может наследовать только от одного родителя, но может реализовывать сколько угодно интерфейсов. Например, смартфон может быть ICamera, IPhone, IMusicPlayer одновременно.

    Заключение

    Мы разобрали фундамент современной разработки:

  • Классы и объекты позволяют описывать сущности.
  • Инкапсуляция защищает данные от некорректного использования.
  • Наследование помогает избежать дублирования кода.
  • Полиморфизм позволяет работать с разными объектами универсальным способом.
  • Интерфейсы задают стандарты поведения для совершенно разных классов.
  • Эти концепции могут показаться абстрактными, но именно они позволяют создавать сложные системы, которые легко поддерживать и расширять. В следующей статье мы поговорим о коллекциях и массивах, чтобы научиться хранить множество наших созданных объектов эффективно.

    3. Продвинутые возможности языка: коллекции, обобщения (Generics) и LINQ

    Продвинутые возможности языка: коллекции, обобщения (Generics) и LINQ

    Добро пожаловать в третью статью курса «Полный курс C#: от новичка до профессионала». В предыдущих частях мы заложили фундамент: изучили синтаксис, переменные, циклы и погрузились в мир объектно-ориентированного программирования (ООП). Теперь вы умеете создавать классы, строить иерархии наследования и защищать данные с помощью инкапсуляции.

    Но реальные приложения редко работают с одним объектом. Обычно нам нужно управлять сотнями, тысячами или миллионами объектов: списком пользователей, каталогом товаров, очередью заказов. Как хранить их эффективно? Как быстро находить нужные данные? Как писать код, который работает с любыми типами данных, не дублируя логику?

    Сегодня мы ответим на эти вопросы, изучив три мощнейших инструмента C#: Обобщения (Generics), Коллекции и LINQ.

    Обобщения (Generics): Универсальный код

    Вспомните массивы. Если нам нужен массив чисел, мы пишем int[]. Если массив строк — string[]. А что, если мы хотим написать метод, который меняет местами два элемента в массиве, независимо от того, числа это или строки?

    Без обобщений нам пришлось бы писать отдельные методы для каждого типа данных (SwapInt, SwapString, SwapCar и т.д.) или использовать устаревший тип object, что небезопасно и медленно. Обобщения позволяют нам создать «шаблон» кода.

    Синтаксис угловых скобок

    Обобщения используют символ <T> (от слова Type), который является заполнителем. Представьте, что T — это пустая коробка, в которую мы положим конкретный тип в момент использования кода.

    Теперь мы можем использовать этот класс для чего угодно:

    Главное преимущество обобщений — безопасность типов. Вы не сможете случайно положить строку в numberBox. Компилятор выдаст ошибку еще до запуска программы.

    Коллекции: Прощайте, массивы

    Массивы в C# имеют один существенный недостаток: они фиксированного размера. Если вы создали массив на 5 элементов, вы не можете добавить шестой, не пересоздавая массив заново. В реальной жизни мы редко знаем точное количество элементов заранее.

    Для решения этой проблемы в пространстве имен System.Collections.Generic существуют коллекции. Это умные структуры данных, которые умеют расширяться динамически.

    !Сравнение статического массива и динамического списка List

    List<T>: Динамический список

    List<T> — это самая популярная коллекция. Она работает как массив, но умеет расти.

    Dictionary<TKey, TValue>: Словарь

    Иногда доступ по индексу (0, 1, 2) неудобен. Представьте телефонную книгу: вы ищете номер не по номеру страницы, а по имени человека. Для этого существует Dictionary (Словарь).

    Словарь хранит пары Ключ — Значение. Ключи должны быть уникальными.

    Производительность коллекций

    Важно понимать, когда использовать список, а когда словарь. В программировании эффективность алгоритмов часто оценивается с помощью «O-нотации».

    Поиск элемента в обычном списке (List) занимает линейное время:

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

    Поиск в словаре (Dictionary) по ключу работает мгновенно, за константное время:

    где означает, что время поиска не зависит от количества элементов. Будь в словаре 10 записей или миллион — поиск по ключу займет одинаковое (очень короткое) время.

    Queue<T> и Stack<T>

    Существуют и специализированные коллекции:

    * Queue<T> (Очередь): Работает по принципу FIFO (First In, First Out — «Первым вошел, первым вышел»). Как очередь в магазине. * Stack<T> (Стек): Работает по принципу LIFO (Last In, First Out — «Последним вошел, первым вышел»). Как стопка тарелок: вы берете ту, которую положили последней.

    LINQ: Магия работы с данными

    Представьте, что у вас есть список из 1000 товаров, и вам нужно найти все товары дешевле 500 рублей, отсортировать их по названию и выбрать только их имена.

    Без LINQ вам пришлось бы писать циклы foreach, условия if, создавать временные списки и писать алгоритмы сортировки. Код занял бы 20 строк.

    LINQ (Language Integrated Query) позволяет сделать это в одну строку. Это язык запросов, встроенный прямо в C#.

    Основные методы LINQ

    Для работы с LINQ нужно подключить пространство имен using System.Linq;. Большинство методов LINQ не меняют исходную коллекцию, а возвращают новую.

    Допустим, у нас есть класс Product:

    #### 1. Фильтрация (Where)

    Метод Where отбирает элементы, соответствующие условию.

    Здесь p => p.Price < 500 — это лямбда-выражение. Читается так: «для каждого элемента p верни истину, если цена p меньше 500».

    #### 2. Сортировка (OrderBy)

    Метод OrderBy (по возрастанию) и OrderByDescending (по убыванию) сортирует коллекцию.

    #### 3. Проекция (Select)

    Метод Select позволяет преобразовать данные. Например, вытащить только названия товаров, отбросив цены.

    #### 4. Объединение в цепочку

    Вся мощь LINQ раскрывается, когда мы объединяем методы в цепочку вызовов:

    !Процесс обработки данных через цепочку методов LINQ

    Важные методы получения данных

    * ToList() и ToArray() — выполняют запрос и превращают результат в список или массив. * First() — возвращает первый элемент. Если элементов нет, выбросит ошибку. * FirstOrDefault() — возвращает первый элемент или значение по умолчанию (null), если ничего не найдено. Это безопаснее. * Any() — возвращает true, если в коллекции есть хотя бы один элемент, удовлетворяющий условию.

    Заключение

    Сегодня мы сделали огромный шаг вперед. Мы перешли от написания простых классов к созданию гибких и масштабируемых систем.

  • Generics позволяют писать код один раз и использовать его для любых типов данных, сохраняя типобезопасность.
  • Коллекции (List, Dictionary) дают нам инструменты для хранения динамических данных, превосходящие обычные массивы.
  • LINQ превращает рутинную обработку данных (поиск, сортировку, фильтрацию) в элегантные и читаемые запросы.
  • Эти инструменты используются в C# повсеместно: от разработки игр на Unity до создания веб-серверов на ASP.NET. В следующей статье мы разберем, как работать с файловой системой и обрабатывать исключительные ситуации, чтобы ваши программы были надежными и умели сохранять данные.

    4. Асинхронное программирование, многопоточность и работа с платформой .NET

    Асинхронное программирование, многопоточность и работа с платформой .NET

    Рад видеть вас на четвертом этапе нашего курса «Полный курс C#: от новичка до профессионала». Мы уже прошли большой путь: от переменных и циклов до создания сложных классов и обработки данных с помощью LINQ. Ваш код стал чистым, структурированным и умным.

    Но есть одна проблема. До сих пор все наши программы работали синхронно. Это значит, что код выполнялся строго строчка за строчкой. Если одна операция занимала 5 секунд (например, скачивание файла), вся программа «зависала» на эти 5 секунд, и пользователь не мог нажать ни одну кнопку.

    Сегодня мы научимся писать код, который умеет делать несколько дел одновременно. Мы разберем асинхронность, многопоточность и узнаем, как устроена платформа .NET изнутри.

    Синхронное vs Асинхронное выполнение

    Давайте начнем с простой аналогии из жизни. Представьте, что вы работаете бариста в кофейне.

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

    Асинхронный подход: Вы принимаете заказ, передаете его на кухню (или запускаете кофемашину) и сразу же поворачиваетесь к следующему клиенту, пока кофе варится в фоновом режиме. Когда кофе готов, вы просто отдаете его.

    В программировании точно так же. Мы не хотим блокировать работу программы ради долгих операций.

    !Сравнение блокирующего (синхронного) и неблокирующего (асинхронного) выполнения задач.

    Многопоточность: Класс Thread

    Исторически первым способом делать дела одновременно было использование потоков (Threads). Поток — это наименьшая единица выполнения кода.

    По умолчанию ваша программа работает в одном потоке — Main Thread (основной поток). Если вы создадите новый поток, он будет работать параллельно с основным.

    Если вы запустите этот код, строчки «Главный поток» и «Второй поток» будут перемешаны. Они работают одновременно.

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

    Асинхронность: async и await

    Microsoft представила революционный подход — Task-based Asynchronous Pattern (TAP). Вместо того чтобы вручную создавать потоки, мы оперируем «Задачами» (Task) и используем два волшебных ключевых слова: async и await.

    * async — ставится перед объявлением метода. Это маркер, который говорит компилятору: «В этом методе будет использоваться асинхронность». * await — ставится перед долгой операцией. Это команда: «Запусти эту задачу и освободи поток, пока она не закончится. Когда закончится — вернись сюда и продолжи».

    Пример асинхронного метода

    Представьте, что мы «скачиваем» файл из интернета.

    Правила асинхронности

  • Возвращаемые типы:
  • * Task — если метод ничего не возвращает (аналог void). * Task<T> — если метод возвращает значение типа T (например, Task<int>). * void — используется только для обработчиков событий (например, нажатие кнопки Button_Click). В остальных случаях избегайте async void, так как такие методы невозможно ожидать и сложно ловить в них ошибки.

  • Заражение: Асинхронность «заразна». Если вы используете await внутри метода, сам метод должен быть async, и тот, кто его вызывает, тоже должен использовать await.
  • Получение результата из задачи

    Если асинхронный метод должен вернуть число, мы используем Task<int>.

    Параллельное программирование

    Иногда нам нужно не просто ждать завершения операции (как при скачивании файла), а использовать все ядра процессора для тяжелых вычислений. Например, обработать 1000 фотографий.

    Для этого существует класс Parallel.

    Важно: Не путайте асинхронность и параллелизм. * Асинхронность (await) — это про ожидание без блокировки (ввод-вывод, сеть, диск). * Параллелизм (Parallel) — это про одновременную работу процессора (вычисления).

    Платформа .NET: Что под капотом?

    Мы много пишем на C#, но кто на самом деле выполняет наш код? C# — это язык, который работает поверх мощной платформы .NET.

    CLR (Common Language Runtime)

    Когда вы нажимаете кнопку «Start» в Visual Studio, происходит магия:

  • Компилятор превращает ваш C# код не в машинный код (нули и единицы), а в IL-код (Intermediate Language). Это промежуточный язык.
  • Запускается CLR (Common Language Runtime) — виртуальная машина .NET.
  • Внутри CLR работает JIT-компилятор (Just-In-Time), который на лету переводит IL-код в машинный код, понятный конкретно вашему процессору.
  • Это позволяет программам на C# запускаться на разных компьютерах без перекомпиляции.

    Garbage Collector (Сборщик мусора)

    В языках вроде C++ программист должен сам выделять память под объекты и сам её освобождать. Забыл освободить — получил утечку памяти. Освободил раньше времени — программа упала.

    В C# работает Garbage Collector (GC). Это автоматический уборщик.

    Представьте, что память — это ресторан. Вы (программист) сажаете гостей (создаете объекты new Car()). Когда гости уходят (переменная больше не используется), вы не обязаны убирать за ними стол. Периодически выходит уборщик (GC), смотрит, за какими столами никто не сидит, и очищает их для новых гостей.

    Вам не нужно думать об удалении объектов. GC сделает это за вас.

    NuGet: Экосистема пакетов

    Профессиональный разработчик не пишет всё с нуля. Если вам нужно работать с JSON, рисовать графики или отправлять запросы в Telegram, скорее всего, кто-то уже написал для этого библиотеку.

    В мире .NET существует NuGet — это огромный репозиторий бесплатных библиотек (пакетов).

    Чтобы добавить суперсилу в свой проект:

  • В Visual Studio нажмите правой кнопкой на проект -> «Manage NuGet Packages».
  • Найдите нужную библиотеку (например, Newtonsoft.Json для работы с JSON).
  • Нажмите «Install».
  • Теперь вы можете использовать готовые классы из этой библиотеки, просто подключив нужное пространство имен через using.

    Заключение

    В этой статье мы шагнули в мир профессиональной разработки:

  • Мы узнали, что асинхронность (async/await) позволяет программам оставаться отзывчивыми, не блокируя интерфейс во время долгих операций.
  • Мы коснулись многопоточности и поняли, что Task — это современный стандарт работы с параллельными задачами.
  • Мы заглянули под капот .NET и узнали про CLR и Garbage Collector, который бережет наши нервы, управляя памятью.
  • Мы познакомились с NuGet, открывающим доступ к тысячам готовых решений.
  • Теперь вы обладаете полным набором инструментов для создания серьезных приложений. Вы знаете синтаксис, ООП, коллекции, LINQ и умеете писать эффективный асинхронный код. Это прочный фундамент, на котором строится карьера любого .NET разработчика.

    5. Профессиональная разработка: паттерны проектирования, принципы SOLID и модульное тестирование

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

    Добро пожаловать в пятую статью курса «Полный курс C#: от новичка до профессионала». Мы прошли долгий путь: от первой строки Hello World до асинхронных запросов и работы с базой данных. Ваш код работает, он выполняет поставленные задачи. Но достаточно ли этого?

    В профессиональной разработке «работающий код» — это лишь половина дела. Код должен быть поддерживаемым, расширяемым и тестируемым. Представьте, что вы строите не собачью будку, а небоскреб. Если вы положите кирпичи криво в фундаменте, на 50-м этаже здание рухнет. В программировании это называется «технический долг».

    Сегодня мы изучим инструменты архитектора программного обеспечения: принципы SOLID, паттерны проектирования и модульное тестирование. Это те навыки, которые отличают «кодера» от инженера-разработчика.

    Принципы SOLID: Фундамент архитектуры

    SOLID — это акроним из пяти принципов, предложенных Робертом Мартином (Дядя Боб). Соблюдение этих правил позволяет писать код, который легко менять и сложно сломать.

    !Пять колонн архитектуры SOLID, поддерживающие надежное приложение

    S — Single Responsibility Principle (Принцип единственной ответственности)

    Класс должен иметь только одну причину для изменения.

    Это значит, что класс должен делать только одну вещь. Если у вас есть класс User, он не должен уметь сохранять себя в базу данных, отправлять email и рассчитывать налоги. Это «Божественный объект» (God Object) — антипаттерн.

    Плохо:

    Хорошо: Разделите ответственности. ReportGenerator создает данные, FileSaver сохраняет, Printer печатает.

    O — Open/Closed Principle (Принцип открытости/закрытости)

    Программные сущности должны быть открыты для расширения, но закрыты для модификации.

    Если вы хотите добавить новую функциональность, вы не должны переписывать старый работающий код. Вы должны дописывать новый.

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

    Теперь, чтобы добавить новогоднюю скидку, вам не нужно трогать старые классы. Вы просто создаете новый класс NewYearDiscount.

    L — Liskov Substitution Principle (Принцип подстановки Барбары Лисков)

    Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы.

    Простыми словами: если класс B наследуется от класса A, то мы должны иметь возможность использовать B везде, где используется A, и ничего не должно сломаться.

    Классический пример нарушения: Квадрат и Прямоугольник. В математике квадрат — это прямоугольник. В программировании, если вы наследуете Square от Rectangle и меняете ширину квадрата, его высота тоже должна измениться. Если код, работающий с прямоугольником, этого не ожидает — принцип нарушен.

    I — Interface Segregation Principle (Принцип разделения интерфейса)

    Клиенты не должны зависеть от методов, которые они не используют.

    Лучше создать много маленьких специализированных интерфейсов, чем один большой интерфейс общего назначения.

    Представьте интерфейс IWorker с методами Work() и Eat(). Если мы создадим класс Robot, который реализует этот интерфейс, нам придется реализовать метод Eat(), хотя роботы не едят. Лучше разделить интерфейс на IWorkable и IFeedable.

    D — Dependency Inversion Principle (Принцип инверсии зависимостей)

    Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.

    Это самый сложный, но самый важный принцип. Он лежит в основе Dependency Injection (Внедрения зависимостей).

    Допустим, ваш класс Shop напрямую создает внутри себя класс Database. Это жесткая сцепка. Вы не сможете протестировать магазин без реальной базы данных. Вместо этого Shop должен зависеть от интерфейса IDatabase.

    Паттерны проектирования: Готовые рецепты

    Паттерны — это проверенные решения типичных проблем. Это не готовый код, который можно скопировать, а концепция. В книге «Gang of Four» (GoF) описано 23 классических паттерна. Мы разберем самые популярные.

    Singleton (Одиночка)

    Гарантирует, что у класса есть только один экземпляр, и предоставляет к нему глобальную точку доступа. Часто используется для конфигураций или подключений к базе данных.

    Factory Method (Фабричный метод)

    Определяет интерфейс для создания объекта, но оставляет подклассам решение о том, какой класс инстанцировать. Это позволяет делегировать создание объектов.

    Представьте логистическое приложение. У вас есть грузовики и корабли. Фабрика решает: если доставка по суше — создать Truck, если по морю — Ship. Код, который заказывает доставку, не знает деталей создания транспорта.

    Observer (Наблюдатель)

    Позволяет одним объектам следить за событиями в других объектах. Это основа событийной модели C# (event).

    Пример: Вы подписались на рассылку новостей. Когда выходит новость (событие), всем подписчикам (наблюдателям) приходит уведомление. Вам не нужно каждую секунду проверять сайт.

    Модульное тестирование (Unit Testing)

    Как убедиться, что ваш код работает правильно? Запустить и потыкать кнопки? Это долго и ненадежно. Профессионалы пишут код, который проверяет другой код.

    Unit-тест (модульный тест) — это маленький метод, который проверяет изолированную часть программы (обычно один метод класса).

    Фреймворки для тестирования

    В мире .NET стандартом де-факто являются: * xUnit — самый современный и популярный. * NUnit — классический, мощный фреймворк. * MSTest — встроенный от Microsoft.

    Мы будем использовать примеры на xUnit.

    Структура теста: AAA

    Хороший тест строится по паттерну AAA:

  • Arrange (Подготовка) — создаем объекты и настраиваем данные.
  • Act (Действие) — вызываем метод, который хотим проверить.
  • Assert (Утверждение) — проверяем, что результат совпадает с ожиданием.
  • !Паттерн AAA: Arrange, Act, Assert

    Пример теста

    Допустим, у нас есть простой калькулятор:

    Напишем для него тест:

    Зачем нужны Mock-объекты?

    Вспомните принцип Dependency Inversion. Если ваш класс зависит от базы данных, как его протестировать? Мы не хотим подключаться к реальной БД в тестах (это медленно и опасно).

    Мы создаем Mock (имитацию). Это фальшивый объект, который притворяется базой данных.

    Для создания моков в .NET часто используют библиотеку Moq.

    Заключение

    Сегодня мы перешли на новый уровень осознанности в программировании.

  • SOLID помогает нам проектировать гибкую архитектуру.
  • Паттерны дают готовые решения для частых проблем.
  • Unit-тесты дают уверенность, что наши изменения ничего не сломали.
  • Эти темы необъятны. О каждом принципе SOLID написаны книги. Но теперь у вас есть карта, по которой можно двигаться дальше. В следующей, заключительной статье основного блока, мы создадим полноценное приложение, объединив все полученные знания: от переменных до архитектуры и тестов.