Tuesday, December 31, 2013

Фабричный метод (Factory Method)

В данной статье рассмотрим рефакторинг приложения на основе паттернов, а именно использование паттерна Factory Method (фабричный метод). Паттерны - мощный инструмент, который может помочь привести код к одному стилю. Такой код можно легко модифицировать и сопровождать. Правда, иногда неумелое использование паттернов может усложнить жизнь разработчикам. Но в большинстве случаев они выступают как спасательный круг в море кода. Недавно довелось услышать интересное высказывание на данный счет: "Вы когда-нибудь бросали чтение классической книги после прочтения двух десятков страниц, толком не понимая, почему же ее так хвалят и что же со мной не так? При этом я не говорю за книги, типа банды четырех, о которой говорят на каждом шагу, но при этом есть лишь четыре человека в мире, которые прочитали ее от начала до конца." С такой фразы начинается статья "О дизайне и сложностях перевода", которая описывает сложности, с которыми столкнётся читатель при чтении книг по проектированию корпоративных решений. Например, книга "DDD" Эванса интересная, но довольно сложная для понимания, так как некоторые моменты  из неё ставят меня в тупик до сих пор. Соглашусь с мнением автора о том, что о данных книгах говорят всюду, но их прочитали только несколько человек, и те, вероятно, вскоре забыли прочитанный материал. На каждом проекте, в котором принимают участие разработчики разного уровня знаний, как правило, понимают, зачем нужны паттерны и как правильно их применять, лишь несколько человек. Иногда есть и другие моменты, когда человек использует паттерн, но не знает его названия и не может структурировать мысли по его поводу. Эта статья покажет возможность того, как паттерны упрощают жизнь разработчику программного обеспечения. Пример для данной статьи написан на основе кода, взятого с реального коммерческого проекта, но по максимуму упрощен для использования. Задача этого кода показать, как был написан основной вариант кода, и как этот код можно изменить. Для примера представлен прототип кода для отправки сообщений на базе компонента MailBee.NET. Интерфейс для отправки и получения сообщений:
public interface ICommunicator
{
    void Send(string from, string to, string[] relatedFiles, IEmailServerSettings settings);
    void Recieve();
}
Интерфейс для работы с почтой:
public interface IEmailCommunicator
{
    /// <summary>
    /// Получить идентификаторы всех имеющихся на сервере сообщений
    /// </summary>
    string[] GetMessageUids(IEmailServerSettings serverSettings);

    /// <summary>
    /// Отправка e-mail
    /// </summary>
    /// <param name="from">Поле "От" в e-mail</param>
    /// <param name="to">Поле "Кому" в e-mail</param>
    /// <param name="relatedFiles">Вложения</param>
    /// <param name="settings">SMTP настройки</param>
    void Send(string from,
                string to,
                string[] relatedFiles,
                IEmailServerSettings settings);

    /// <summary>
    /// Возвращает список имен файлов, которые были загружены с ошибкой
    /// </summary>
    /// <param name="notProcessedUids">Идентификаторы необработанных сообщений</param>
    /// <param name="serverSettings">Объект параметров сервера</param>
    /// <param name="directoryToSave">Папка для сохранения вложений</param>
    /// <param name="isNeedToDeleteMessageAfterReceive">Признак необходимости удаления письма после получения</param>
    /// <returns>Список имен файлов, полученных с ошибкой</returns>
    List<string> GetEmails(string[] notProcessedUids,
                                                IEmailServerSettings serverSettings,
                                                string directoryToSave,
                                                bool isNeedToDeleteMessageAfterReceive);

    /// <summary>
    /// Проверка аутентификации на на потовом сервере
    /// </summary>
    /// <param name="serverSettings">Параметры сервера</param>
    bool ServerAuthentication(IEmailServerSettings serverSettings);
}
Интерфейс для настройки почтового сервера:
public interface IEmailServerSettings
{
    /// <summary>
    /// Сервер
    /// </summary>
    string Address { get; set; }

