Рефакторинг в C# — наше всё: как MainViewModel в WPF стал 1300+ строк и как пришлось убираться (DevCrm 08)!

Когда счётчик строк переваливает за 1300, код начинает сопротивляться.
Когда счётчик строк переваливает за 1300, код начинает сопротивляться.

В очередной раз запускаю проверить, как всё работает, открываю MainViewModel.cs, и тут до меня доходит: тысяча триста с лишним строк. И это уже не смешно.

Открываю его, а там всё намешано. Авторизация, контакты, юзеры, задачи, чат, экспорт. Команды, свойства, фильтры, видимость элементов. Всё в одной куче, и понять, где что начинается и где заканчивается, можно только если ты сам это писал неделю назад. А если вернуться через месяц?

Знаете это чувство, когда открываешь ящик стола, а там всё свалено в одну кучу: от скрепок до старых чеков? Вещи-то все нужные, но найти что-то конкретное — целое приключение. Вот с MainViewModel было так же. Вроде бы «просто главный файл», а по факту — отдельный мир со своей логикой, в котором можно заблудиться.

Я даже помню, как выглядел этот файл. Одних команд было двадцать шесть штук.

public ICommand AddCommand { get; private set; } = null!; public ICommand SaveCommand { get; private set; } = null!; public ICommand DeleteCommand { get; private set; } = null!; public ICommand ClearCommand { get; private set; } = null!; public ICommand RefreshCommand { get; private set; } = null!; public ICommand ExportExcelCommand { get; private set; } = null!; public ICommand ExportPdfCommand { get; private set; } = null!; public ICommand LoginCommand { get; private set; } = null!;

И это только команды. Свойства, поля, методы загрузки, сохранения, удаления, экспорта, бэкапа. Каждое свойство с вызовом OnPropertyChanged. Каждый метод обёрнут в try-catch с логированием. Когда количество строк перевалило за тысячу триста, навигация по файлу превратилась в квест. Это была та самая минута, когда рефакторинг кода из приятной теории становится вопросом выживания.

Чтобы добраться от команды до метода, который она вызывает, нужно было прокрутить пол-экрана. Пока прокрутишь, уже забываешь, зачем вообще сюда пришёл. Откладывать рефакторинг кода дальше было просто опасно: такой объём в одном месте не выдержит ни один разработчик.

Как я дошёл до жизни такой

Всё началось с того, что я делал свою CRM на WPF. Программу для учёта контактов — многие зовут её срм или црм, суть одна. Хотелось получить локальную crm, которая живёт прямо на компьютере, офлайн, без подписок и интернета в придачу. Дёшево и без лишнего шума: типичная срм система для личного использования, только моя и только под меня. Вся бизнес-логика жила во вьюмоделях, и сначала это казалось удобным.

Сначала MainViewModel был маленьким и пушистым. Команды для контактов, пара свойств, базовая логика. Всё работало, всё было понятно. Но потом добавились юзеры. Потом задачи. Потом чат. Потом экспорт в Excel, Word, PDF. Потом видимость вкладок. Потом фильтры. С каждым пунктом большой ViewModel рос на глазах.

Каждая новая фича добавляла 50–100 строк. И всё в один файл. Потому что «ну это же просто команда, она же связана с главным окном, пусть тут и живёт». Классическая ловушка, в которую попадает любой, кто откладывает рефакторинг C# на потом.

К третьему месяцу разработки я открыл файл и увидел: 1300+ строк. И понял — так дальше нельзя. Это уже не оптимизация WPF приложения, это уже борьба за выживание. Пришлось провести декомпозицию класса, иначе код превращался в нечитаемую простыню.

Знакомое чувство? Вот так выглядит God Object изнутри.
Знакомое чувство? Вот так выглядит God Object изнутри.

Partial-класс: спасение или временная заплатка?

Надо было что-то делать. И тут пригодился partial. Частичный класс позволял разнести код по файлам аккуратно и без магии.

Partial class в C# позволяет разбить один класс на несколько файлов. Для компилятора это всё тот же MainViewModel, ничего не меняется. А для разработчика появляются аккуратные разделы, каждый со своим назначением. Это первый шаг к тому, чтобы провести грамотную декомпозицию класса, не переписывая всё с нуля. Удобно, когда вся бизнес-логика живёт во вьюмоделях, а ViewModel похожа на разросшийся органайзер: разложил по полкам — и дышать легче.

И тогда я вспомнил, как в детстве складывал конструктор: сначала всё в одну кучу, а потом по коробочкам. MainViewModel — это тоже отдельная коробочка, просто большая. И если её разобрать на части, станет проще. Пример partial class c# для такого случая — как раз то, что надо: минимум изменений, максимум порядка.

Объявление класса остаётся одно:

