Статьи

Средства обеспечения MVC Вдохновленного продолжения Передачи стиль

В этой статье я начинаю серию производной от того, как я проектирования blueBill Mobile , Android в приложении, но в дополнении к технологии , связанной с тем (не только Android) Я буду много говорить об общих стратегиях дизайна. Соответствующее усилие было сделано , чтобы определить дизайн , что делает тестирование легче , а также снижает технологию релевантной части коды к минимуму.

На самом деле, все приложение разрабатывается как ядро , которое является просто Java и могут быть реализованы с помощью различных решений пользовательского интерфейса, таких как Android, веб — приложений и настольных; примеры будут приведены в последующих статьях , которые охватывают также Vaadin, Wicket (для веб — части) и платформы NetBeans (для настольной части). Статьи будут сосредоточены на разные темы случая к случаю, перемещаясь из конструктивных соображений , чтобы понизить детали уровня , касающиеся осуществления, даже в том числе инструменты, такие как Maven, в среде IDE NetBeans, в Maven-андроида-плагин и плагин NetBeans Android.

В этой первой статье мы будем иметь дело с конструктивными соображениями об использовании MVC вместе с некоторыми идеями, вдохновленные из техники callled Продолжение-Пас стиля (CPS). Последняя часть этой статьи посвящена тестированию и предполагает предварительное знание Mockito.

Я должен поблагодарить многие друг в КУВШИНЕ Milano и в том JavaPosse за то, что помогли мне лучше понять КП и правильный способ обратиться к нему.

Примеры кода в основном взяты из blueBill Mobile для Android, версия 1,0-АЛЬФА-3.

Давайте начнем с простой вещью. blueBill Mobile содержит небольшое чтения RSS-канал, называемый «Новости», который используется для доставки сообщений пользователям из блога проекта. Требования просты:

  1. Пользователь вводит экран специфического и видит список тем (с субтитрами и публикации времени).

  2. Каждая тема может быть помечена как читать или не читать с помощью контекстного меню, и это оказывается по-другому, чтобы отразить свое состояние.

  3. Тема может быть выбрана, чтобы ее можно было прочитать полностью (и будет помечена как прочитанная).

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

  5. Предпочтение говорит, разрешено ли приложению подключаться к Интернету.

  6. Если сетевые подключения разрешены, служба новостей получает свежую копию канала.

  7. Если сетевые соединения не допускаются и сохраненная копия хранится в памяти, новости визуализируются с предупреждением и не предпринимается никаких попыток сделать, чтобы получить новую копию.

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

  9. Неожиданные ошибки (например, отказы подключения к сети) будут надлежащим образом сообщается.

Это довольно просто, но позволяет изучить некоторые направления проектирования потока управления, перемещающегося из бэкэнда (служба новостей) и пользовательского интерфейса (подтверждения), а также стратегии тестирования.

NewsService предоставляет два метода:

public interface NewsService
  {
    
    public void eventuallyCheckForUpdates();

    public void getNewsFeed (@Nonnull AvailabilityNotifier availabilityNotifier);
  }
  1. в конечном итогеCheckForUpdates () может быть вызвано при инициализации приложения и, если разрешены сетевые подключения, оно ищет свежую копию канала в фоновом режиме. Это позволяет сэкономить время и в итоге уже загрузить данные, когда пользователь их запросит. Мы можем спокойно игнорировать его в рамках этого упражнения, поскольку оно не взаимодействует с другими классами, которые мы ввели.

  2. getNewsFeed () — это метод, который извлекает канал: он вызывается NewsViewController, когда он хочет отобразить данные в NewsView.

Есть две характеристики getNewsFeed (), на которые мы должны обратить внимание:

  1. он может иметь несколько результатов (например, канал может быть доступным или нет по разным причинам);

  2. это может быть выполнено за совсем другое время (канал может быть доступен немедленно, или он может быть загружен из сети, операция, которая может потребовать некоторого времени)