    /// <summary>
    /// Порт
    /// </summary>
    int Port { get; set; }

    /// <summary>
    /// Имя пользователя
    /// </summary>
    string UserName { get; set; }
       
    /// <summary>
    /// Пароль пользователя
    /// </summary>
    string UserPassword { get; set; }

    /// <summary>
    /// Признак необходимости использовать SSL
    /// </summary>
    bool UseSsl { get; set; }

    /// <summary>
    /// Тип сервера настроек электронной почты
    /// </summary>
    EmailServerType EmailServerType { get; set; }
}
Рассмотрим саму отправку и прием писем с помощью данной библиотеки.
public class EmailCommunicator : ICommunicator, IEmailCommunicator
{
    #region [ private methods ]

    private static void CheckSmtpServerParams(IEmailServerSettings smtpServerParams)
    {
        if (smtpServerParams == null)
            throw new ArgumentNullException("smtpServerParams");
        if (smtpServerParams.EmailServerType != EmailServerType.Smtp)
            throw new ArgumentException("smtpServerParams has different type from EmailServerType.Smtp");
    }

    private static void CheckPop3ServerParams(IEmailServerSettings pop3ServerParams)
    {
        if (pop3ServerParams == null)
            throw new ArgumentNullException("pop3ServerParams");
        if (pop3ServerParams.EmailServerType != EmailServerType.Pop3)
            throw new ArgumentException("pop3ServerParams has different type from EmailServerType.Pop3");
    }

    private static void SmtpServerAuthenticationInternal(Smtp mailer, IEmailServerSettings smtpServerSettings)
    {
        if (mailer == null)
            throw new ArgumentNullException("mailer");

        CheckSmtpServerParams(smtpServerSettings);

        int portInteger = smtpServerSettings.Port;
          
        // Создаем объект для указания параметров SMTP-сервера
        //
        var server = new SmtpServer
        {
            Name = smtpServerSettings.Address,
            AuthMethods = string.IsNullOrEmpty(smtpServerSettings.UserName)
                            ? AuthenticationMethods.None
                            : AuthenticationMethods.Auto,
            AuthOptions = AuthenticationOptions.PreferSimpleMethods,
            AccountName = smtpServerSettings.UserName,
            Port = portInteger,
            Password = smtpServerSettings.UserPassword
        };
        //
        // Использование SSL
        //
        if (smtpServerSettings.UseSsl)
            server.SslMode = SslStartupMode.OnConnect;
        //
        // Добавляем параметры SMTP-сервера
        //
        mailer.DnsServers.Clear();
        mailer.SmtpServers.Clear();
        mailer.SmtpServers.Add(server);
        //
        // Аутентификация на SMTP-сервере
        //
        if (!mailer.IsConnected)
            mailer.Connect();

        mailer.Hello();

        if (mailer.IsLoggedIn)
            return;
        var loginSuccess = mailer.Login();

        if (!loginSuccess)
            throw new Exception("Login error");
           
    }

    private static void Pop3ServerAuthAndLoginInternal(Pop3 pop3, IEmailServerSettings pop3ServerSettings)
    {
        if (pop3 == null)
            throw new ArgumentNullException("pop3");

        CheckPop3ServerParams(pop3ServerSettings);

        int portInteger = pop3ServerSettings.Port;

        if (pop3ServerSettings.UseSsl)
            pop3.SslMode = SslStartupMode.OnConnect;
        //
        // Соединяемся с POP3-сервером и проводим аутентификацию
        //
        if (!pop3.IsConnected)
            pop3.Connect(pop3ServerSettings.Address, portInteger);

        if (!pop3.IsLoggedIn)
            pop3.Login(pop3ServerSettings.UserName,
                        pop3ServerSettings.UserPassword,
                        AuthenticationMethods.Auto,
                        AuthenticationOptions.PreferSimpleMethods,
                        null);
        }

    private static bool IsHeaderCorrect(MailMessage header)
    {
        return header != null &&
                !string.IsNullOrEmpty((string)header.UidOnServer);
    }

