Статьи

Давайте изобретем больше колес

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

Многие разработчики сосредоточены на том, какие модные рамки они изучат дальше. Или какой язык программирования может решить все их проблемы. Прежде чем мы перейдем к инструментам, полезно узнать, как они действительно работают. Мой девиз: «Я не буду использовать фреймворк, который не смог бы воссоздать». Конечно, слишком много времени требуется для создания такой же полной структуры, как многие из доступных, но я, по крайней мере, смогу решить все обычные случаи самостоятельно.

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

Пример, который мне очень понравился, — это создание веб-приложения на Java без фреймворка. Я могу использовать простой домен, такой как создание адресной книги, где вы можете зарегистрировать свои контакты и искать их. В честь моих друзей по C # я решил ту же задачу в C #: как создать веб-приложение без MVC, без ASP.NET или даже без IIS.

Разработка, основанная на тестировании, является для меня важным инструментом для ясного мышления. Я сделал исключение из правила калькулятора выше и использовал несколько тестовых библиотек:  SimpleBrowser.WebDriverFluentAssertions  и  NUnit . Этот тест демонстрирует поведение, которое я хочу от приложения, когда я закончу:

     

[Test]
public void ShouldFindSavedPerson()
{
// Start a web server INSIDE THE TEST 😀
var server = new My.Application.WebServer();
server.Start();
 
var browser = new SimpleBrowser.WebDriver.SimpleBrowserDriver();
browser.Url = server.BaseUrl;
 
// Navigate to the "add contact" page
browser.FindElement(By.LinkText("Add contact")).Click();
 
// Add a new contact
browser.FindElement(By.Name("fullName")).SendKeys("Darth Vader");
browser.FindElement(By.Name("address")).SendKeys("Death Star");
browser.FindElement(By.Name("saveContact")).Submit();
 
// Navigate to the "find contact" page
browser.FindElement(By.LinkText("Find contact")).Click();
 
// Execute some queries:
browser.FindElement(By.Name("nameQuery")).SendKeys("vader");
browser.FindElement(By.Name("nameQuery")).Submit();
browser.FindElement(By.CssSelector("#contacts li")).Text
.Should().Be("Darth Vader (Death Star)");
browser.FindElement(By.Name("nameQuery")).SendKeys("anakin");
browser.FindElement(By.Name("nameQuery")).Submit();
browser.FindElements(By.CssSelector("#contacts li"))
.Should().BeEmpty();
}

Я добавляю пустой класс для My.Application.WebServer, и тест завершается ошибкой в ​​строке browser.Url = server.BaseUrl, поскольку там нет реального сервера.

Для реализации сервера я использую симпатичный небольшой класс, который является частью базовой библиотеки .NET:  System.Net.HttpListener . Вот основы:

     

class WebServer
{
public void Start()
{
var listener = new System.Net.HttpListener();
listener.Prefixes.Add(BaseUrl);
listener.Start();
new Thread(HttpThread).Start(listener);
}
 
private void HttpThread(object listenerObj)
{
HttpListener listener = (HttpListener)listenerObj;
while (true)
{
var context = listener.GetContext();
using (context.Response)
{
}
}
}
}

Запустив тест снова, я получаю еще один шаг вперед. На этот раз мне сказали, что тест не может найти ссылку «Добавить контакт». Не большой сюрприз, так как я не обслуживаю HTML! Небольшое изменение в коде WebServer исправит это:

     

 var context = listener.GetContext();
using (context.Response)
{
new AddressBookController().Service(context);
}

Тогда нам просто нужно создать простую реализацию для AddressBookController.Service:

     

class AddressBookController
{
internal void Service(HttpListenerContext context)
{
var html = "<html>" +
"<p><a href='/contact/create'>Add contact</a></p>" +
"<p><a href='/contact/'>Find contact</a></p>" +
"</html>";
var buffer = Encoding.UTF8.GetBytes(html);
context.Response.OutputStream.Write(buffer, 0, buffer.Length);
}
}

Опять же, тест продвинется на один шаг вперед. Теперь мы видим, что главная страница представлена ​​ссылками «Добавить контакт» и «Найти контакт». После нажатия кнопки «Добавить контакт» тест, конечно, не сможет найти поле fullName, поскольку мы еще не создали форму. Метод HandleGetRequest проверяет URL, чтобы определить, какая страница должна отображаться:

     

internal void Service(HttpListenerContext context)
{
var html = HandleGetRequest(context.Request);
var buffer = Encoding.UTF8.GetBytes(html);
context.Response.OutputStream.Write(buffer, 0, buffer.Length);
}
 