Одна из первых вещей, что приходит на ум, чтобы реализовать этот метод как простой «добытчик», так она возвращает различные значения, представляющие результат. Например, экземпляр RssFeed, если данные доступны, null, если нет; или исчислительный тип для различных результатов, и вызывающий абонент будут затем получить фактическую подачу с методом getRssFeed () в случае положительного результата; или, возможно, отрицательные результаты будут представлены в виде исключения, которое описывает причину, по которой данные недоступны. Во всех этих случаях метод не будет блокировать до завершения.

Мы должны увидеть некоторые проблемы уже на этих помещениях. Например:

  1. нулевые / результаты не являющиеся нулевыми, должны быть обработаны с, если / еще и перечислительной результате поощряет использование переключателя. Оба подхода следует избегать в конструкции хорошего OO, так как они не масштабируются: после эволюции NewsService, который добавил новый результат мы могли бы легко забыть обновить все переключатели в приложении, добавляя необходимый новый случай.

  2. Методы блокировки не могут напрямую вызываться компонентом представления во многих технологиях, например, Swing, Android или некоторых веб-платформах; другие технологии (в основном веб-ориентированные) не будут иметь проблем. Обычно для решения этой проблемы существуют специальные библиотечные функции, такие как хорошо известный SwingWorker в Swing или аналогичный AsyncTask в Android, которые разбивают последовательность операций на части, связанные с соответствующим потоком. Эти решения работают, но программист должен помнить, чтобы применять их по мере необходимости (и в некоторой степени создать сильную связь дизайна с технологией); что, в свою очередь, требует, чтобы службы должным образом документировали блокировку некоторых открытых методов. Если в ходе развития проекта метод изменит свое блокирующее поведение, вероятно, пользовательский интерфейс будет работать неправильно. Другими словами, я нене хочу видеть SwingWorker или AsyncTask в частях кода, которые не зависят от технологии.

Но есть вопрос более тонкий. Рассматривая спецификации, давайте сосредоточимся на том, что сетевые подключения должны быть активированы посредством предпочтения, или система должна явно запросить подтверждение у пользователя. Давайте представим, как мы собираемся написать NewsViewController: он вызывает getNewsFeed () и, если он отвечает, что требуется сетевое соединение, открывает диалоговое окно подтверждения. Пользователь в конечном итоге подтверждает, что он хочет подключиться, и в этот момент NewViewController вызывает … что? Возможно, другой метод NewsService, такой как getNewsFeedWithoutConfirmation (), который подключается без запроса обратной связи.

Все в порядке? В конце концов , это будет работать , и я видел (и написано) много частей кода , как это. Проблема в том , что мешает рассеянно разработчика от прямого вызова getNewsFeedWithoutConfirmation (), таким образом , обходя любые проверки и нарушения спецификаций? Дело в том , что мы подвергнем методы в службе , которая не должно быть названа , но при точных предварительных условиях. Я думаю , что вы уже видели много раз, в JavaDocs, комментарии предупреждают, что «Этот метод НЕ НАЗВАТЬ ПОКА …». Конечно, многие не читают JavaDocs и, честно говоря, чаще всего такие предпосылки не должным образом задокументированы. Обычно это заканчивается тем , что много головной боли. Конечно, приложение должно быть проверено и правильно написанный тест обнаружит ошибку. Но надежный код навязываетполитика (для того, что возможно): предотвращение ошибок происходят по построению лучше, чем позволяют ошибке произойти и позже охоту на них.

С другой стороны, давайте проверим, правильно ли мы распределили обязанности между классами. Какой класс должен нести ответственность за соблюдение политики подтверждения соединения? Несомненно, NewsService, который инкапсулирует сетевое соединение; наверняка это не обязанность NewsViewController, которая должна только правильно координировать поток управления между NewsService и NewsView. Теперь в гипотетической реализации, которую мы нарисовали, ответственность распределяется между двумя классами: NewsService уведомляет о необходимости подтверждения (правильно), но NewsViewController способен напрямую загружать команду без дополнительного подтверждения. Вместо этого, он должен просто сообщить NewsService, что получил подтверждение (как получить подтверждение — это вопрос пользовательского интерфейса),и пусть последний решит, как продолжить (не вопрос пользовательского интерфейса).

