Хороший код или плохой? Лично для меня хороший код обладает следующими качествами:
- Код легко понятен разработчикам разной квалификации и хорошо структурирован
- Код легко изменять и поддерживать
- Приложение выполняет свои функции и обладает достаточной, для выполняемого круга задач, отказоустойчивостью
Несмотря на короткое описание, о том, как добиться выполнения трех этих условий, написано много толстых книг.
Почему именно эти критерии? Сразу оговорюсь, речь сейчас идет о разработке ПО для бизнеса (enterprise application). Критерии оценки кода для систем реального времени, самолетов, систем жизнеобеспечения и МКС отличаются.
Наверняка всем или почти всем знаком этот треугольник.
Одновременно из трех условий — быстро, качественно, дешево — можно выполнить только два.
При разработке бизнес-приложений наша цель – разработать достаточно хорошее ПО за приемлемый срок, уложившись в бюджет. На практике это значит, что в нашем приложении могут быть ошибки, но в не критичных для системы местах. Давайте рассмотрим несколько примеров, чтобы лучше понять, что это значит.
Кейс №1
На форме есть поле ввода текста (textarea). Можно ввести туда 100500 символов и наше приложение свалится с исключением «превышена максимальная длина запроса».
Является ли это нашим кейсом? Едва ли. Если это публичная часть, стоит поставить Java-script валидатор и не давать постить такие формы. Настраивать как-то дополнительно сервер для тех, кто отключил java-script – просто трата времени, а значит – бюджета. Мы не будем этого делать. Да, в нашем приложении есть потенциальная ошибка. Нужно ли кому-то, чтобы ее исправили? Нет.
Кейс №2
От клиента приходит запрос: нам нужна страница партнеров. На этой странице будет краткая информация о каждом из них и ссылка на страницу, на которой мы должны встроить в iframe страницу с сайта партнера.
Интересный кейс. Наверное, нам нужно внести изменение в БД, добавить сущность партнер, db-entity by design мы не передаем во View, так что нужна еще DTO, функция маппинга из db-entity в dto, еще миграция БД для авто-деплоя. Хм, еще нужна страница в админке, чтобы добавить партнера. Не забыть о роутинге для контроллера, чтобы было /Partner/<PARTNER_NAME> Довольно много работенки…
Верно для 10.000 партнеров. Давайте поинтересуемся: «сколько партнеров будет»? Если ответ «в этом году три». Нам стоит пересмотреть свои взгляды. Это опять перерасход времени и бюджета. Всего 3 партнера?
namespace Habrahabr.Models.Partner
{
public class PartnerViewModel
{
public string Name { get; set; }
public string WebSiteUrl { get; set; }
public string ShortDescription { get; set; }
public string FullDescription { get; set; }
}
}
using System.Web.Mvc;
using Habrahabr.Models.Partner;
namespace Habrahabr.Controllers
{
public class PartnerController : Controller
{
private static PartnerViewModel[] partners = new[]
{
new PartnerViewModel()
{
Name = "Coca-Cola",
WebSiteUrl = "http://coca-cola.com/partner",
ShortDescription = "Coca-cola short description",
FullDescription = "Coca-cola full description"
},
new PartnerViewModel()
{
Name = "Ikea",
WebSiteUrl = "http://ikea.com/partner",
ShortDescription = "Ikea short description",
FullDescription = "Ikea full description"
},
new PartnerViewModel()
{
Name = "Yandex",
WebSiteUrl = "http://yandex.ru/partner",
ShortDescription = "Yandex short description",
FullDescription = "Yandex full description"
}
};
public ActionResult Index()
{
// TODO: populate partners from database if a lot of partner required
return View(partners);
}
// TODO: refactor this if a lot of partner required
[ActionName("Coca-Cola")]
public ActionResult CocaCola()
{
return View("_PartnerDetail", partners[0]);
}
public ActionResult Ikea()
{
return View("_PartnerDetail", partners[1]);
}
public ActionResult Yandex()
{
return View("_PartnerDetail", partners[2]);
}
}
}
@model IEnumerable<Habrahabr.Models.Partner.PartnerViewModel>
<ul>
@foreach (var p in @Model)
{
<li>
<h3>@Html.ActionLink(p.Name, p.Name, "Partner")</h3>
<div>@p.ShortDescription</div>
<p class="info">@Html.ActionLink("Partner page", p.Name, "Partner")</p>
</li>
}
</ul>
@model Habrahabr.Models.Partner.PartnerViewModel
<h2>@Model.Name</h2>
<div>@Model.FullDescription</div>
@if (!string.IsNullOrEmpty(Model.WebSiteUrl))
{
<iframe width="100%" height="500" src="@Model.WebSiteUrl"></iframe>
}
На все про все – максимум час времени с учетом ввода текстов. Мы разработали достаточно хорошую страницу партнеров. Отвечает ли он нашим критериям:
- Понятен и структурирован – да
- Легко изменяем – да
- Достаточно надежен – да
Предметная область, доступ к данным и архитектура приложения
Фаулер правильно заметил в своей книге Enterprise Patterns of Enterprise Application Architecture, что термин «архитектура» невероятно размыт:
Термин «архитектура» пытаются трактовать все, кому не лень, и всяк на свой лад. Впрочем, можно назвать два общих варианта. Первый связан с разделением системы на наиболее крупные составные части; во втором случае имеются в виду некие конструктивные решения, которые после их принятия с трудом поддаются изменению. Также растет понимание того, что существует более одного способа описания архитектуры и степень важности каждого из них меняется в продолжение жизненного цикла системы.
У выше описанного подхода с партнерами есть одно существенное ограничение. Нельзя постоянно писать код приложения в таком стиле. Рано или поздно система вырастет. В данном случае мы «зашили» логику приложения. Еще пара таких контроллеров и понять работу системы будет довольно сложно, особенно новым разработчикам.
В момент, когда выяснится, что партнеров будет не 3, а 100500 и показывать надо не всех, а только тех, кто заплатил в ночь с пятницы на субботу (с 13 на 14ое) и принес в жертву 6 девственниц, а страницы надо ротировать в соответствие с положением Венеры относительно Юпитера (в реальной жизни кейсы, конечно, другие, но не зря говорят, что для разработчика нет ничего более нелогичного, чем «бизнес-логика», состоящая сплошь из исключений и частных случаев). Нужно задуматься об архитектуре в обоих смыслах слова.
Мы заблаговременно оставили «швы» в приложении, начнем распарывать их и пришивать необходимые куски. Мы воспользуемся ORM, переименуем PartnerViewModel в Partner и замапим класс партнера в БД. Я никогда не использовал Database-First и не воспринимал всерьёз Entity Framework, пока не вышла версия с Code-First подходом. В данном случае, я не вижу необходимости мапить Partner в PartnerViewModel. Да, семантически эти сущности выполняют разные задачи и в общем случае PartnerViewModel может отличаться, но пока нет ни единой причины писать абсолютно такой-же класс. Мапить ViewModel имеет смысл, когда без этого нельзя обойтись, не потащив логику в представление. Обычно необходимость во ViewModel’ях возникает, если в интерфейсе требуется показать сложную форму, оперирующую сразу несколькими сущностями предметной области, или есть необходимость воспользоваться атрибутами из сборки System.Web для свойств модели. В последнем случае, я склонен тащить за собой эту зависимость в сборку с предметной областью, если я уверен, что в ближайшие полгода в приложении не появится новой точки входа, кроме основного веб-приложения.
В одном проекте, в котором я участвовал, для доступа к данным необходимо было вызвать сервис, который обращался к фасаду, который в свою очередь обращался к DAL-слою, который вызывал Entity Framework. Фасад и DAL находились в разных сборках, а EF использовал подход Database-First. Всю логику обычно выполнял DAL, а остальные слои просто перемапливали результаты в 90% случаев в абсолютно идентичные DTO. При этом никаких других клиентов у системы не было и сервис был объективно не нужен. Когда я спросил «зачем такой оверхед», разработчик, который дольше работал на проекте сказал: «Не знаю, наш другой проект так написан, решили сделать по аналогии». Да, этот «другой проект» состоял из 20 приложений и только на то, чтобы развернуть его ушло 2 дня. У API этого приложения много клиентов. Такая сложная структура была необходимостью. В нашем случае это была стрельба из пушки по воробьям.
Но вернемся к нашим
Хороший вариант воспользоваться паттерном
IOC/DI
С ростом приложения возникает опасность сделать его слишком сильно-связанным, а это сделает систему неповоротливой и будет мешать изменению кода.
Не буду в очередной раз описывать, что такое IOC/DI и с чем его едят, просто нужно взять за правило:
- Не создавать зависимости явно
- Использовать IOC-контейнеры
При этом не обязательно создавать интерфейс на каждый класс в проекте. Зачастую интерфейс бывает проще выделить в дальнейшем. R# прекрасно справляется с этой задачей. Но держите это в голове, когда будете писать класс. Мыслите его интерфейсом, а не конкретной реализацией. Используя DI вы сможете гибко управлять реализациями. В случае необходимости, вы сможете выкинуть одну реализацию и заменить ее другой, например, если по каким-то причинам база данных перестанет вас устраивать, вы сможете прозрачно заменить ее, заменив реализацию репозитория. Кроме этого, используя IOC/DI гораздо проще писать тестируемый код.
Тесты
Я не фанат 90-100% покрытия проекта. Я думаю, что чаще всего, это замедляет разработку и делает поддержку кода гораздо более муторным занятием. Я предпочитаю покрывать тестами поведение системы, т.е. основные бизнес-правила приложения. Таким образом, тесты не только страхуют от ошибок, но и помогают новым разработчикам быстрее понять логику работы системы. Такие тесты работают лучше, чем любая документация. Особенно эффективны тесты, использующие BDD – нотацию.
Дальнейшее развитие системы
Если приложение продолжает расти, сложно дать конкретные советы: слишком много факторов, которые необходимо учесть. Слишком многое зависит от специфики проекта. Хорошим решением может быть SOA. Вообще декомпозиция любой большой системы – отличная идея. Десять проектов загружено у вас в IDE или сто — разница очень большая. При разработке софта такого масштаба не обойтись без deploy-скриптов, инсталляторов и релиз-менеджмента, но это уже совсем другая история…
Лямбды, аспекты, функциональщина и прочий холивор
Напоследок решил оставить заведомо спорные моменты. Я заметил, что многие разработчики склонны «привыкать» к стеку, с которым они работают. Если программист «привык» к своему стеку, он склонен, тащить его в каждый новый проект, независимо от целесообразности. Так, я неоднократно слышал критику C# за «заигрывания» с функциональным стилем, лямбды, которые компилируются в «черт-знает что» и «не отлаживаются» и за прочие extension-методы. Объективно, мы видим, что в php добавили замыкания, в java8 появится Stream для потоковой обработки коллекций, значит многим разработчикам это по душе.
Не стоит бояться комбинировать парадигмы, если оно того стоит. Задайте себе вопросы «на сколько платформа/язык/ос подходят для выполнения задачи?» и «какие технологии помогут мне выполнить задачу максимально эффективно?». Лучше всего мою точку зрения иллюстрирует цитата iliakan:
Я так прикинул, что будет быстрее выучить Erlang или заставить Java работать достаточно быстро и решил выучить Erlang