    /// <summary>
    /// Проверка аутентификации на SMTP-сервере
    /// </summary>
    /// <param name="smtpServerSettings">Объект параметров SMTP-сервера</param>
    private static bool SmtpServerAuthentication(IEmailServerSettings smtpServerSettings)
    {
        CheckSmtpServerParams(smtpServerSettings);
        using (var mailer = new Smtp())
        {
            try
            {
                SmtpServerAuthenticationInternal(mailer, smtpServerSettings);
            }
            catch (Exception e)
            {
                //Log Exception
                return false;
            }
            finally
            {
                try
                {
                    if (mailer.IsConnected)
                        mailer.Disconnect();

                }
                // ReSharper disable EmptyGeneralCatchClause
                catch
                // ReSharper restore EmptyGeneralCatchClause
                {
                    // Обработка исключения пропущена поскольку не корректная работа метода Disconnect()
                    // сделана на всякий случай не влияет на логику программы
                }
            }
        }
        return true;
    }

    /// <summary>
    /// Проверка аутентификации на POP3-сервере
    /// </summary>
    /// <param name="pop3ServerSettings">Объект параметров РОР3-сервера</param>
    private static bool Pop3ServerAuthentication(IEmailServerSettings pop3ServerSettings)
    {
        CheckPop3ServerParams(pop3ServerSettings);
        using (var pop3 = new Pop3())
        {
            try
            {
                Pop3ServerAuthAndLoginInternal(pop3, pop3ServerSettings);
            }
            catch (Exception e)
            {
                //WriteErrorLog(pop3.Log, e);
                return false;
            }
            finally
            {
                try
                {
                    if (pop3.IsConnected)
                        pop3.Disconnect();
                }
                // ReSharper disable EmptyGeneralCatchClause
                catch
                // ReSharper restore EmptyGeneralCatchClause
                {
                    // Обработка исключения пропущена поскольку не корректная работа метода Disconnect()
                    // сделана на всякий случай не влияет на логику программы
                }
            }
        }
        return true;
    }

    #endregion

    #region [ public methods ]

    /// <summary>
    /// Получить идентификаторы всех имеющихся на сервере сообщений
    /// </summary>
    public string[] GetMessageUids(IEmailServerSettings pop3ServerSettings)
    {
        CheckPop3ServerParams(pop3ServerSettings);

        using (var pop3 = new Pop3())
        {
            pop3.InboxPreloadOptions = Pop3InboxPreloadOptions.Uidl;

            Pop3ServerAuthAndLoginInternal(pop3, pop3ServerSettings);

            string[] uids;
            try
            {
                uids = pop3.GetMessageUids();
            }
            finally
            {
                try
                {
                    if (pop3.IsConnected)
                        pop3.Disconnect();
                }
                // ReSharper disable EmptyGeneralCatchClause
                catch
                // ReSharper restore EmptyGeneralCatchClause
                {
                    // Обработка исключения пропущена поскольку не корректная работа метода Disconnect()
                    // сделана на всякий случай не влияет на логику программы
                }
            }

            return uids;
        }
    }


    /// <summary>
    /// Отправка e-mail
    /// </summary>
    /// <param name="from">Поле "От" в e-mail</param>
    /// <param name="to">Поле "Кому" в e-mail</param>
    /// <param name="relatedFiles">Вложения</param>
    /// <param name="settings">SMTP настройки</param>
    public void Send(string from,
                        string to,
                        string[] relatedFiles,
                        IEmailServerSettings settings)
    {
        using (var mailer = new Smtp())
        {
            try
            {
                SmtpServerAuthenticationInternal(mailer, settings);
                //
                // Указываем параметры сообщения
                //
                mailer.From.AsString = String.IsNullOrEmpty(from)
                                        ? "default"
                                        : from;
                mailer.To.AsString = to;
                mailer.Subject = string.Empty;//заголовки не реализованы
                mailer.BodyPlainText = string.Empty;//тело письма не реализовано
                foreach (var file in relatedFiles)
                {
                    mailer.AddAttachment(file);
                }
                //
                // Отправляем
                //
                mailer.Send();
            }
            finally
            {
                try
                {
                    if (mailer.IsConnected)
                        mailer.Disconnect();
                }
                // ReSharper disable EmptyGeneralCatchClause
                catch
                // ReSharper restore EmptyGeneralCatchClause
                {
                    // Обработка исключения пропущена поскольку не корректная работа метода Disconnect()
                    // сделана на всякий случай не влияет на логику программы
                }
            }
        }
    }