Давайте попробуем лучший дизайн, чтобы справиться с проблемами, которые мы нашли до сих пор — для ясности, давайте вспомним их:

  1. мы не хотим использовать переключатель или if / else в реализации NewsViewController;

  2. мы не хотим обратить особое внимание в случае getNewsFeed () блоки в течение длительного времени, и мы не хотим, чтобы внедрить технологии конкретных решений в модулях, предназначенных не зависят от технологии пользовательского интерфейса;

  3. мы хотим включить всю ответственность политики подтверждения соединения в NewsService.

Принятие асинхронного стиля передачи сообщений является элегантным решением проблемы потоков. На практике мы гарантируем, что getNewsFeed () завершается немедленно, даже когда результат не готов; вызывающий будет уведомлен позже, с помощью обратного вызова, который передается в качестве аргумента:

    public static interface AvailabilityNotifier
      {
        public void notifyFeedAvailable (@Nonnull RssFeed newsFeed);

        public void notifyCachedFeedAvailable (@Nonnull RssFeed newsFeed);
        
        public void notifyFeedUnavailable();

        public void notifyFeedCouldBeDownloaded (@Nonnull DownloadConfirmation confirmation);
      }

AvailabilityNotifier будет вызываться в одном из четырех методов для одного из четырех возможных результатов:

  1. notifyFeedAvailable () уведомляет, что свежий канал доступен, и переносит его в качестве параметра;

  2. notifyCachedFeedAvailable () уведомляет о доступности кэшированного канала (по какой-то причине не удалось получить свежую копию) и переносит его в качестве параметра;

  3. notifyFeedUnavailable () уведомляет, что невозможно получить какие-либо данные;

  4. notifyFeedCouldBeDownloaded () сообщает, что есть нет данных, но это может быть возможным, чтобы загрузить его, подключив к сети.

Конечно, наличие определенного метода обратного вызова для каждого результата также устраняет необходимость переключения. Если версия будущего NewsService позволит пятый результат, наш код не будет компилироваться, если не обеспечить реализацию пятого метода, заставляя нас заботиться о новом поведении. Или, возможно, это будет возможно обеспечить реализацию по умолчанию нового метода (таким образом, превращая AvailabilityNotifier в абстрактный класс), чтобы иметь обратную совместимость с существующим кодом. Мы были бы безопасны в обоих случаях.

Теперь давайте сосредоточимся на четвертом методе notifyFeedCouldBeDownloaded (): это тот, который уведомляет о том, что данные могут быть загружены после подтверждения. Как видите, у него есть аргумент, называемый DownloadConfirmation. Это еще один обратный вызов:

    public static interface DownloadConfirmation
      {
        public void confirmDownload();
      }

Представленный единственный метод verifyDownload () позволяет завершить операцию (конечно, если пользователь отменяет операцию, метод не будет вызван). doDownload () снова работает асинхронно, возвращаясь немедленно (поэтому он может быть вызван потоком пользовательского интерфейса); Когда операция будет завершена с положительным или отрицательным результатом, начальный экземпляр AvailabilityNotifier будет уведомлен снова. Таким образом, вместо того, чтобы выставлять getNewsFeedWithoutConfirmation () в NewsService, предупреждая, что он не должен вызываться, но при некоторых обстоятельствах, метод verifyDownload () доступен только для объекта, который материализуется и, следовательно, может быть вызван, именно в этих обстоятельствах.

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

Следует отметить, что реализация DownloadConfirmation может даже обеспечивать проверку в confirmDownload (), поэтому его можно назвать только один раз в случае (и, возможно, в течение определенного интервала времени). Это предотвратило бы трюки, такие как хранение ссылки на DownloadConfirmation вызвать confirmDownload () несколько раз во время других взаимодействий. Конечно, мы не можем автоматически обеспечивать надлежащее выполнение в NewsViewController (после того, как все это может просто делать вид, что пользователь подтвердил, не требуя ничего), но мы сделали некоторые разумные усилия, чтобы избежать некоторых тривиальных ошибок.

