Проектирование кода

Я практик и всегда писал с точки зрения практики. В журнале было опубликовано уже тонна моих примеров, сетевых и прикольных, были и книги. При написании маленьких примеров стараешься писать код как можно проще и понятнее, что не допустимо при написании даже средних проектов, не говоря уже о более крупных. Сегодня я решил повернуться к теории передом, а к практике задом. И не просите, наклоняться не буду, буду рассказывать.

Проектирование кода

Проблема учебной литературы по программированию, которая дает базовые знания в том, что там примеры всегда просты. Авторы (и я в том числе) нарушают правила проектирования ради наглядности и простоты. Ну а читатели, научившись писать простые телефонные справочники и лепить весь код в один класс главной формы, даже не пытаются читать книги по проектированию, а сразу идут создавать программы. Это громадная ошибка.

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

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

Модель, представление, контроллер

Мне удалось поработать в нескольких компаниях в России, и я заметил, что подход MVC (Model View controller) некоторые вообще не применяют, а некоторые применяют, но неправильно. Я видел только Web проекты, где программисты хоть как-то пытались разделить логику от представления, а вот в Delphi проектах ничего подобного вообще не встречал. Может быть, мне не везло, а может я не так смотрел, в общем, я решил поговорить именно на эту тему.

Скачайте любой из моих проектов с сайта www.cydsoft.com и установите себе на компьютер. Теперь загляните в папку с установленной программой. Обратите внимание, что там помимо исполняемого файла будет несколько библиотек. В самом исполняемом файле будет находиться только основное окно программы, основные диалоговые окна и обработчики событий для действий в окнах. Никакой логики в исполняемом файле нет. Весь код реализующий логику я всегда выношу в отдельные модули или даже библиотеки. Если проект намечается большой, то логика будет в библиотеках, а если проект маленький, то просто в отдельных модулях в отдельной папке.

Что это мне дает. Возьмем, например, программы CyD Network Utilities и CyD Web Development Tools. У этих программ есть схожие модули. Например, обе программы умеют тестировать сайт в поисках уязвимостей SQL Injection, XSS или других ошибок и обе программы умеют анализировать Web сайты по разным параметрам. Но визуальный интерфейс в программах реализован по-разному, а логика используется одна и та же.

Визуальный интерфейс CyD Network Utilities выполнен с использованием WinForms, а у CyD Web Development Tools он сделан с помощью WPF. Но при этом я не писал логику дважды. Библиотека кода, в которой реализованы все алгоритмы проверок и тестирования находятся в отдельных библиотеках. Они отделены от представления. Таким образом, я создаю визуальный интерфейс, и создаю библиотеку с логикой. В обработчиках событий я пишу только вызовы библиотеки движка и отображаю результаты работы движка, но не более.

Структура MVC

В C# при создании новой формы, среда разработки создает три файла для каждой формы, form.cs, form.resx и form.designer.cs. Файл resx содержит ресурсы, и там кода нет, хотя его можно отнести к уровню представления. Файл designer.cs тоже нужно относить к уровню представления, а вот form.cs это должен быть контроллер.

В Delphi формы состоят из двух файлов .dfm и .pas (по крайней мере когда-то состояли, а что сейчас я даже и не знаю). Файл .dfm – это представление, а соответствующий ему .pas файл – это контроллер. Никакого кода, выполняющего логику программы в контроллере не должно быть. Там должны быть обработчики событий, которые могут вызывать методы движка и код отображения данных на поверхности представления.

А где же модель для Delphi? Можно создать отдельную папку и в нее складывать классы, которые будут обрабатывать всю логику приложения. В этом случае вы абстрагируетесь от визуальных форм, правда все останется в одном исполняемом файле. Создавать DLL в Delphi это издевательство над программистами, не то, что в .NET, но хотя бы отделение поможет вам в будущем в сопровождении кода.

Что касается Web проектов на ASP.NET, то даже в голой реализации (я имею ввиду без Microsoft MVC) необходимо абстрагироваться и разбивать все на составляющие блоки. Файлы aspx будут представлением, файлы aspx.cs – контроллерами, а уровень модели нужно убирать в отдельные модули. У одной питерской конторы я видел решение, в котором добавили еще один уровень контроллера. Они наверно посчитали, что aspx.cs – это тоже представление и получилось, что у них было две библиотеки Controller.dll и Model.dll. Только в 99% случаев функции контроллера Controller.dll были просто посредниками и вызывали одноименные функции модели.

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

Движок программы может быть выполнен как в отдельных библиотеках, так и просто в отдельном модуле внутри исполняемого файла. Но логика должна быть максимально отделена от представления, тогда вы получаете максимальную гибкость, и не будете зависеть от того, что отображает данные. Будь то WinForms, WPF или даже ASP.NET. Один и тот же код может быть использован в разных проектах без дополнительной модификации.