private string HandleGetRequest(HttpListenerRequest request)
{
if (request.Url.LocalPath == "/contact/create")
{
return "<html>" +
"<form method='post' action='/contact/create'>" +
"<p><input type='text' name='fullName'/></p>" +
"<p><input type='text' name='address'/></p>" +
"<p><input type='submit' name='saveContact' value='Save'/></p>" +
"</form>" +
"</html>";
}
else
{
// As before
}
}

Мы почти закончили сохранение контактов. Тест не может найти ссылку «Найти контакт» после отправки формы. Метод methodService должен быть модифицирован для обработки запросов POST и выполнения перенаправления обратно в меню:

     

internal void Service(HttpListenerContext context)
{
if (context.Request.HttpMethod == "GET")
{
var html = HandleGetRequest(context.Request);
var buffer = Encoding.UTF8.GetBytes(html);
context.Response.OutputStream.Write(buffer, 0, buffer.Length);
}
else
{
context.Response.Redirect(context.Request.Url.GetLeftPart(UriPartial.Authority));
}
}

Нам все еще не хватает формы для поиска контактов. Мы получим некоторую помощь от шаблона копирования / вставки:

     

private string HandleGetRequest(HttpListenerRequest request)
{
if (request.Url.LocalPath == "/contact/create") ...
else if (request.Url.LocalPath == "/contact/")
{
return "<html>" +
"<form method='get' action='/contact/'>" +
"<p><input type='text' name='nameQuery'/></p>" +
"<p><input type='submit' value='Find'/></p>" +
"</form>" +
"</html>";
}
else ...
}

Следующая ошибка очевидна — нам нужно включить контакты в ответ:

     

 class Contact
{
public string FullName { get; set; }
public string Address { get; set; }
}
 
private static List<Contact> contacts = new List<Contact>();
 
private string HandleGetRequest(HttpListenerRequest request)
{
else if (request.Url.LocalPath == "/contact/")
{
var contactsHtml = string.Join("",
contacts.Select(c => "<li>" + c.FullName + " (" + c.Address + ")</li>")))
return string.Format("<html>" + ...
"<ul id='contacts'>{0}</ul>" +
"</html>", contactsHtml);
 
}

Единственное, чего не хватает, это хранить контакты, когда мы публикуем форму «Добавить контакт»:

     

 internal void Service(HttpListenerContext context)
{
if (context.Request.HttpMethod == "GET") ...
else
{
// Read the parameters from the POST body (Request.InputStream)
var request = context.Request;
var encoding = context.Request.ContentEncoding;
var reader = new StreamReader(context.Request.InputStream, encoding);
var parameters = HttpUtility.ParseQueryString(reader.ReadToEnd(), encoding);
 
context.Response.Redirect(HandlePostRequest(request, parameters));
}
}
 
private string HandlePostRequest(HttpListenerRequest request, NameValueCollection parameters)
{
contacts.Add(new Contact() { FullName = parameters["fullName"], Address = parameters["address"] });
return request.Url.GetLeftPart(UriPartial.Authority);
}

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

     

 private string HandleGetRequest(HttpListenerRequest request)
{
if (request.Url.LocalPath == "/contact/create") ...
else if (request.Url.LocalPath == "/contact/")
{
var query = request.QueryString["nameQuery"];
var contactsHtml = string.Join("",
contacts
.Where(c => query == null || c.FullName.ToLower().Contains(query.ToLower()))
.Select(c => "<li>" + c.FullName + " (" + c.Address + ")</li>"));
return string.Format("<html>" +
...
"<ul id='contacts'>{0}</ul>" +
"</html>", contactsHtml);
}
else ...
}

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

В этой статье показано, как на самом деле работает HTTP и как за кулисами работают фреймворки, такие как ASP.NET MVC. Есть много деталей, которые мы рады, что фреймворк может исправить для нас, например, кодировка символов и чтение содержимого запроса POST. И есть много вещей, которые оказываются не такими сложными, как вы думаете, например, реальный шаблон «перенаправление на пост». В более чем одном проекте я понял, что, потратив несколько дней на понимание базовой технологии, я смог бы реализовать проект намного лучше и быстрее без «очевидных» популярных платформ, которые все рекомендуют вам использовать.

Я изобрел колесо в этой статье? Вы можете утверждать, что я так и сделал, но позвольте мне максимально расширить метафору «изобретать колесо»:

Мой опыт показывает, что многие «автомобили» сегодня имеют соосные колеса, в которых ось не установлена ​​в центре. Возможно, колесо было плохо сконструировано, или, может быть, машина была собрана неправильно. Может быть, мы заметили, что машина подпрыгивает, потому что два колеса имеют смещенный ось. И затем мы проводим большую работу, пытаясь настроить эти колеса для синхронизации подпрыгивания. Наконец мы публикуем статьи о приятных ровных волнах нашего автомобиля после выравнивания ошибок в колесах.

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

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