Ниже приведена схема API новостей, где вы можете увидеть отношения между классами, описанными до сих пор:

Теперь мы можем взглянуть на реализацию метода getNewsFeed () в NewsService (я не показываю полный список, но имена методов самоописываются):

    @Override
    public void getNewsFeed (final @Nonnull AvailabilityNotifier availabilityNotifier)
      {

        try
          {
            availabilityNotifier.notifyFeedAvailable(cache.getRssFeed());
          }
        catch (NotFoundException e)
          {
            ensureCacheIsInitialized(availabilityNotifier);
            
            if (!cache.getStatus().isDownloadNeeded())
              {
                readNewsFeedAndNotifyAvailability(availabilityNotifier);                    
              }
            else if (preferences.get().isNewsDownloadAllowed())
              {
                downloadNewsFeedAndNotifyAvailability(availabilityNotifier);
              }
            else
              {
                availabilityNotifier.notifyFeedCouldBeDownloaded(new DownloadConfirmation() 
                  {
                    public void confirmDownload() 
                      {
                        downloadNewsFeedAndNotifyAvailability(availabilityNotifier);
                      }
                  });
              }
          }
      }

Ранее мы говорили, что мы можем думать об этом проекте как об асинхронной передаче сообщений: каждое взаимодействие представлено сообщением (вызовом метода обратного вызова), которое несет объект (AvailabilityNotifier и DownloadConfirmation), который представляет состояние в определенной точке взаимодействия. , Этот дизайн представляет некоторые сходства с техникой, называемой продолжением стиля прохождения (CPS) , В то время как в прямом функциональном стиле подпрограмма просто возвращает значение, с помощью CPS вызывающая сторона передает объект, называемый «продолжением», подпрограмме, и этот объект получит результат подпрограммы и решит, как поступить. В нашем случае обратные вызовы AvailabilityNotifier и DownloadConfirmation делают работу, очень похожую на продолжения. Я говорю «похоже», поскольку CPS работает с более строгими правилами и с более глубокими последствиями; в любом случае, вероятно, правильно сказать, что некоторые идеи в дизайне, описанном в этой статье, были вдохновлены CPS.

Использование продолжений в пользовательском интерфейсе было впервые описано в статье Продолжения пользовательского интерфейса (Деннис Куан, Дэвид Хейн, Дэвид Р. Каргер, Роберт Миллер), где авторы указали, как их можно использовать для эффективного представления потока управления. для взаимодействия между пользователем и пользовательским интерфейсом. Имеет смысл процитировать реферат, так как он описывает требования к дизайну, которые были представлены в предыдущем примере:

«Диалоговые окна, которые собирают параметры для команд, часто создают эфемерные, неестественные прерывания нормального потока выполнения программы, побуждая пользователя завершить диалоговое окно как можно быстрее, чтобы программа могла обработать эту команду. В этой статье мы рассмотрим идею преобразования акта сбора параметров от пользователя в объект первого класса, называемый продолжением пользовательского интерфейса. Программы могут создавать продолжения пользовательского интерфейса, определяя, какая информация должна быть получена от пользователя, и предоставляя обратный вызов (т. Е. Продолжение), который должен быть уведомлен о собранной информации. … Кроме того, продолжения пользовательского интерфейса, как и другие парадигмы прохождения продолжения, могут использоваться для обеспечения непрерывного выполнения программы, пока пользователь определяет параметры команды на досуге.»

В нашем предыдущем примере мы использовали продолжения между службой и контроллером представления. Давайте посмотрим на другой пример API-интерфейса BlueBill News, на этот раз между контроллером и представлением. Во-первых, давайте вспомним методы, представленные представлением:

public interface LockableView 
  {
    public void lock (@Nonnull UserNotification notification);
    
    public void unlock(); 
  }

 

