Feel Good.

27 января 2010

Интерфейс, Каркас, Реализация

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

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

Недолго думая, можно сразу начать писать единый класс, с "главным" методом, который будет делать все и сразу, а чтобы тело метода не раздувалось до гигантских размеров, выносишь часть кода во "вспомогательные" методы. Стоп! Посмотрите, не успел проект начаться, как мы уже получаем "монолитный" кусок кода.
Возьмите за правило начинать любой проект примерно так:

  1. Описать интерфейсы (interface).
  2. Описать каркас системы (abstract class).
  3. Реализовать интерфейсы.
  4. Реализовать каркас.

Давайте подумаем, какие сущности присутствуют в системе, и какие между ними связи. Итак, это письмо и почтовый ящик. Опишем их с помощью интерфейсов соответственно:


public interface ILetter

{

    int Id { get; }

    string Subject { get; }

    string Body { get; }

}


public interface IMailBox

{

    string Login { get; set; }

    string Password { get; set; }

    string Host { get; set; }

    int Port { get; set; }

 

    void Connect();

    IEnumerable<ILetter> ReciveAll();

    void Delete(int id);

    void Disconnect();

}


Мы описали(не реализовали) то, с чем будем работать в дальнейшем. Пришло время описать поведение системы, или как наши сущности взаимодействуют друг с другом. Для начала опишем каркас(framework) системы в виде абстрактного класса:

public abstract class MailProcessorBase

{

    protected IMailBox _mailBox;

 

    public void ProcessAll(bool cleanup)

    {

        _mailBox.Connect();

        IEnumerable<ILetter> letters = _mailBox.ReciveAll();

        foreach (ILetter letter in letters)

        {

            ProcessLetter(letter);

            if (cleanup)

                _mailBox.Delete(letter.Id);

        }

        _mailBox.Disconnect();

    }

 

    protected abstract void ProcessLetter(ILetter letter);

}


Чем хорош каркас? Тем, что он помогает быстро разобраться и понять общую логику работы модуля. Взглянув на каркас, Вы видите, какие основные компоненты он содержит, и то, как эти компоненты взаимодействуют. Сама же реализация этих компонент Вам, на данном этапе, не нужна. Убедитесь, что Вы правильно описали сущности и связи между ними, потому что теперь мы будем реализовать наши интерфейсы. В чем прелесть сего действия? А прелесть его в том, что мы можем реализовывать этот интерфейс как хотим, на свой вкус и цвет:

public class Letter : ILetter

{

    protected int _id;

    public int Id

    {

        get { return _id; }

    }

 

    protected string _subject;

    public string Subject

    {

        get { return _subject; }

    }

 

    protected string _body;

    public string Body

    {

        get { return _body; }

    }

 

    public Letter(int id, string subject, string body)

    {

        _id = id;

        _subject = subject;

        _body = body;

    }

}


public class Pop3MailBox : IMailBox

{       

    public string Login

    {

        get;

        set;

    }

 

    public string Password

    {

        get;

        set;

    }

 

    public string Host

    {

        get;

        set;

    }

 

    public int Port

    {

        get;

        set;

    }

 

    private POP3_Client _client = null;

 

    public Pop3MailBox(string login, string password, string host, int port)

    {

        _client = new POP3_Client();

    }

 

    public void Connect()

    {

        _client.Connect(Host, Port);

        _client.Authenticate(Login, Password, true);

    }

 

    public IEnumerable<ILetter> ReciveAll()

    {

        List<ILetter> letters = new List<ILetter>();

        POP3_MessagesInfo messagesInfo = _client.GetMessagesInfo();

        foreach (POP3_MessageInfo messageInfo in messagesInfo.Messages)

        {

            int msgId = messageInfo.MessageNr;

            ILetter letter =

                new Letter(

                    msgId,

                    _client.GetMessageSubject(msgId),

                    _client.GetMessageBody(msgId)

                    );

            letters.Add(letter);

        }

        return letters;

    }

 

    public void Delete(int id)

    {

        _client.DeleteMessage(id);

    }

 

    public void Disconnect()

    {

        _client.Disconnect();

    }                   

}


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

public class SimpleMailProcessor : MailProcessorBase

{

    public SimpleMailProcessor(IMailBox mailBox)

    {           

        _mailBox = mailBox;

    }

 

    protected override void ProcessLetter(ILetter letter)

    {

        Console.WriteLine("[{0}] {1}", letter.Subject, letter.Body);

    }

}


При реализации интерфейса, мне как раз понадобилась сторонняя POP3 библиотека (POP3_Client), и это мое право, ведь это моя реализация интерфейса. Главное, что мой класс реализует интерфейс. Помните, как раньше, чтобы узнать в общих чертах, как работает модуль, надо было лезть в код? Так вот, теперь можно только лишь взглянуть на интерфейс. Отлично, у нас все готово, осталось только использовать написанное:


IMailBox mailBox = new Pop3MailBox("login", "pwd", "pop.mail.ru", 110);

MailProcessorBase processor = new SimpleMailProcessor(mailBox);

processor.ProcessAll(false);


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

2 комментария:

  1. MailProcessorBase полагает, что его наследник запишет в поле _mailBox корректное значение, в то же время наследники об этом ничего не подозревают.
    Предпочтительнее было бы сделать поле _mailBox приватным и присваивать ему значение в защищенном конструкторе, предоставив при этом наследникам защищенное свойство MailBox, доступное только для чтения.

    ОтветитьУдалить
  2. Спасибо, нужное замечание.
    Можно расширить класс MailProcessorBase protected-конструктором, который будет требовать передачи в него ссылки на IMailBox.

    ОтветитьУдалить