Объем функций обратного вызова, которые изменяют переменные экземпляра в Dart

Хотя вопросы такого рода задают часто, я думаю, что у меня есть более конкретное ограничение, которое делает проблему немного более интересной. Я пишу клиентское приложение на Dart, используя шаблон MVC. Моя цель проста: прослушивать клики по кнопке, запускать асинхронный запрос к внутреннему API и представлять эти данные пользователю.

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

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

// view.dart
class FooView {
  // The root element of the view with which we're concerned.
  static final Element root = querySelector('#thisView');

  FooView() { init(); }

  void init() { root.hidden = false; }

  // Appends the new data into an unordered list.
  void update(List<Map<String,String>> list) {
    UListElement container = root.querySelector('ul#dataContainer');
    container
      ..hidden = true
      ..children.clear();
    for ( Map<String,String> item in list ) {
      container.append(new LIElement()
        ..id = item['id']
        ..text = item['text']
      );
    container.hidden = false;
  }


// model.dart
class FooModel {
  // Instance variable to hold processed data from the async request.
  List<Map<String,String>> dataList;

  // Makes async request, returning data to caller.
  List<Map<String,String>> getData() {
    HttpRequest
      .getString('example.com/api/endpoint')
      .then( (String data) {
        dataList = JSON.decode(data);
      });
    return dataList;
  }
}


// controller.dart
class FooController {
  FooModel model;
  FooView view;

  FooController() {
    model = new FooModel;
    view = new FooView;
  }

  void registerHandlers() {
    // When this button is clicked, the view is updated with data from the model.
    ButtonElement myButton = view.root.querySelector('#myButton');
    myButton.onClick.listen( (Event e) {
      view.update(model.getData());
    });
  }
}

Ошибки, которые я вижу, связаны с появлением поля model.dataList null в конце всего этого. Мое первое замечание заключается в том, что я не понимаю области действия функций обратного вызова. Как я сначала понял, обратный вызов будет обрабатывать данные запроса, когда они будут получены, и просто установит переменную экземпляра, когда они будут готовы. Возможно, переменная экземпляра имеет псевдоним и модифицируется в рамках обратного вызова, но переменная, которую я хочу вернуть, никогда не затрагивается.

Я подумал о передаче объекта Future методу представления, который затем просто выполнит обработку и добавит элементы в DOM в качестве побочного эффекта. Этот метод сломал бы мой дизайн MVC (даже больше, чем сейчас в этом минимальном рабочем примере).

Также очень возможно, что я использую асинхронное программирование совершенно неправильно. Если подумать об этом подробнее, то мой асинхронный вызов бесполезен, потому что я в основном делаю блокирующий вызов view.update() в контроллере при возникновении события. Возможно, мне следует передать запрос Future контроллеру и запустить оттуда then() метод запроса при срабатывании обработчика событий.

В какой области Dart находятся функции обратного вызова и как я могу получить из них данные с минимальными побочными эффектами и максимальной инкапсуляцией?

Примечание. Мне не нравится вдаваться в подробности этого часто обсуждаемого вопроса, но я прочитал предыдущие ответы на подобные вопросы безрезультатно.


person Community    schedule 21.07.2014    source источник
comment
Dart использует зоны для обработки контекста: dartlang.org/articles/zones   -  person JAre    schedule 22.07.2014
comment
Я еще не читал ваши ответы и ваш вопрос не очень внимательно, но мне интересно, почему у вас есть HttpRequest в модели. Думаю, это должен делать контролер.   -  person Günter Zöchbauer    schedule 22.07.2014
comment
@ GünterZöchbauer Я думаю, если у вас есть пара реализаций модели. Например, один получает данные с сервера, другой - из локального хранилища. И оба предоставляют общий интерфейс доступа к данным, тогда контроллеру (ведущему) не нужно знать специфику источника данных.   -  person JAre    schedule 22.07.2014
comment
IMHO модель может содержать информацию, откуда загружать данные и куда сохранять, но контроллер должен загружать / сохранять. Другой вопрос: рассматривали ли вы возможность использования Angular или Polymer для связывания модели и представления?   -  person Günter Zöchbauer    schedule 22.07.2014
comment
@ GünterZöchbauer Я согласен с вами в установке HttpRequest в контроллер. Как только я это сделал, у меня есть контроллер, который обрабатывает Future и обновляет представление по завершении. Теперь модель просто содержит объекты, которые используются для визуализации представления, вместо того, чтобы включать логику для выполнения запросов. Я думал об использовании Angular или Polymer, но это мое первое приложение для Dart, и я хотел бы сначала изучить основы.   -  person    schedule 23.07.2014


Ответы (3)


Вы возвращаете datalist в getData до того, как HttpRequest вернется.

  // Makes async request, returning data to caller.
  List<Map<String,String>> getData() {
    return HttpRequest                               // <== modified
      .getString('example.com/api/endpoint')
      .then( (String data) {
        return JSON.decode(data);                    // <== modified
      });
    // return dataList;                              // <== modified


  void registerHandlers() {
    // When this button is clicked, the view is updated with data from the model.
    ButtonElement myButton = view.root.querySelector('#myButton');
    myButton.onClick.listen( (Event e) {
      model.getData().then((data) => view.update(data));  // <== modified
    });
  }
person Günter Zöchbauer    schedule 22.07.2014
comment
Это решение работает. В итоге я последовал вашему предложению перенести логику запроса на контроллер, что позволило мне быть по-настоящему асинхронным! Спасибо за помощь! - person ; 23.07.2014

Метод getData инициирует асинхронный HTTP-запрос, а затем немедленно возвращается до получения / анализа ответа. Вот почему model.datalist это null.

Чтобы сделать эту работу с минимальными усилиями, вы можете сделать getData синхронным: (примечание: я изменил тип dataList, просто чтобы он работал с образцом службы JSON http://ip.jsontest.com/)

// model.dart
class FooModel {

  // Instance variable to hold processed data from the async request.
  Map<String, String> dataList;

  // Makes async request, returning data to caller.
  Map<String, String> getData() {
    var request = new HttpRequest()
      ..open('GET', 'http://ip.jsontest.com/', async: false)
      ..send();
    dataList = JSON.decode(request.responseText);
    return dataList;
  }
}

Хотя это может нарушить вашу цель, я согласен с вашими опасениями по поводу вызова блокировки и лично рассмотрел бы возможность сохранения асинхронности HTTP-запроса и того, чтобы getData возвращал новое будущее, которое ссылается на ваш класс модели или проанализированные данные. Что-то вроде:

// model.dart
class FooModel {

// Instance variable to hold processed data from the async request.
Map<String,String> dataList;

// Makes async request, returning data to caller.
Future<Map<String, String>> getData() {
  return HttpRequest
    .getString('http://ip.jsontest.com/')
    .then( (String data) {
      dataList = JSON.decode(data);
      return dataList;
    });
  }
}

и в контроллере:

void registerHandlers() {

  // When this button is clicked, the view is updated with data from the model.
  ButtonElement myButton = FooView.root.querySelector('#myButton');
  myButton.onClick.listen( (Event e) {
    model.getData().then((Map<String, String> dataList) {
      view.update(dataList);
    });
  });
}
person Cristian Almstrand    schedule 22.07.2014
comment
Это похоже на мой ответ. Здесь вы видите, что вам не нужно использовать Completer для таких простых случаев. Просто убедитесь, что каждому асинхронному вызову предшествует return и что then(xxx) возвращает значение результата. Завершитель предназначен для случаев, когда цепочка асинхронных вызовов не может быть соединена от начала до конца. Например, когда вы вызываете метод, который возвращает Future, и завершение должно выполняться в другом методе (например, в обработчике событий - когда событие было получено, мы знаем, что процедура async завершена). - person Günter Zöchbauer; 22.07.2014
comment
Согласовано. Я соответственно обновил пример, что делает асинхронное решение двух ответов идентичным. Рассмотрим исходную версию Гюнтера, проголосовавшую за него :) - person Cristian Almstrand; 22.07.2014

Вы можете использовать Stream, чтобы сделать ваш дизайн слабосвязанным и асинхронным:

class ModelChange {...}  
class ViewChange {...}

abstract class Bindable<EventType> {
  Stream<EventType> get updateNotification;
  Stream<EventType> controllerEvents;
}

class Model implements Bindable<ModelChange> {
  Stream<ModelChange> controllerEvents;
  Stream<ModelChange> get updateNotification => ...
}

class View implements Bindable<ViewChange> {
  Stream<ViewChange> controllerEvents;
  Stream<ViewChange> get updateNotification => ...
}    

class Controller {    
  final StreamController<ViewChange> viewChange = new StreamController();
  final StreamController<ModelChange> modelChange = new StreamController();  

  Controller.bind(Bindable model, Bindable view) {
    view.controllerEvents = viewChange.stream;
    model.controllerEvents = modelChange.stream;
    view.updateNotification.forEach((ViewChange vs) {
      modelChange.add(onViewChange(vs));
    });
    model.updateNotification.forEach((ModelChange mc) {
      viewChange.add(onModelChange(mc));
    });
  }
  ModelChange onViewChange(ViewChange vc) => ...
  ViewChange onModelChange(ModelChange mc) => ...
}
person JAre    schedule 22.07.2014