Рефакторинг в C# — наше всё: как MainViewModel в WPF стал 1300+ строк и как пришлось убираться (DevCrm 08)!
В очередной раз запускаю проверить, как всё работает, открываю MainViewModel.cs, и тут до меня доходит: тысяча триста с лишним строк. И это уже не смешно.
Открываю его, а там всё намешано. Авторизация, контакты, юзеры, задачи, чат, экспорт. Команды, свойства, фильтры, видимость элементов. Всё в одной куче, и понять, где что начинается и где заканчивается, можно только если ты сам это писал неделю назад. А если вернуться через месяц?
Знаете это чувство, когда открываешь ящик стола, а там всё свалено в одну кучу: от скрепок до старых чеков? Вещи-то все нужные, но найти что-то конкретное — целое приключение. Вот с MainViewModel было так же. Вроде бы «просто главный файл», а по факту — отдельный мир со своей логикой, в котором можно заблудиться.
Я даже помню, как выглядел этот файл. Одних команд было двадцать шесть штук.
И это только команды. Свойства, поля, методы загрузки, сохранения, удаления, экспорта, бэкапа. Каждое свойство с вызовом OnPropertyChanged. Каждый метод обёрнут в try-catch с логированием. Когда количество строк перевалило за тысячу триста, навигация по файлу превратилась в квест. Это была та самая минута, когда рефакторинг кода из приятной теории становится вопросом выживания.
Чтобы добраться от команды до метода, который она вызывает, нужно было прокрутить пол-экрана. Пока прокрутишь, уже забываешь, зачем вообще сюда пришёл. Откладывать рефакторинг кода дальше было просто опасно: такой объём в одном месте не выдержит ни один разработчик.
Как я дошёл до жизни такой
Всё началось с того, что я делал свою CRM на WPF. Программу для учёта контактов — многие зовут её срм или црм, суть одна. Хотелось получить локальную crm, которая живёт прямо на компьютере, офлайн, без подписок и интернета в придачу. Дёшево и без лишнего шума: типичная срм система для личного использования, только моя и только под меня. Вся бизнес-логика жила во вьюмоделях, и сначала это казалось удобным.
Сначала MainViewModel был маленьким и пушистым. Команды для контактов, пара свойств, базовая логика. Всё работало, всё было понятно. Но потом добавились юзеры. Потом задачи. Потом чат. Потом экспорт в Excel, Word, PDF. Потом видимость вкладок. Потом фильтры. С каждым пунктом большой ViewModel рос на глазах.
Каждая новая фича добавляла 50–100 строк. И всё в один файл. Потому что «ну это же просто команда, она же связана с главным окном, пусть тут и живёт». Классическая ловушка, в которую попадает любой, кто откладывает рефакторинг C# на потом.
К третьему месяцу разработки я открыл файл и увидел: 1300+ строк. И понял — так дальше нельзя. Это уже не оптимизация WPF приложения, это уже борьба за выживание. Пришлось провести декомпозицию класса, иначе код превращался в нечитаемую простыню.
Partial-класс: спасение или временная заплатка?
Надо было что-то делать. И тут пригодился partial. Частичный класс позволял разнести код по файлам аккуратно и без магии.
Partial class в C# позволяет разбить один класс на несколько файлов. Для компилятора это всё тот же MainViewModel, ничего не меняется. А для разработчика появляются аккуратные разделы, каждый со своим назначением. Это первый шаг к тому, чтобы провести грамотную декомпозицию класса, не переписывая всё с нуля. Удобно, когда вся бизнес-логика живёт во вьюмоделях, а ViewModel похожа на разросшийся органайзер: разложил по полкам — и дышать легче.
И тогда я вспомнил, как в детстве складывал конструктор: сначала всё в одну кучу, а потом по коробочкам. MainViewModel — это тоже отдельная коробочка, просто большая. И если её разобрать на части, станет проще. Пример partial class c# для такого случая — как раз то, что надо: минимум изменений, максимум порядка.
Объявление класса остаётся одно:
А дальше в каждом файле своя часть. Я разбил 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
Эту работу я делал уже потом, постепенно. И она оказалась куда важнее, чем просто раскладывание по коробочкам.
Я начал выносить логику в сервисы:
• ClientService — работа с контактами (загрузка, сохранение, удаление).
• UserService — управление юзерами и ролями.
• ChatService — логика чата.
• ExportService — экспорт данных.
MainViewModel стал тоньше. Он больше не знает, как сохранить контакт в базу. Он просто вызывает ClientService.Save(client). Вот он, настоящий MVVM WPF — ViewModel координирует, а не тащит всё на себе. Это и есть правильная декомпозиция класса, когда каждый отвечает за своё. Рефакторинг кода ушёл глубже: из косметики он превратился в нормальную архитектуру приложения.
Но первым делом partial решил главную проблему большого 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)
📚 Смежные циклы: нейросети и ИИ-агенты