public partial class MainViewModel : INotifyPropertyChanged { // ... }

А дальше в каждом файле своя часть. Я разбил MainViewModel на несколько файлов:

• MainViewModel.cs — ядро, конструктор, общие свойства, команды.

• MainViewModel.Auth.cs — вход и выход, IsAuthenticated, смена пароля.

• MainViewModel.Clients.cs — загрузка, сохранение, удаление, корзина.

• MainViewModel.Users.cs — управление юзерами и ролями.

• MainViewModel.Tasks.cs — задачи.

• MainViewModel.Chat.cs — чат.

• MainViewModel.Export.cs — экспорт в Excel, Word, PDF.

• MainViewModel.Commands.cs — команды.

• MainViewModel.Visibility.cs — видимость вкладок и панелей.

Казалось бы, просто перенёс код из одного файла в другие. Что тут такого? А эффект — как будто разобрал захламлённый шкаф. Любо дорого смотреть. Если нужно разобраться с чатом, я открываю Chat.cs. Если проблема с экспортом, открываю Export.cs. Не нужно листать тысячи строк в поисках одного метода. Это и есть спокойный рефакторинг кода, который не ломает логику, а наводит порядок.

Девять файлов вместо одного. Порядок, а не хаос.
Девять файлов вместо одного. Порядок, а не хаос.

Где конкретно встрял

Ожидал проблем, но не думал, что настолько.

Первая: partial не лечит архитектурные проблемы. Если у тебя God Object, который делает всё, то разбиение на файлы не поможет. Это как разложить бардак по разным коробкам. Бардак меньше не стал, просто теперь он в девяти коробках.

Вторая: навигация между файлами. Когда у тебя 9 файлов partial-класса, ты начинаешь забывать, в каком файле что лежит. Пришлось повозиться с именованием и группировкой. Зато теперь, когда нужно найти что-то связанное с авторизацией, я точно знаю — открываю Auth.cs.

Третья, самая подлая: конструктор и инициализация. Если конструктор разбросан по нескольким файлам, можно легко запутаться в порядке инициализации. Пришлось сделать один главный конструктор в MainViewModel.cs, а инициализацию команд вынести в Commands.cs. Это как с электрикой: один щиток, один ввод, а дальше — по комнатам.

Четвёртая: отладка. Когда breakpoint срабатывает в MainViewModel.Chat.cs, ты сначала не понимаешь, откуда ты сюда попал. Пришлось повозиться с трассировкой вызовов и окном Call Stack в Visual Studio. Зато теперь, когда вижу стек вызовов, сразу понимаю, откуда ветер дует. Возиться с большим ViewModel в отладке — отдельное удовольствие, но его того стоило разрулить.

Отладка God Object — это как блуждать в лабиринте без карты.
Отладка God Object — это как блуждать в лабиринте без карты.

Настоящая декомпозиция: уходим от God Object

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

Я начал выносить логику в сервисы:

• ClientService — работа с контактами (загрузка, сохранение, удаление).

• UserService — управление юзерами и ролями.

• ChatService — логика чата.

• ExportService — экспорт данных.

MainViewModel стал тоньше. Он больше не знает, как сохранить контакт в базу. Он просто вызывает ClientService.Save(client). Вот он, настоящий MVVM WPF — ViewModel координирует, а не тащит всё на себе. Это и есть правильная декомпозиция класса, когда каждый отвечает за своё. Рефакторинг кода ушёл глубже: из косметики он превратился в нормальную архитектуру приложения.

Но первым делом partial решил главную проблему большого ViewModel. Я снова начал понимать, где что лежит. А это, как показывает практика, половина дела, если не больше. Иногда самый правильный рефакторинг — не переписать всё, а сначала навести порядок.

Тонкий ViewModel, толстые сервисы. Так и должно быть.
Тонкий ViewModel, толстые сервисы. Так и должно быть.

Итог: кому это нужно, а кому нет

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

Кому подойдёт такой подход:

– если у вас большой ViewModel (500+ строк) и вы не знаете, с чего начать рефакторинг C#;

– если вы хотите быстро улучшить читаемость кода без полной переработки архитектуры;

– если вы работаете в команде и нужно разделить ответственность между разработчиками.

Кому не подойдёт:

– если у вас маленький проект (до 300 строк) — partial только усложнит жизнь;

– если вы готовы сразу делать правильную архитектуру десктопных приложений с сервисами и Dependency Injection — не тратьте время на partial, делайте сразу правильно.

Частые вопросы

Что такое partial class в C#?

Это возможность разбить объявление одного класса на несколько файлов. Компилятор собирает их в одну цельную сущность. Удобно для больших ViewModel или сгенерированного кода. Это базовый приём в рефакторинге кода для WPF.

Не лучше ли сразу использовать сервисы?

Если есть время и опыт — да, лучше сразу выносить логику в сервисы (через Dependency Injection). Но если проект уже большой и горит, а читаемость нужна вчера — partial поможет выиграть время.

Как понять, что ViewModel стал слишком большим?

Три верных приметы: не можете найти нужный метод за 10 секунд; файл разросся больше 500 строк; в одном классе больше 20 команд. Любая из них — повод задуматься о декомпозиции класса.

А что делать после разбиения на partial-файлы?

Следующий шаг — выносить бизнес-логику в отдельные сервисы, создавать отдельные ViewModel для сложных экранов и использовать DI-контейнер. По сути, это переход от косметического рефакторинга к настоящей архитектуре.

А дальше меня, как обычно, понесло. Раз уж мы разобрались с архитектурой и вынесли логику в сервисы, почему бы не оптимизировать работу с базой данных? Но это уже следующая история. Всё началось с обзора моей CRM на WPF, где я рассказывал про первые шаги. А про то, зачем в CRM вообще нужен чат, и как он добавил ещё пару сотен строк в главный файл, — отдельная история.

А вы сталкивались с разросшимся ViewModel? Как выходили — partial, сервисы или сразу переписывали с нуля? Расскажите в комментариях.

📚 Читайте также в цикле DevCrm (CRM на WPF)

📚 Смежные циклы: нейросети и ИИ-агенты