Кстати, просто для примера можно скачать проект с открытым исходным кодом с Digital Photo Workshop. В нем я не использую отдельных библиотек (пока), а просто разместил весь код логики в отдельную папку Engine. Такое имеет право на жизнь для небольших проектов, в которых я точно уверен, что их код не будет разделяться между различными проектами.

Классы

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

Даже сложные устройства состоят из составляющих, которые выполняют строго определенную задачу. Вот возьмем, например, автомобиль. Сложное устройство, которое состоит из узлов, каждое из которых создано для решения четкой задачи. Я думаю, что если бы инженеры захотели, то они смогли бы засунуть коробку передач прямо в двигатель, но сопровождение такого автомобиля может быть ужасным. Если что-то сломается в коробке, придется разбирать весь этот громадный агрегат, который включает в себя как элементы двигателя, так и трансмиссии.

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

Зависимости классов

Очень сложно в двух словах описать все трюки проектирования, потому что случаи бывают разные, но шаблоны все же существуют. Когда строите код, дробите все на отдельные классы по логике. И чтобы проверить, правильно ли вы разбили и описали класс, обращайте внимание на зависимость одного класса от другого. Если класс А зависит от класса Б минимально и класса Б легко заменить классом В, потому что связи идет через пару свойств или методов, то такая структура кода отличная.

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

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

  1. Класс представления, который будет создавать окно, и отображать данные;
  2. Класс движка игры, который будет создавать новый уровень, управлять блоками;
  3. Класс блока, который будет хранить состояние блока, тип и должен содержать методы для поворота блока.

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

А что если перенести отображение данных на уровень движка. На уровне представления будет только один вызов метода:

Engine.Draw(контекст для рисования);

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

Не всегда функцию рисования нужно переносить в движок. Иногда это должно быть реализовано за пределами движка. Действовать нужно по ситуации, но проектировать код нужно так, чтобы замена одного класса давалась с минимальными затратами. Замена представления не должна требовать изменений или переноса логики.

Итог

Я не могу сказать, что я эксперт в проектировании и только мои советы самые лучшие. Я только делюсь тем, что накопил в загажнике за долгие годы создания программ. У каждого могут быть свои представления об идеальном коде, и все приходит с практикой. Если же вам понравилось, я готов продолжить иногда свои советы и не только по проектированию классов.

На последок дам свой любимый совет, который даю всем на своем блоге – никогда не делайте ничего тупо, потому что это тупо. Анализируйте и ищите золотую середину.

Еще пример. Допустим ты везде использовал компонент TTCPConnection. Я бы вообще такие компоненты запретил как зло, но допустим. Никогда не ставь его на форму. Создай класс MyMegaNetworkEngine и помести в него создание компонента и вызов методов, которые тебе нужны.

Теперь если через год появится TTCPConnectio2, в котором новые методы и новые функции, то тебе достаточно просто изменить свой класс MyMegaNetworkEngine и абсолютно все формы заработают без проблем на новом движке. не нужно прыгать по формам и исправлять их одна за одной. Или же ты можешь без проблем перейти на совершенно другой компонент TTCPSuperConnection от другого производителя не переделывая свои визуальные формы.

Я недолгое время работал в компании, где использовали BDE и использовали его прямо на формах. Они не могут перейти на что-то более современное, потому что код спроектирован не верно. Если бы у них была библиотека типа OurMegaDBEngine, в которой были бы все функции работы с базой, то для перехода на другую технологию, базу данных или просто для оптимизации достаточно было бы подправить OurMegaDBEngine, и не нужно было бы переделывать сотни форм в нескольких заменяя BDE на DBExpress или ADO.

В той компании пытались написать прослойку (я учавствовал в ее написании), которая бы выглядела как BDE, но использовала бы ADO. То же решение, но это громадный косталь. Интересно запустили ли ее. Самое удивительное, что в компании работает очень много профессиональных ребят, отлично знающих Delphi и программирование. Некоторые знают Delphi лучше меня. Они сделали достаточно отличный продукт и много в нем сделано отлично. Но маленькая ошибка в проектировании заставила их застрять в 20-м веке.



Внимание!!! Если ты копируешь эту статью себе на сайт, то оставляй ссылку непосредственно на эту страницу. Спасибо за понимание

Комментарии

Паника, что-то случилось!!! Ничего не найдено в комментариях. Срочно нужно что-то добавить, чтобы это место не оставалось пустым.

Добавить Комментарий

О блоге

Программист, автор нескольких книг серии глазами хакера и просто блогер. Интересуюсь безопасностью, хотя хакером себя не считаю

Обратная связь

Без проблем вступаю в неразборчивые разговоры по e-mail. Стараюсь отвечать на письма всех читателей вне зависимости от страны проживания, вероисповидания на русском или английском языке.

Пишите мне


Я в социальных сетях
Facebook Telegram Програмысли
Youtube Instagram Твитер