    public void Recieve()
    {
        throw new NotImplementedException();
    }

    /// <summary>
    /// Возвращает список имен файлов, которые были загружены с ошибкой
    /// </summary>
    /// <param name="notProcessedUids">Идентификаторы необработанных сообщений</param>
    /// <param name="serverSettings">Объект параметров сервера</param>
    /// <param name="isNeedToDeleteMessageAfterReceive">Признак необходимости удаления письма после получения</param>
    /// <returns>Список имен файлов, полученных с ошибкой</returns>
    public List<string> GetEmails(string[] notProcessedUids,
                                                        IEmailServerSettings serverSettings,
                                                        string directoryToSave,
                                                        bool isNeedToDeleteMessageAfterReceive)
    {
        CheckPop3ServerParams(serverSettings);
        using (var pop3 = new Pop3())
        {
            try
            {
                pop3.InboxPreloadOptions = Pop3InboxPreloadOptions.Uidl;
                //
                // Список имен файлов, которые были загруженны с ошибкой
                //
                var result = new List<string>();
                //
                // Создаем временную папку для сохранения квитанций
                //
                try
                {
                    if (!Directory.Exists(directoryToSave))
                        Directory.CreateDirectory(directoryToSave);
                }
                catch (Exception ex)
                {
                    throw new Exception("Create save ticket directory",
                                                ex);
                }

                Pop3ServerAuthAndLoginInternal(pop3, serverSettings);

                //
                // Получаем список уже прочитанных писем на РОР3-сервере для "отсеивания" новых писем
                //
                foreach (var uid in notProcessedUids)
                {
                    bool? isHandledTicketEmailMessage = null;

                    var messageIndex = pop3.GetMessageIndexFromUid(uid);
                    var messageHeader = pop3.DownloadMessageHeader(messageIndex);

                    if (IsHeaderCorrect(messageHeader))
                    {
                        //
                        // Загружаем сообщение
                        //
                        var mailMessage = pop3.DownloadEntireMessage(messageIndex);
                        //
                        // Просматриваем и сохраняем приложения письма
                        //                            

                        foreach (var attachment in mailMessage.Attachments.OfType<Attachment>())
                        {
                            //
                            // Записываем файл вложения в directoryToSave
                            //
                            try
                            {
                                var uniqueFileName = attachment.FilenameOriginal;

                                File.WriteAllBytes(uniqueFileName, attachment.GetData());
                                //
                                // Если при записи предыдущих вложений не возникало ошибок, то помечаем письмо к удалению
                                //
                                if (isHandledTicketEmailMessage == null)
                                    isHandledTicketEmailMessage = true;
                            }
                            catch
                            {
                                //Если при получении возникла ошибка, то записываем имя файла в результат - как ошибочный
                                result.Add(attachment.Filename);

                                isHandledTicketEmailMessage = false;
                            }
                        }
                    }

                    //
                    // Удаляем письмо с POP3-сервера, если указан признак необходимости
                    // удаления письма после получения и в письме содержится подходяшее приложение
                    //
                    try
                    {
                        if (isNeedToDeleteMessageAfterReceive
                            && isHandledTicketEmailMessage.HasValue
                            && isHandledTicketEmailMessage.Value)
                        {
                            pop3.DeleteMessage(messageIndex);
                        }
                    }
                    // ReSharper disable EmptyGeneralCatchClause
                    catch
                    // ReSharper restore EmptyGeneralCatchClause
                    {
                        // Ошибку удаления письма пользователю не показываем
                    }
                }

                return result;
            }
            finally
            {
                try
                {
                    if (pop3.IsConnected)
                        pop3.Disconnect();
                }
                // ReSharper disable EmptyGeneralCatchClause
                catch
                // ReSharper restore EmptyGeneralCatchClause
                {
                    // Обработка исключения пропущена поскольку не корректная работа метода Disconnect()
                    // сделана на всякий случай не влияет на логику программы
                }
            }
        }
    }