public interface NewsView extends LockableView
  {
    public void bindActions (@Nonnull Action markAllMessagesAsReadAction);

    public void populate (@Nonnull PresentationModel newsFeedPM);

    public void notifyFeedUnavailable (@Nonnull UserNotificationWithFeedback notification);

    public void notifyFeedIsCached (@Nonnull UserNotification notification);
    
    public void notifyAllMessagesMarkedAsRead (@Nonnull UserNotification notification);

    public void confirmToDownloadNews (@Nonnull UserNotificationWithFeedback notification);
  }

Представление спроектировано таким образом, что оно предоставляет методы, представляющие все возможные взаимодействия с контроллером. Они, как правило:

  1. запросы на рендеринг данных (populate ());

  2. запросы на визуализацию уведомления пользователя (например, notifyFeedUnavailable ());

  3. запросы на обратную связь от пользователя (например, verifyToDownloadNews ()).

Больше ничего нет, поскольку представления являются тупыми объектами: фактически вся логика должна оставаться внутри контроллера. Реализация представления — это просто связывание с соответствующей технологией пользовательского интерфейса (Swing, Android и т. Д.), И именно эта реализация должна соблюдать ограничения потоков. Например, мы можем предположить, что реализация Swing обернет все методы в EventQueue.invokeLater (). Я дам более подробную информацию об этом в другой статье.

Теперь давайте сосредоточимся на verifyToDownloadNews (), который запрашивает подтверждение у пользователя. Опять же, этот метод является асинхронным и имеет аргумент обратного вызова, который представляет состояние в этой точке потока управления: UserNotificationWithFeedback, объект, который содержит что-то для отображения на дисплее (например, вопрос «Хотите ли вы подключиться?») и т. д.) и предоставляет методы verify () и cancel (), представляющие ответ пользователя. Ответственность за представление заключается в том, чтобы связать это продолжение с диалоговым окном с помощью кнопок «ОК» / «Отмена», чтобы ответ пользователя соответствующим образом передавался в NewsViewController. Подробная информация о реализации UserNotificationWithFeedback будет дана в следующей статье, но мы можем просто повторить представленные операции:

В этих условиях реализация взаимодействия с помощью NewsViewController в его методе showNewsFeed () проста, и код очень удобен для чтения (забавное одиночное подчеркивание _ является просто ярлыком для NewsViewController.class, который передается в ресурс Java). обработчик пакета без загрязнения читабельности):

    private final NewsService.AvailabilityNotifier availabilityNotifier = new NewsService.AvailabilityNotifier() 
      {
        public void notifyFeedAvailable (final @Nonnull RssFeed newsFeed) 
          {
            populateAndUnlockView(newsFeed);
          }

        public void notifyCachedFeedAvailable (final @Nonnull RssFeed newsFeed) 
          {
            populateAndUnlockView(newsFeed);
            view.notifyFeedIsCached(notification().withText(_, "obsoleteNews"));
          }

        public void notifyFeedUnavailable() 
          {
            markAllMessagesAsReadAction.setEnabled(false);
            view.unlock();
            view.notifyFeedUnavailable(notificationWithFeedback().withCaption(_, "unavailableNewsTitle")
                                                                 .withText(_, "unavailableNewsMessage")
                                                                 .withFeedback(new Feedback()
              {
                @Override
                public void onConfirm() 
                  {
                    flowController.finish();
                  }
              }));
          }

        public void notifyFeedCouldBeDownloaded (final @Nonnull NewsService.DownloadConfirmation confirmation) 
          {
            view.confirmToDownloadNews(notificationWithFeedback().withCaption(_, "confirmDownloadTitle")
                                                                 .withText(_, "confirmDownloadMessage")
                                                                 .withFeedback(new Feedback()
              {
                @Override
                public void onConfirm()
                  {
                    confirmation.confirmDownload();
                  }

                @Override
                public void onCancel()
                  {
                    view.unlock();
                    flowController.finish();
                  }
              }));
          }
      };

    @Nonnull
    public void showNewsFeed()
      {
        view.lock(notification().withText(_, "preparingNews")); 
        newsService.get().getNewsFeed(availabilityNotifier);
      }

