Статьи

Повесть о повторном использовании — рефакторинг частей MangoTree

Работая над MangoTree — клиентом Twitter для Windows Phone, я изменяю тонны кода, написанного в самом начале, адаптируя его к новым тестовым сценариям и сценариям. Одним из основных элементов в моем приложении является класс OAuthClient — его целью является организация потока аутентификации OAuth со всеми необходимыми методами. Однако проблема заключалась в том, что я случайно начал использовать его в качестве слоя подключения к Твиттеру. 

Одно из правил, которые я нарушил, заключается в том, что код не будет использоваться повторно. OAuth не зависит от Twitter, так почему именно я пытаюсь обработать данные, полученные через API Twitter в этом классе? У меня был оператор switch внутри метода обратного вызова GetResponse, а также специальный обратный вызов для обновления состояния. Это в значительной степени уменьшило переносимость моего кода до нуля. Я работал над настройкой некоторых элементов OAuthClient и в итоге получил что-то вроде этого:

using System;
using System.Net;
using System.Collections.Generic;
using System.Linq;
using MangoTree.Utility;
using System.Security.Cryptography;
using System.Text;
using System.IO;

namespace MangoTree.Twitter
{
    public class OAuthClient
    {
        private Action<object[]> CompletionAction { get; set; }
        private RequestType TypeOfRequest { get; set; }

        public void PerformRequest(Dictionary<string, string> parameters, string url, string requestMethod, string consumerSecret, string token, string contentType, Action<object[]> completionAction, RequestType requestType)
        {
            CompletionAction = completionAction;
            TypeOfRequest = requestType;

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
            request.Method = requestMethod;
            request.ContentType = contentType;

            string OAuthHeader = GetOAuthHeader(parameters, requestMethod, url, consumerSecret, token);
            request.Headers["Authorization"] = OAuthHeader;

            request.BeginGetResponse(new AsyncCallback(GetResponse), request);
        }

        private void GetResponse(IAsyncResult result)
        {
            HttpWebRequest request = (HttpWebRequest)result.AsyncState;

            if (TypeOfRequest == RequestType.Read)
            {
                HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);

                using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                {
                    string completeString = reader.ReadToEnd();

                    CompletionAction(new object[] { completeString });
                }
            }
            else
            {
                CompletionAction(new object[] { request, result });
            }
        }

        private string GetOAuthHeader(Dictionary<string, string> parameters, string httpMethod, string url, string consumerSecret, string tokenSecret)
        {
            parameters = parameters.OrderBy(x => x.Key).ToDictionary(v => v.Key, v => v.Value);

            string concat = string.Empty;

            string OAuthHeader = "OAuth ";

            foreach (string k in parameters.Keys)
            {
                if (Parameters.Sanitized.Contains(k))
                    concat += k + "=" + StringHelper.SanitizeStatus(StringHelper.EncodeToUpper(parameters[k])) + "&";
                else
                    concat += k + "=" + parameters[k] + "&";

                if (!Parameters.Banned.Contains(k))
                    OAuthHeader += k + "=" + "\"" + parameters[k] + "\", ";
            }

            concat = concat.Remove(concat.Length - 1, 1);
            concat = StringHelper.EncodeToUpper(concat);

            concat = httpMethod + "&" + StringHelper.EncodeToUpper(url) + "&" + concat;

            byte[] content = Encoding.UTF8.GetBytes(concat);

            HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(consumerSecret + "&" + tokenSecret));
            hmac.ComputeHash(content);

            string hash = Convert.ToBase64String(hmac.Hash);
            hash = hash.Replace("-", "");

            OAuthHeader += "oauth_signature=\"" + Uri.EscapeDataString(hash) + "\"";

            return OAuthHeader;
        }
    }
}

Прежде всего, теперь есть параметр Action <object []> — я понятия не имею, что я собираюсь делать с возвращенными данными. Сегодня это может быть строка JSON, завтра я могу решить перейти на XML. Если уровень обработки интегрирован в GetResponse , я не получаю никакой гибкости. С указанным делегатом выполняемое действие может быть любым — все, что мне нужно, это переданные вторичные объекты.

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

Методы HTTP и типы контента также передаются независимо — даже при том, что количество фактически используемых ограничено, изменения могут произойти на стороне платформы, и лучше подготовиться.

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

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

2. Код, который «просто работает», становится все труднее поддерживать по мере роста приложения — в небольших проектах я, вероятно, оставил бы код, о котором я упоминал выше, без изменений, особенно если цель моего приложения была чрезвычайно простой (например, просто обновить положение дел). Однако, поскольку приложение становится более сложным, тот же код может вызвать головную боль, когда речь заходит о реорганизации потока приложения.

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

4. Ни один код долго не остается прежним — даже самые хорошо спланированные проекты нуждаются в модификации. Напишите код таким образом, чтобы вы могли изменить его позже.

5. Подумайте о других ситуациях, когда ваш код может быть повторно использован — могу ли я использовать свой слой OAuth для Facebook? Если ответ да, то я хорошо это написал. Если ответ «нет», мне нужно переписать этот слой. Это принесет пользу всем в долгосрочной перспективе.