    /// <summary>
    /// Проверка аутентификации на на потовом сервере
    /// </summary>
    /// <param name="serverSettings">Параметры сервера</param>
    public bool ServerAuthentication(IEmailServerSettings serverSettings)
    {
        switch (serverSettings.EmailServerType)
        {
            case EmailServerType.Smtp:
                {

                    return SmtpServerAuthentication(serverSettings);
                }
            case EmailServerType.Pop3:
                {
                    return Pop3ServerAuthentication(serverSettings);
                }
            default:
                return false;
        }
    }

    #endregion
}
Код получился довольно большой и плохо расширяемый. За задумкой разработчика данного творения, этот класс можно легко расширять и использовать в дальнейшем. Моя задача для такого кода состояла в том, чтобы добавить поддержку протокола IMAP для входящих сообщений. Первый вариант, как я это реализовал, - добавил в данный класс функции, необходимые для реализации протокола IMAP для данного класса:
  • CheckImapServerParams;
  • ImapServerAuthAndLoginInternal;
  • ImapServerAuthentication.
Посмотрим на данный код с другой стороны. Что делать, если в наше приложение добавить поддержку ещё некоторого протокола для почты, снова дублировать код? Если внимательно посмотреть на код, то с первого раза в нём видно очень много дублирования. Есть несколько способов решить данную проблему. Два способа, которые пришли мне в голову сразу, как только я увидел данный код:

  • Отделить код по отправке и получения почты по разным интерфейсам. Для нашего примера их будет 2: IReciever  и ISender.
  • Сделать один общий интерфейс IMailCommunicator в котором объединить логику.
Первый вариант более изящный, но плодит больше логики. Второй вариант проще, так как у нас интерфейс для отправки, по сути, только один (SMTP), то метод для отправки сообщения мы можем добавить в общий интерфейс. Я выбрал второй вариант потому, что я довольно ленив, и также из-за того, что этот вариант можно реализовать с помощью фабричного метода. Рассмотрим описание данного паттерна с книги  банды четырех. Фабричный метод – паттерн, порождающий классы.
Назначение: определяет интерфейс для создания объекта, но оставляет подклассам решение о том, какой класс инстанцировать. Фабричный метод позволяет классу делегировать инстанцирование подклассам.  Другими словами, мы передаем в метод некоторые параметры,  а он на основании этих параметров создаст нам класс, который будет выполнять всю необходимую логику. Огромный плюс такого подхода в простоте использования и реализации; также данный код легко поддаётся тестированию. Мы можем не изменять интерфейс IEmailCommunicator, а сделать обертку над существующими классами на основании паттерна "фабричный метод". Но поскольку мы предпочитаем нормальный подход для написания кода, изменим данный класс; теперь также можно избавиться от интерфейса ICommunicator.  Посмотрим, что у нас получилось:
Интерфейс IEmailCommunicator
public interface IEmailCommunicator
{
    IMailCommunicator GetMailCommunicator(IEmailServerSettings settings);
}   
Как видим, интерфейс IEmailCommunicator значительно упростился. С реализацией тоже стало все очень просто и прозрачно.
public class EmailCommunicator : IEmailCommunicator
{
    public IMailCommunicator GetMailCommunicator(IEmailServerSettings settings)
    {
        if (settings == null)
            throw new ArgumentException("settings is null");

        switch (settings.EmailServerType)
        {
            case EmailServerType.Imap:
                return new ImapCommunicator(settings);
            case EmailServerType.Pop3:
                return new PopCommunicator(settings);
            case EmailServerType.Smtp:
                return new SmtpCommunicator(settings);
            default:
                throw new ArgumentException("Communicator not found");
        }
    }
}
Теперь класс EmailCommunicator, который составлял порядка 400 строк кода, стал очень компактным. А вся логика по верификации перенеслась в конкретные классы. Для примера рассмотрим, как реализован класс SmtpCommunicator.
public class SmtpCommunicator : IMailCommunicator
{
    #region [ vars ]
    private readonly IEmailServerSettings _serverSettings;
    #endregion