Стоит посмотреть, как реализованы тесты. Ниже приведен фрагмент теста для DefaultNewsViewController, реализации NewsViewController по умолчанию:

public class DefaultNewsViewControllerTest 
  {
    
    private DefaultNewsViewController fixture;
    
    private NewsView view;
    
    private NewsService newsService;
    
    private NewsService.DownloadConfirmation downloadConfirmation;
    
    private FlowController flowController;
    
    @BeforeMethod
    public void setupFixture() 
      {
        view = mock(NewsView.class);
        flowController = mock(FlowController.class);
        newsService = mock(NewsService.class);
        downloadConfirmation = mock(NewsService.DownloadConfirmation.class);
        fixture = new DefaultNewsViewController(view, flowController);
      }
    
    @Test(timeOut=2000)
    public void showNewsFeed_must_start_downloading_when_the_news_feed_can_be_downloaded_and_the_user_confirms()
      throws Exception
      {
        doAnswer(notifyFeedCouldBeDownloaded()).when(newsService).getNewsFeed(any(AvailabilityNotifier.class));
        doAnswer(confirm()).when(view).confirmToDownloadNews(any(UserNotificationWithFeedback.class));

        fixture.showNewsFeed();
        waitForViewInteraction();
        
        inOrder.verify(view).lock(argThat(notification("", "Loading news...")));        
        inOrder.verify(newsService).getNewsFeed(any(AvailabilityNotifier.class));
        inOrder.verify(view).confirmToDownloadNews(argThat(notificationWithFeedback("Confirmation", "Please confirm that you want to download the latest news.")));
        inOrder.verify(downloadConfirmation).confirmDownload(); 
        verifyNoMoreInteractions(view, flowController, newsService, downloadConfirmation);
      }
    
    @Test(timeOut=2000)
    public void showNewsFeed_must_dismiss_the_view_when_the_news_feed_can_be_downloaded_and_the_user_cancels()
      throws Exception
      {
        doAnswer(notifyFeedCouldBeDownloaded()).when(newsService).getNewsFeed(any(AvailabilityNotifier.class));
        doAnswer(cancel()).when(view).confirmToDownloadNews(any(UserNotificationWithFeedback.class));

        fixture.showNewsFeed();
        waitForViewInteraction();
        
        inOrder.verify(view).lock(argThat(notification("", "Loading news...")));        
        inOrder.verify(newsService).getNewsFeed(any(AvailabilityNotifier.class));
        inOrder.verify(view).confirmToDownloadNews(argThat(notificationWithFeedback("Confirmation", "Please confirm that you want to download the latest news.")));
        inOrder.verify(view).unlock();
        inOrder.verify(flowController).finish();
        verifyNoMoreInteractions(view, flowController, newsService, downloadConfirmation);
      }
  }

FlowController не был описан в этой статье (будет представлен в будущем), в любом случае просто имейте в виду, что это навигационный контроллер, а его метод finish () означает, что текущий вид должен быть отклонен (что бы это ни значило для конкретного Технология пользовательского интерфейса, используемая для реализации).

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

В тесте используются TestNG , Mockito и несколько пользовательских классов Mockito для обеспечения некоторого синтаксического сахара; в частности, notifyFeedCouldBeDownloaded (), verify () и cancel () являются реализациями Mockito’s Answer для имитации поведения обратных вызовов. Например. вот список одного из этих ответов:

    @Nonnull
    private Answer<Void> notifyFeedCouldBeDownloaded()
      {
        return new Answer<Void>()
          {
            public Void answer (final @Nonnull InvocationOnMock invocation) 
              {
                final AvailabilityNotifier notifier = (AvailabilityNotifier)invocation.getArguments()[0];
                notifier.notifyFeedCouldBeDownloaded(downloadConfirmation);
                return null;
              }
          };
      }    

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

Другая статья даст более подробную информацию о реализации представления.