    #region [ .ctor ]
    public SmtpCommunicator(IEmailServerSettings serverSettings)
    {
        if (serverSettings == null)
            throw new ArgumentNullException("serverSettings");
        if (serverSettings.EmailServerType != EmailServerType.Smtp)
            throw new ArgumentException("serverSettings has different type from EmailServerType.Smtp");

        _serverSettings = serverSettings;
    }
    #endregion

    #region [ public methods ]
    public string[] GetMessageUids()
    {
        throw new NotImplementedException("Smtp can't get message uids");
    }

    public void Send(string @from, string to, string[] relatedFiles)
    {
        using (var mailer = new Smtp())
        {
            try
            {
                ServerAuthenticationInternal(mailer);
                //
                // Указываем параметры сообщения
                //
                mailer.From.AsString = String.IsNullOrEmpty(from)
                                        ? "default"
                                        : from;
                mailer.To.AsString = to;
                mailer.Subject = string.Empty;//заголовки не реализованы
                mailer.BodyPlainText = string.Empty;//тело письма не реализовано
                foreach (var file in relatedFiles)
                {
                    mailer.AddAttachment(file);
                }
                //
                // Отправляем
                //
                mailer.Send();
            }
            finally
            {
                try
                {
                    if (mailer.IsConnected)
                        mailer.Disconnect();
                }
                // ReSharper disable EmptyGeneralCatchClause
                catch
                // ReSharper restore EmptyGeneralCatchClause
                {
                    // Обработка исключения пропущена поскольку не корректная работа метода Disconnect()
                    // сделана на всякий случай не влияет на логику программы
                }
            }
        }
    }

    public List<string> RecieveEmails(string[] notProcessedUids, string directoryToSave,
                                bool isNeedToDeleteMessageAfterReceive)
    {
        throw new NotImplementedException("Smtp can't recieve email");
    }

    public bool ServerAuthentication()
    {
        using (var mailer = new Smtp())
        {
            try
            {
                ServerAuthenticationInternal(mailer);
            }
            catch (Exception e)
            {
                //Log Exception
                return false;
            }
            finally
            {
                try
                {
                    if (mailer.IsConnected)
                        mailer.Disconnect();

                }
                // ReSharper disable EmptyGeneralCatchClause
                catch
                // ReSharper restore EmptyGeneralCatchClause
                {
                    // Обработка исключения пропущена поскольку не корректная работа метода Disconnect()
                    // сделана на всякий случай не влияет на логику программы
                }
            }
        }
        return true;
    }
    #endregion

    #region [ private methods ]
    private void ServerAuthenticationInternal(Smtp mailer)
    {
        if (mailer == null)
            throw new ArgumentNullException("mailer");

        int portInteger = _serverSettings.Port;

        // Создаем объект для указания параметров SMTP-сервера
        //
        var server = new SmtpServer
        {
            Name = _serverSettings.Address,
            AuthMethods = string.IsNullOrEmpty(_serverSettings.UserName)
                            ? AuthenticationMethods.None
                            : AuthenticationMethods.Auto,
            AuthOptions = AuthenticationOptions.PreferSimpleMethods,
            AccountName = _serverSettings.UserName,
            Port = portInteger,
            Password = _serverSettings.UserPassword
        };
        //
        // Использование SSL
        //
        if (_serverSettings.UseSsl)
            server.SslMode = SslStartupMode.OnConnect;
        //
        // Добавляем параметры SMTP-сервера
        //
        mailer.DnsServers.Clear();
        mailer.SmtpServers.Clear();
        mailer.SmtpServers.Add(server);
        //
        // Аутентификация на SMTP-сервере
        //
        if (!mailer.IsConnected)
            mailer.Connect();

        mailer.Hello();

        if (mailer.IsLoggedIn)
            return;
        var loginSuccess = mailer.Login();

        if (!loginSuccess)
            throw new Exception("Login error");
    }
    #endregion
}
Код стал более компактным и позволяет расширять данный класс и добавлять необходимые проверки, не боясь запутаться в собственном "спагетти коде". Также рассмотрим, как реализован протокол Pop3.
public class PopCommunicator : IMailCommunicator
{
    #region [ vars ]
    private readonly IEmailServerSettings _serverSettings;
    #endregion

    #region [ .ctor ]
    public PopCommunicator(IEmailServerSettings serverSettings)
    {
        if (serverSettings == null)
            throw new ArgumentNullException("serverSettings");
        if (serverSettings.EmailServerType != EmailServerType.Pop3)
            throw new ArgumentException("serverSettings has different type from EmailServerType.Pop3");
        _serverSettings = serverSettings;
    }
    #endregion

    #region [ public methods ]
    public string[] GetMessageUids()
    {
        using (var pop3 = new Pop3())
        {
            pop3.InboxPreloadOptions = Pop3InboxPreloadOptions.Uidl;

            ServerAuthAndLoginInternal(pop3);

            string[] uids;
            try
            {
                uids = pop3.GetMessageUids();
            }
            finally
            {
                try
                {
                    if (pop3.IsConnected)
                        pop3.Disconnect();
                }
                // ReSharper disable EmptyGeneralCatchClause
                catch
                // ReSharper restore EmptyGeneralCatchClause
                {
                    // Обработка исключения пропущена поскольку не корректная работа метода Disconnect()
                    // сделана на всякий случай не влияет на логику программы
                }
            }

            return uids;
        }
    }

    public void Send(string @from, string to, string[] relatedFiles)
    {
        throw new NotImplementedException("Pop protocol can't send message");
    }

    public List<string> RecieveEmails(string[] notProcessedUids, string directoryToSave, bool isNeedToDeleteMessageAfterReceive)
    {
        using (var pop3 = new Pop3())
        {
            try
            {
                pop3.InboxPreloadOptions = Pop3InboxPreloadOptions.Uidl;
                //
                // Список имен файлов, которые были загруженны с ошибкой
                //
                var result = new List<string>();
                //
                // Создаем временную папку для сохранения квитанций
                //
                try
                {
                    if (!Directory.Exists(directoryToSave))
                        Directory.CreateDirectory(directoryToSave);
                }
                catch (Exception ex)
                {
                    throw new Exception("Create save ticket directory", ex);
                }

                ServerAuthAndLoginInternal(pop3);

                //
                // Получаем список уже прочитанных писем на РОР3-сервере для "отсеивания" новых писем
                //
                foreach (var uid in notProcessedUids)
                {
                    bool? isHandledTicketEmailMessage = null;

                    var messageIndex = pop3.GetMessageIndexFromUid(uid);
                    var messageHeader = pop3.DownloadMessageHeader(messageIndex);

                    if (IsHeaderCorrect(messageHeader))
                    {
                        //
                        // Загружаем сообщение
                        //
                        var mailMessage = pop3.DownloadEntireMessage(messageIndex);
                        //
                        // Просматриваем и сохраняем приложения письма
                        //                           

                        foreach (var attachment in mailMessage.Attachments.OfType<Attachment>())
                        {
                            //
                            // Записываем файл вложения в directoryToSave
                            //
                            try
                            {
                                var uniqueFileName = Path.Combine(directoryToSave, attachment.FilenameOriginal);

                                File.WriteAllBytes(uniqueFileName, attachment.GetData());
                                //
                                // Если при записи предыдущих вложений не возникало ошибок, то помечаем письмо к удалению
                                //
                                if (isHandledTicketEmailMessage == null)
                                    isHandledTicketEmailMessage = true;
                            }
                            catch
                            {
                                //Если при получении возникла ошибка, то записываем имя файла в результат - как ошибочный
                                result.Add(attachment.Filename);

                                isHandledTicketEmailMessage = false;
                            }
                        }
                    }

                    //
                    // Удаляем письмо с POP3-сервера, если указан признак необходимости
                    // удаления письма после получения и в письме содержится подходяшее приложение
                    //
                    try
                    {
                        if (isNeedToDeleteMessageAfterReceive
                            && isHandledTicketEmailMessage.HasValue
                            && isHandledTicketEmailMessage.Value)
                        {
                            pop3.DeleteMessage(messageIndex);
                        }
                    }
                    // ReSharper disable EmptyGeneralCatchClause
                    catch
                    // ReSharper restore EmptyGeneralCatchClause
                    {
                        // Ошибку удаления письма пользователю не показываем
                    }
                }

                return result;
            }
            finally
            {
                try
                {
                    if (pop3.IsConnected)
                        pop3.Disconnect();
                }
                // ReSharper disable EmptyGeneralCatchClause
                catch
                // ReSharper restore EmptyGeneralCatchClause
                {
                    // Обработка исключения пропущена поскольку не корректная работа метода Disconnect()
                    // сделана на всякий случай не влияет на логику программы
                }
            }
        }
    }

    public bool ServerAuthentication()
    {
        using (var pop3 = new Pop3())
        {
            try
            {
                ServerAuthAndLoginInternal(pop3);
            }
            catch (Exception)
            {
                return false;
            }
            finally
            {
                try
                {
                    if (pop3.IsConnected)
                        pop3.Disconnect();
                }
                // ReSharper disable EmptyGeneralCatchClause
                catch
                // ReSharper restore EmptyGeneralCatchClause
                {
                    // Обработка исключения пропущена поскольку не корректная работа метода Disconnect()
                    // сделана на всякий случай не влияет на логику программы
                }
            }
        }
        return true;
    }
    #endregion

    #region [ private methods ]
    private void ServerAuthAndLoginInternal(Pop3 pop3)
    {
        if (pop3 == null)
            throw new ArgumentNullException("pop3");

        int portInteger = _serverSettings.Port;

        if (_serverSettings.UseSsl)
            pop3.SslMode = SslStartupMode.OnConnect;
        //
        // Соединяемся с POP3-сервером и проводим аутентификацию
        //
        if (!pop3.IsConnected)
            pop3.Connect(_serverSettings.Address, portInteger);

        if (!pop3.IsLoggedIn)
            pop3.Login(_serverSettings.UserName,
                        _serverSettings.UserPassword,
                        AuthenticationMethods.Auto,
                        AuthenticationOptions.PreferSimpleMethods,
                        null);
    }

    private bool IsHeaderCorrect(MailMessage header)
    {
        return header != null &&
                !string.IsNullOrEmpty((string)header.UidOnServer);
    }
    #endregion
}

Добавление паттерна "фабричный метод" значительно упростило нам реализацию данного кода и позволило без проблем добавить поддержку нового протокола. Если Вам вдруг понадобится добавить какой-либо протокол ещё, Вы можете сделать это без проблем. Также огромным плюсом данного подхода является то, что Вы  можете покрыть данные классы интеграционными и юнит-тестами, что нельзя было сделать в первом коде. А благодаря использованию IoC контейнеров, как, например, Autofac Вы можете сделать использование данного подхода  гибким и элегантным.  Надеюсь, что статья подвигнет Вас к изучению паттернов проектирования, если Вы с ними еще не знакомы. Исходники к данной статье можно скачать по ссылке TestEmailWorker

No comments:

Post a Comment