Форум программистов «Весельчак У»
  *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

  • Рекомендуем проверить настройки временной зоны в вашем профиле (страница "Внешний вид форума", пункт "Часовой пояс:").
  • У нас больше нет рассылок. Если вам приходят письма от наших бывших рассылок mail.ru и subscribe.ru, то знайте, что это не мы рассылаем.
   Начало  
Наши сайты
Помощь Поиск Календарь Почта Войти Регистрация  
 
Страниц: [1]   Вниз
  Печать  
Автор Тема: Как придерживаться односторонней связи между классами?  (Прочитано 32714 раз)
0 Пользователей и 1 Гость смотрят эту тему.
neco
Участник

kz
Offline Offline

« : 03-04-2010 09:28 » 

Есть пожизненная проблема. Мои классы всегда почему-то хотят знать друг о друге и меня это расстраивает. Не могу понять, как надо начать мыслить, чтобы такого не получалось.
Далее я немного углублюсь в подробности (можете пропустить), сам вопрос после этой секции.
==============
Например, сейчас пишу StatefulControl для asp.net. Смысл контрола в том, чтобы упростить создание визуальных контролов, которые переходят из состояния в состояние. К примеру, поисковик людей имеет три состояния - состояние поиска (текстбокс, кнопка "найти"), состояние результатов поиска (таблица с найденными людьми с возможностью выбора одного из них) и состояние, когда сотрудник выбран (span с именем выбранного сотрудника и кнопка "изменить", возвращающая контрол в первое состояние).
Кто писал контролы, переходящие из состояния в состояние на asp.net, тот знает, что там есть кое-какая закавыка с тем, что чтобы сменить состояние, надо сначала восстановить предыдущее состояние полностью, потом словить событие (кнопки, например), очистить текущий набор созданных контролов и создать новый набор, основываясь на новом состоянии.
Когда это пишешь где-то так третий раз, становится уже не смешно и хочется вынести общую часть раз и навсегда.
==============
Вначале начал создавать диаграммку, потом нервы сдали и стал писать сразу кодом, поскольку фантазии не хватает, представить какие проблемы будут в коде - как-то проще представить диаграмму по коду.
Первым классом идёт StatefulControl
Код:
    public abstract class StatefulControl : CompositeControl {
        private StateManager _state_mode_manager;

        public StatefulControl() {
            _state_mode_manager = new StateManager(CreateProvider());
        }
        protected override void OnInit(EventArgs e) {
            base.OnInit(e);
            this.Page.RegisterRequiresControlState(this);
        }
        protected override void OnLoad(EventArgs e) {
            base.OnLoad(e);
        }
        protected override void CreateChildControls() {
            _state_mode_manager.RestoreStateByName(_state.SavedStateModeName);
            this.Controls.Add(_state_mode_manager.GetRootControl());
        }
        public override void RenderControl(System.Web.UI.HtmlTextWriter writer) {
            _state_mode_manager.RenderControls(writer);
        }

        private StatefulControlState _state;
        protected override void LoadControlState(object savedState) {
            _state = (StatefulControlState)savedState;
        }
        protected override object SaveControlState() {
            _state.SavedStateModeName = _state_mode_manager.CurrentStateMode.StateModeName;
            return _state;
        }
        protected abstract IStateModesProvider CreateProvider();
    }
Фактически целью этого класса является упростить конечный класс (чтобы в него это не пихать). В общем-то просто даёт команды StateManager'у в нужный момент.

Собственно сам StateManager:
Код:
    class StateManager {
        IStateModesProvider _provider;
        public StateManager(IStateModesProvider provider) {
            _provider = provider;
            _root_control = new Control();
        }
        public void RestoreStateByName(string name) {
            _current_state_mode = _provider.GetStateModeByName(name);
            _current_state_mode.CreateControlsOnRestoring();
            _root_control.Controls.Clear();
            _root_control.Controls.Add(_current_state_mode);
        }
        public void SwitchToState(BaseStateModeControl state) {
            _current_state_mode = state;
            _current_state_mode.CreateControlsOnStateChanging();
            _root_control.Controls.Clear();
            _root_control.Controls.Add(_current_state_mode);
        }
        private Control _root_control;
        public Control GetRootControl() {
            return _root_control;
        }
        public void RenderControls(HtmlTextWriter writer) {
            _root_control.RenderControl(writer);
        }

        private BaseStateModeControl _current_state_mode;
        public BaseStateModeControl CurrentStateMode {
            get {
                return _current_state_mode;
            }
        }
    }
Он у меня переводит управляет состояниями - т.е. принимает команду на смену состояния и меняет его должным образом.
Ещё он у меня стал хранителем корневого контрола - это произошло случайно, когда я развязывал связь между StateManager'ом и StatefuControl'ом. Раньше я сообщал StatefulControl StateManager'у (в классе-обёртке), чтобы он потом передал его конечным состояниям, чтобы они в него пихали свои контролы (текстбоксы, кнопки). Т.е. StateManager и StatefulControl фактически знали друг о друге. Чтобы сделать связь односторонней я добавил StateManager'у метод GetRootControl и теперь у него StatefulControl сам спрашивает корневой элемент.
Честно говоря до реального теста ещё не дошёл, подозреваю, что может не сработать, поэтому и лишнюю ответственность из StateManager'а убирать не спешу - возможно придётся переделывать.

Конечные состояния (с кнопками и текстбоксами) будут создаваться провайдером состояний IStateModesProvider, который будет создан фабричным методом в StatefulControl'е и передан StateManager'у в конструкторе.

А вот дальше у меня возник вопрос.
Дело в том, что я хочу, чтобы мои конечные состояния сами управляли переходом в другие состояния (это удобно в конечном итоге при использовании всей этой структуры - использовать события кнопок, гридов, чтобы менять состояние, прямо внутри этих событий). И здесь неизбежны (на мой взгляд) два варианты:
- они должны посылать эту команду напрямую StateManager'у. Тогда StateManager должен сообщить себя провайдеру состояний, а он в свою очередь сообщит его конечным состояниям.
- они будут посылать команду провайдеру, а он будет пересылать её StateManager'у.
И в том и в другом случае налицо появление встречных зависимостей. На данный момент вроде как очевидно, что StateManager должен знать о провайдере и состояниях (их интерфейсах), а теперь ещё появляется встречная связь - провайдер и/или состояния тоже знают про менеджера.
И всё бы ничего, но я уже пару раз обжёгся на таких связях, поскольку появляется необходимость при кодинге одного класса помнить, какой этап жизни переживает другой класс. Т.е. например, я начну в конструкторе StateManager'а сообщать Provider'у самого себя:
Код:
        public StateManager(IStateModesProvider provider) {
            _provider = provider;
            _provider.Manager = this;
            _root_control = new Control();
        }
а конкретный провайдер у меня был уже создан в фабричном методе, поэтому в конструкторе провайдера я не смогу передать менеджера конечным состояниям (поскольку мне его ещё не выставили).
Получается, что я должен буду добавить метод для инициализации конечных состояний:
Код:
        public StateManager(IStateModesProvider provider) {
            _provider = provider;
            _provider.Manager = this;
            _provider.InitManagerToStates();
            _root_control = new Control();
        }
или просто
Код:
        public StateManager(IStateModesProvider provider) {
            _provider = provider;
            _provider.Manager = this;
            _provider.InitStates();
            _root_control = new Control();
        }
но уже создавать конечные состояния я буду только в методе InitStates() и буду рассчитывать, что в этом методе менеджер у меня уже будет.
А потом в ходе метода InitStates выяснится, что я не могу по полной использовать Manager'а, поскольку его _root_control ещё не был проинициализирован. Конечно, я могу пойти и переставить их местами, но это уже танцы с бубнами - такое рано или поздно подводит (или забываешь, или перестановки начинают конфликтовать).

и вот это меня и смущает - получается в любой момент времени, я должен помнить о том, как себя чувствует одно из свойств - проинициализировано оно или нет.
Подозреваю, что проблема именно во встречных зависимостях. Если бы зависимости были строго древовидными, то процесс инициализации был бы строго определён и был бы не настолько важен, либо очевиден.
Кроме того, я вижу у себя нарушение заповеди "делать в конструкторе как можно меньше" - я про такое слышал и нутром чувствую, что в конструкторе надо только назначать присланные "свыше" переменные своим полям. И уж точно никаких операторов new (как у меня в конструкторе StatefulControl'а, например) быть и подавно не должно. Но не могу от них избавиться - если тащу всё наверх, то становится слишком много зависимостей между классами (через конструкторы) и всё превращается в кашу (по крайней мере на диаграмме).

Помогите, а? Советом каким-нибудь, или может таблетки какие попить?
Как избегать таких проблем? И как решить конкретно эту?

Спасибо!

p.s. поскольку текста много, ещё раз просуммирую вышесказанное.
StatefulControl, занимается спецификой asp.net, знает о StateManager'е.
StateManger, занимается переводом целевого контрола из одного состояния в другое, знает о StateProvider'е, чтобы работать через него с конечными состояниями.
StateProvider, инициализирует и хранит в себе конечные состояния.
Классы конечных состояний - классы, которые будут содержать в себе визуальные компоненты (текстбоксы, кнопки, гриды) и заниматься непосредственно тем, чем надо для данного контрола.
Записан

Всю ночь не ем, весь день не сплю - устаю
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« Ответ #1 : 03-04-2010 17:15 » 

Цитата: neco
Как избегать таких проблем?
Чётко видеть решаемую проблему.

Классы - это всё следствия. И диаграммы не помогут. Начнём с исходного: контролы ASP.NET.

1) В чём заключается проблема существующих контролов? (Одним предложением)
2) Что из тобою предпринятого и каким образом эту проблему решает?

(Кстати, такая трассировка требований до кода и в RUP имеется, но в соседней теме ты её пропустил.)

P.S. Я видел нечто отдалённо похожее на формулировку проблемы в этом месте:
Цитата: neco
Кто писал контролы, переходящие из состояния в состояние на asp.net, тот знает, что там есть кое-какая закавыка с тем, что чтобы сменить состояние, надо сначала восстановить предыдущее состояние полностью, потом словить событие (кнопки, например), очистить текущий набор созданных контролов и создать новый набор, основываясь на новом состоянии.
Когда это пишешь где-то так третий раз, становится уже не смешно и хочется вынести общую часть раз и навсегда.
Я писал такие контролы и, единожды поняв, как программируется "закавыка", более никогда проблем не испытывал. В общем и целом ты сказал правильно: восстановить состояние, принять события и сформировать новое состояние.

Поскольку твои контролы и состояния разнообразы по структуре, вся эта структура и связанная с ней логика будет находиться в классах реализации. Непонятно, от чего ты пытаешься избавиться, нагородив эти абстрактные классы.
Цитата: neco
но уже создавать конечные состояния я буду только в методе InitStates() и буду рассчитывать, что в этом методе менеджер у меня уже будет.
А потом в ходе метода InitStates выяснится, что я не могу по полной использовать Manager'а, поскольку его _root_control ещё не был проинициализирован. Конечно, я могу пойти и переставить их местами, но это уже танцы с бубнами - такое рано или поздно подводит (или забываешь, или перестановки начинают конфликтовать).

и вот это меня и смущает - получается в любой момент времени, я должен помнить о том, как себя чувствует одно из свойств - проинициализировано оно или нет.
Подозреваю, что проблема именно во встречных зависимостях.
Т.е. в результате вся логика работы исходного контрола развалилась? Но какая проблема при этом пыталась решаться?..
« Последнее редактирование: 03-04-2010 17:34 от Dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
neco
Участник

kz
Offline Offline

« Ответ #2 : 03-04-2010 18:43 » 

Цитата: Dimka
1) В чём заключается проблема существующих контролов? (Одним предложением)
В том, что код всех состояний в них намешан в одном классе и это затрудняет их поддержку (да и написание тоже).

Цитата: Dimka
2) Что из тобою предпринятого и каким образом эту проблему решает?
Основная задача - распилить все состояния по разным классам. На данный момент мною предпринятые шаги с грехом пополам решают данную проблему.
Также задачей является упростить создание таких компонентов - т.е. различную требуху (механику восстановления предыдущего состояния и переход в следующее) написать один раз и благополучно забыть. Но при этом немаловажная задача - не добавить новую требуху. Вот с этим беда. Хочется, чтобы при написании нового стэйт-контрола уже не надо было помнить о том, как писался код базовой компоненты - чтобы использование было интуитивным и не давало возможности ошибиться.

Т.е. например то, что при наследовании от абстрактного класса StatefulControl я буду вынужден реализовать фабричный метод для создания провайдера состояний - для меня интуитивный шаг.
Создание самого провайдера и состояний тоже должно быть по возможности как можно проще. При всём при этом хотелось бы поменьше использовать наследование как более жёсткий способ повторного использования кода.

Цитата: Dimka
Поскольку твои контролы и состояния разнообразы по структуре, вся эта структура и связанная с ней логика будет находиться в классах реализации.
У меня пока структура состояний (каждого из них) довольно однообразна. Каждый имеет набор контролов, которые имеют события. События либо меняют что-то в этом же состоянии, либо инициируют переход в следующее состояние передавая какой-нибудь параметр новому состоянию.

Цитата: Dimka
Непонятно, от чего ты пытаешься избавиться, нагородив эти абстрактные классы.
Могу вот привести код класса, в котором я не парился над кахиженом и решал задачу в лоб:
Код:
    [ValidationProperty("SelectedAppId")]
    public abstract class CareSearchCtl : CompositeControl {
        private MyState _state = new MyState();
        // SearchStart
        private Panel _pnl_for_txt;
        private TextBox _txt_for_search;
        private Button _btn_go;
        // SearchResultView
        private GridView _dg_results;
        // SearchCompleted
        private Label _lbl_selected;
        private LinkButton _btn_search_again;
        private LinkButton _btn_clear;

        #region Create and Destroy Controls
        private void cc_PanelForTextBox() {
            if (_pnl_for_txt == null) {
                _pnl_for_txt = new Panel();
                _pnl_for_txt.ID = "pnl";
                _pnl_for_txt.Attributes.Add("style", "display:inline");
                cc_ButtonGo();
                _pnl_for_txt.DefaultButton = _btn_go.ID;

                Controls.Add(_pnl_for_txt);
            }
        }
        private void cd_PanelForTextBox() {
            if (_pnl_for_txt != null) {
                Controls.Remove(_pnl_for_txt);
                _pnl_for_txt = null;
            }
        }
        private void cc_TextForSearch() {
            if (_txt_for_search == null) {
                _txt_for_search = new TextBox();
                _txt_for_search.ID = "txt_for_search";
                _txt_for_search.Text = _state.SelectedAppName;
                _txt_for_search.CssClass = "psc_input";

                cc_PanelForTextBox();
                _pnl_for_txt.Controls.Add(_txt_for_search);
            }
        }
        private void cd_TextForSearch() {
            if (_txt_for_search != null) {
                Controls.Remove(_txt_for_search);
                _txt_for_search = null;
            }
        }
        private void cc_ButtonGo() {
            if (_btn_go == null) {
                _btn_go = new Button();
                _btn_go.ID = "btn_go";
                _btn_go.Text = GetMLString("Go");
                _btn_go.CssClass = "psc_button_go";
                _btn_go.Click += new EventHandler(_btn_go_Click);
                Controls.Add(_btn_go);

                //Panel pnl = new Panel();
                //pnl.ID = "asdasd";
                //pnl.DefaultButton = _btn_go.ID;
                //Controls.Add(pnl);
            }
        }
        private void cd_ButtonGo() {
            if (_btn_go != null) {
                Controls.Remove(_btn_go);
                _btn_go = null;
            }
        }
        private void cc_DataGrid() {
            if (_dg_results == null) {
                _dg_results = new GridView();
                _dg_results.ID = "grid";
                _dg_results.AutoGenerateColumns = false;
                _dg_results.EnableViewState = false;
                _dg_results.EmptyDataText = "No application found. Try again or enter custom name.";
                // binding setup

                ButtonField clmn_badge = new ButtonField();
                clmn_badge.CommandName = "select";
                clmn_badge.DataTextField = "Id";
                clmn_badge.HeaderText = GetMLString("Id");
                _dg_results.Columns.Add(clmn_badge);

                BoundField clmn_name = new BoundField();
                clmn_name.DataField = "Name";
                clmn_name.HeaderText = GetMLString("Name");
                _dg_results.Columns.Add(clmn_name);

                HyperLinkField clmn_link = new HyperLinkField();
                clmn_link.HeaderText = GetMLString("GIL Link");
                clmn_link.DataNavigateUrlFormatString = "http://optionspanel.chevron.com/versiondetail.aspx?action=install&airID={0}&AppType=P";
                clmn_link.DataNavigateUrlFields = new string[] { "AirId" };
                clmn_link.DataTextFormatString = "{0}";
                clmn_link.DataTextField = "AirId";
                _dg_results.Columns.Add(clmn_link);

                _dg_results.SelectedIndexChanged += new EventHandler(_dg_results_SelectedIndexChanged);
                Controls.Add(_dg_results);
            }
        }
        private void cd_DataGrid() {
            if (_dg_results != null) {
                Controls.Remove(_dg_results);
                _dg_results = null;
            }
        }
        private void cc_LabelSelected() {
            if (_lbl_selected == null) {
                _lbl_selected = new Label();
                _lbl_selected.ID = "lbl_selected";
                ApplySelectedApplication();
                Controls.Add(_lbl_selected);
            }
        }
        private void cd_LabelSelected() {
            if (_lbl_selected != null) {
                Controls.Remove(_lbl_selected);
                _lbl_selected = null;
            }
        }
        private void cc_ButtonSearch_Again() {
            if (_btn_search_again == null) {
                _btn_search_again = new LinkButton();
                _btn_search_again.ID = "btn_again";
                _btn_search_again.Style.Add("margin-left", "10px");
                _btn_search_again.Text = GetMLString("Change");
                _btn_search_again.Click += new EventHandler(_btn_search_again_Click);
                _btn_search_again.Enabled = !IsReadOnly;
                Controls.Add(_btn_search_again);
            }
        }
        private void cd_ButtonSearch_Again() {
            if (_btn_search_again != null) {
                Controls.Remove(_btn_search_again);
                _btn_search_again = null;
            }
        }
        private void cc_ButtonClear() {
            if (_btn_clear == null) {
                _btn_clear = new LinkButton();
                _btn_clear.ID = "btn_clear";
                _btn_clear.Style.Add("margin-left", "10px");
                _btn_clear.Text = GetMLString("Clear");
                _btn_clear.Click += new EventHandler(_btn_clear_Click);
                _btn_clear.Enabled = !IsReadOnly;
                Controls.Add(_btn_clear);
            }
        }
        private void cd_ButtonClear() {
            if (_btn_clear != null) {
                Controls.Remove(_btn_clear);
                _btn_clear = null;
            }
        }
        private void SetupControls_SearchStart() {
            cd_LabelSelected();
            cd_ButtonSearch_Again();
            cd_ButtonClear();
            cd_DataGrid();
            cc_PanelForTextBox();
            cc_TextForSearch();
            cc_ButtonGo();
        }
        private void SetupControls_SearchResultView() {
            cd_LabelSelected();
            cd_ButtonSearch_Again();
            cd_ButtonClear();
            cc_PanelForTextBox();
            cc_TextForSearch();
            cc_ButtonGo();
            cc_DataGrid();
        }
        private void SetupControls_SearchCompleted() {
            cd_TextForSearch();
            cd_ButtonGo();
            cd_DataGrid();
            cd_PanelForTextBox();
            cc_LabelSelected();
            cc_ButtonSearch_Again();
            cc_ButtonClear();
        }
        #endregion

        #region control events
        void _btn_go_Click(object sender, EventArgs e) {
            PerformSearch();
        }
        void _dg_results_SelectedIndexChanged(object sender, EventArgs e) {
            PerformMakeSelection();
        }
        void _btn_search_again_Click(object sender, EventArgs e) {
            PerformSwitchToSearchModeAgain();
        }
        void _btn_clear_Click(object sender, EventArgs e) {
            ClearSelection();
        }
        #endregion

        protected override void OnInit(EventArgs e) {
            base.OnInit(e);
            this.Page.RegisterRequiresControlState(this);
        }

        protected override void CreateChildControls() {
            base.CreateChildControls();
            CreateStateControls();
        }
        private void CreateStateControls() {
            switch (CurrentState) {
                case StateMode.SearchStart: {
                        SetupControls_SearchStart();
                        break;
                    }
                case StateMode.SearchResultView: {
                        SetupControls_SearchResultView();
                        break;
                    }
                case StateMode.SearchCompleted: {
                        SetupControls_SearchCompleted();
                        break;
                    }
                default: throw new PeopleSearcherException("People Search Control: unknown state of control while Creating Controls!");
            }
        }
        private void PerformSearch() {
            CurrentState = StateMode.SearchResultView;
            IList<ICareApplication> lst;
            using (ICareApplicationAdapter adap = GetNewCareApplicationAdapter()) {
                lst = adap.GetAppsByName(_txt_for_search.Text);
            }
            _dg_results.DataSource = lst;
            _dg_results.DataBind();
            //if (EnableViewState) { // стал хранить в ControlState по той причине, что влом опять вытягивать всё из базы, а EnableViewState на практике должен быть выключен всю дорогу
            // сохраняем всю выборку во ViewState, чтоб потом не тянуть с базы
            SearchResult = lst;
            SearchResultSavedInViewState = true;
            //}
        }
        private void PerformMakeSelection() {
            // запоминаем выбранного чувака
            ICareApplication selected_emp;
            if (CurrentState == StateMode.SearchResultView) {
                selected_emp = GetSelectedApplication();
            } else {
                throw new PeopleSearcherException("People Search Control: before making selection control has to be in SearchResultView state!");
            }
            CurrentState = StateMode.SearchCompleted;
            // применяем выбранного чувака
            SelectedApplication = selected_emp;
            ApplySelectedApplication();
            // чистим ViewState
            SearchResult = null;
        }
        private void ApplySelectedApplication() {
            if (_lbl_selected != null) {
                _lbl_selected.Text = GetFormattedApplication();
            }
        }
        public string GetFormattedApplication() {
            ICareApplication selected_emp = SelectedApplication;
            if (selected_emp != null) {
                return string.Format("{0}", selected_emp.Name);
            } else {
                return "no application selected";
            }
        }

        private void PerformSwitchToSearchModeAgain() {
            CurrentState = StateMode.SearchStart;
        }
        private void ClearSelection() {
            SelectedApplication = null;
            ApplySelectedApplication();
            CurrentState = StateMode.SearchStart;
        }
        private ICareApplication GetSelectedApplication() {
            int idx = _dg_results.SelectedIndex;
            if (idx == -1) {
                throw new PeopleSearcherException("no one employee is selected! can not perform selection!");
            }
            //if (SearchResultSavedInViewState) {
            IList<ICareApplication> lst = SearchResult;
            return lst[idx];
            //} else {
            //    //_dg_results.SelectedItem.Cells[0]
            //    throw new NotImplementedException();
            //}
        }
        public int? SelectedAppId {
            get { return _state.SelectedAppId; }
            set { _state.SelectedAppId = value; }
        }
        public string SelectedAppName {
            get { return _state.SelectedAppName; }
            set { _state.SelectedAppName = value; }
        }
        public ICareApplication SelectedApplication {
            get {
                object rez = null;
                if (EnableViewState) {
                    rez = ViewState["selected_app"];
                }
                if (rez == null) {
                    using (ICareApplicationAdapter adap = GetNewCareApplicationAdapter()) {
                        if (_state.SelectedAppId.HasValue) {
                            rez = adap.GetOneApplicationById(_state.SelectedAppId.Value);
                        } else if (!string.IsNullOrEmpty(_state.SelectedAppName)) {
                            rez = adap.GetOneApplicationByName(_state.SelectedAppName);
                        }
                    }
                }
                return (ICareApplication)rez;
            }
            set {
                if (EnableViewState) {
                    ViewState["selected_app"] = value;
                }
                if (value != null) {
                    SelectedAppId = value.Id;
                    SelectedAppName = value.Name;
                } else {
                    SelectedAppId = null;
                    SelectedAppName = null;
                }
            }
        }
        private bool SearchResultSavedInViewState {
            get {
                object obj = ViewState["srch_rez_in_vs"];
                if (obj == null) {
                    obj = false;
                }
                return (bool)obj;
            }
            set {
                ViewState["srch_rez_in_vs"] = value;
            }
        }
        private IList<ICareApplication> SearchResult {
            get {
                object obj = _state.SearchResult;// ViewState["current_search_result"];
                if (obj == null) {
                    obj = new List<ICareApplication>();
                }
                return (IList<ICareApplication>)obj;
            }
            set {
                //ViewState["current_search_result"] = value;
                _state.SearchResult = value;
            }
        }
        public bool IsReadOnly {
            get { return _state.IsReadOnly; }
            set { _state.IsReadOnly = value; }
        }

        //protected override void RenderContents(HtmlTextWriter writer) {
        //    //base.RenderContents(writer);
        //}
        protected override void Render(HtmlTextWriter writer) {
            EnsureChildControls();
            switch (CurrentState) {
                case StateMode.SearchStart: {
                        writer.WriteBeginTag("div");
                        writer.Write(HtmlTextWriter.TagRightChar);
                        _pnl_for_txt.RenderControl(writer);
                        _btn_go.RenderControl(writer);
                        writer.WriteEndTag("div");
                        break;
                    }
                case StateMode.SearchResultView: {
                        writer.WriteBeginTag("div");
                        writer.Write(HtmlTextWriter.TagRightChar);
                        _pnl_for_txt.RenderControl(writer);
                        _btn_go.RenderControl(writer);
                        writer.WriteEndTag("div");
                        _dg_results.RenderControl(writer);
                        break;
                    }
                case StateMode.SearchCompleted: {
                        writer.WriteBeginTag("div");
                        writer.Write(HtmlTextWriter.TagRightChar);
                        _lbl_selected.RenderControl(writer);
                        _btn_search_again.RenderControl(writer);
                        _btn_clear.RenderControl(writer);
                        writer.WriteEndTag("div");
                        break;
                    }
                default: {
                        base.Render(writer);
                        break;
                    }
            }
        }

        #region Abstract entities
        protected abstract IAppContext ApplicationContext { get; }
        protected abstract ICareApplicationAdapter GetNewCareApplicationAdapter();
        #endregion

        #region StateMode
        private enum StateMode {
            SearchStart, SearchResultView, SearchCompleted
        }

        private StateMode CurrentState {
            get {
                if (IsReadOnly) {
                    return StateMode.SearchCompleted;
                }
                return _state.StateMode;
            }
            set {
                if (_state.StateMode != value) {
                    _state.StateMode = value;
                    CreateStateControls();
                }
            }
        }
        public void SetModeSearchCompleted() {
            ApplySelectedApplication();
            CurrentState = StateMode.SearchCompleted;
        }
        #endregion

        #region State
        [Serializable()]
        private class MyState {
            private StateMode _state_mode = StateMode.SearchStart;
            private int? _selected_app_id = null;
            private string _selected_app_name = string.Empty;
            private bool _is_readonly = false;
            private IList<ICareApplication> _search_result;

            public StateMode StateMode {
                get { return _state_mode; }
                set { _state_mode = value; }
            }
            public int? SelectedAppId {
                get { return _selected_app_id; }
                set { _selected_app_id = value; }
            }
            public string SelectedAppName {
                get { return _selected_app_name; }
                set { _selected_app_name = value; }
            }
            public bool IsReadOnly {
                get { return _is_readonly; }
                set { _is_readonly = value; }
            }
            public IList<ICareApplication> SearchResult {
                get { return _search_result; }
                set { _search_result = value; }
            }
        }
        protected override object SaveControlState() {
            return _state;
        }
        protected override void LoadControlState(object savedState) {
            _state = (MyState)savedState;
        }
        #endregion

        #region constants
        private const string CNST_SRCH_BADGE = "badge";
        private const string CNST_SRCH_USERID = "user_id";
        private const string CNST_SRCH_LASTNAME = "last_name";
        #endregion

        [global::System.Serializable]
        public class PeopleSearcherException : Exception {
            //
            // For guidelines regarding the creation of new exception types, see
            //    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
            // and
            //    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp
            //

            public PeopleSearcherException() { }
            public PeopleSearcherException(string message) : base(message) { }
            public PeopleSearcherException(string message, Exception inner) : base(message, inner) { }
            protected PeopleSearcherException(
              System.Runtime.Serialization.SerializationInfo info,
              System.Runtime.Serialization.StreamingContext context)
                : base(info, context) { }
        }

        #region utils
        private string GetMLString(string str_code) {
            return ApplicationContext.MLServ.GetString(str_code);
        }
        #endregion
    }
Этот контрол служит для поиска приложений в репозитории. Есть аналогичный - для поиска сотрудников. В теории таких аналогов может быть куча.
На очереди контрол, позволяющий редактировать информацию о телефонах - будет непохож на предыдущие по количеству состояний и их структуре, но скелет будет похожим.
Делать его таким же способом уже не хочется.
Записан

Всю ночь не ем, весь день не сплю - устаю
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« Ответ #3 : 03-04-2010 22:00 » 

Цитата: neco
В том, что код всех состояний в них намешан в одном классе и это затрудняет их поддержку (да и написание тоже).
Ок. В точности для этого случая есть шаблон проектирования "Состояние" (State).

Цитата: neco
Основная задача - распилить все состояния по разным классам. На данный момент мною предпринятые шаги с грехом пополам решают данную проблему.
Также задачей является упростить создание таких компонентов - т.е. различную требуху (механику восстановления предыдущего состояния и переход в следующее) написать один раз и благополучно забыть. Но при этом немаловажная задача - не добавить новую требуху. Вот с этим беда. Хочется, чтобы при написании нового стэйт-контрола уже не надо было помнить о том, как писался код базовой компоненты - чтобы использование было интуитивным и не давало возможности ошибиться.
Один из способов найти решение проблемы - посмотреть, как похожую проблему решают "белые люди" Улыбаюсь А ты рассматривал структурную организацию и работу аналогичных контролов? Например, "вкладки" (tabs). В них каждая страница по сути является отдельным контролом, задача сводится к тому, чтобы контейнер (твой контрол с меняющимся содержимым) по определённым событиям или командам менял своё содержимое. Каждое твоё состояние разумно описывать отдельным контролом, ко всему прочему внутри состояния могут быть события, не требующие смены состояния, и такой контрол в этом случае будет работать как обычный контрол (что удобно для разработчика).

Возникает тонкость лишь в хранении невидимых в данный момент состояний. В данном случае попытка использовать шаблон проектирования "хранитель" по-моему неудачна. По той причине, что все эти состояния относятся к какой-то общей сущности, и для работы в одном состоянии, возможно, потребуется информация из других состояний. В общем случае (например, для desktop-приложения) это не является проблемой; в твоём случае имеем огромную проблему, поскольку восстановление другого состояния из хранителя подразумевает загрузку контрола этого состояния.

Такая проблема решается использованием архтитектуры MVC (Model-View-Controller) или, как это делалось у Microsoft до WPF.NET, архитектуры Document-View. Можно считать, что Document=Model, а View=View+Controller. При таком решении веб-контролы не хранят в себе данные, а лишь отображают данные модели и управляют изменением данных в модели. Модель может храниться независимо от контролов (либо в сессии, либо выгружаться на страницу при помощи любого текущего контрола). Если состояния между собой тесно взаимосвязаны, одна модель может быть использована для нескольких контролов-состояний, если нет - по отдельной модели на каждое состояние и общая модель со словарём моделей состояний.

При этом вся прикладная логика выносится в модель, а вся логика управления внешним видом сохраняется в контролах. Помимо этого контролы выполняют операции загрузки состояния из модели и выгрузки состояния в модель.

Таким способом я за пару дней писал и вебовские "вкладки" и "мастера", с которыми другие разработчики не могли справиться неделями и месяцами. Причём без всяких своих собственных StatefulControl. Чёткого понимания, что делаешь, и как всё это работает, вполне достаточно для быстрой и качественной разработки Улыбаюсь

Я бы тебе посоветовал "потренироваться на кошках". По крайней мере я всегда пишу прототип какого-то частного нетривиального решения, чтобы в нём было только это решение в максимально простом виде, и больше ничего. Сделай отладочное приложение с контролом, у которого 2 состояния, циклически переключающиеся друг в другах (например, в одном красная кнопка, а в другом - синяя). И тщательно проанализируй все части такого приложения, как они взаимодействуют, какую информацию используют.
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
neco
Участник

kz
Offline Offline

« Ответ #4 : 04-04-2010 07:12 » 

Насчёт "тренироваться на кошках" - я собственно так всегда и делаю. Если есть большая задача - сначала делаю маленький прототипчик, на котором отрабатываю все сложные моменты, а потом уже пишу основной класс.
Но - такую тренировку приходится повторять каждый раз, поскольку каждый раз забываю в каком порядке происходит вызов, например, LoadState, Load и DataBound. Нечасто мне приходится их писать.

Хранить данные состояний я собираюсь в едином месте, доступном для всех состояний - до реализации просто ещё не дошёл.

Итого, я так понял, что ты мне предлагаешь не париться с написанием общей части в виде StatefulControl'а, а писать каждый компонент с нуля, просто по общему принципу, так?
Записан

Всю ночь не ем, весь день не сплю - устаю
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« Ответ #5 : 04-04-2010 07:43 » 

Цитата: neco
Итого, я так понял, что ты мне предлагаешь не париться с написанием общей части в виде StatefulControl'а, а писать каждый компонент с нуля, просто по общему принципу, так?
Нет, я сказал, что если хорошо понимать принцип, никаких запар не возникает, а твои запары проистекают от неполного понимания принципа, и ты своё неполное понимание хочешь упрятать в StatefulControl именно для того, чтобы вообще потом не вспоминать, как же это работает. Истинно говорю: не выйдет Улыбаюсь

Вопрос создания вспомогательного контрола - это вопрос отдельный. Я думаю, некую выгоду в части написания единого механизма переключения состояний из этого извлечь можно. Но не более того: все остальные провайдеры, фабрики и т.п. мне видятся попытками, нагромождая код, сбежать от необходимости точного понимания процесса работы.

Хорошее решение предельно просто и понятно всякому, красиво, универсально (его не надо дотачивать напильником под каждый конкретный случай).
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
neco
Участник

kz
Offline Offline

« Ответ #6 : 04-04-2010 08:11 » 

Хорошее решение предельно просто и понятно всякому, красиво, универсально (его не надо дотачивать напильником под каждый конкретный случай).
Звучит, конечно, элегантно. ))
Но не приближает меня к пониманию, как же быть.

Ты абсолютно прав, что я пытаюсь упрятать необходимость каждый раз понимать жизненный цикл asp.net контрола. Но по-моему в этом нет ничего зазорного. Жизненный цикл asp.net компонент уже извращён тем, что MS поверх вебной сущности решила создать эмуляцию работы с десктопными контролами. Мы испытываем проблемы, о которых, как мне кажется, обычному PHP программёру даже и неизвестно.
И я бы взялся за серьёзное использование Asp.Net MVC (который вроде как ближе к вебной сущности), если бы сама MS позиционировала Asp.Net MVC как полноценную замену WebForms. Так нет же - они прямо заявляют, что с помощью этой шняжки вы сможете решить часть проблем, но есть случаи, когда WebForms вам всё же понадобятся.
Собственно поэтому и занимаюсь извратами. Но хочу заниматься ими только один раз - а дальше чтобы всё было понятно и логично.

Но это уже лирическое отступление. )
Записан

Всю ночь не ем, весь день не сплю - устаю
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« Ответ #7 : 04-04-2010 14:46 » 

Хорошо, давай будем потихоньку разбирать.

Я написал приложение, как я обычно пишу в таких случаях.

Оно состоит из страницы, на которую помещаю контрол с изменяющимся внешним видом в зависимости от состояния. Для определённости считаем, что каждое состояние реализуется своим собственным множеством контролов. У меня каждое состояние - это кнопка, на которой написано, сколько раз её уже кликали.

Убрав весь мусор дизайнера:
Код: (Text) Default.aspx
<%@ Page Language="C#" AutoEventWireup="true" ClassName="MainPage" %>
<%@ Register TagPrefix="uc" TagName="StateControl" Src="StateControl.ascx" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
        <head>
                <title>State Control Test</title>
        </head>
        <body>
                <form id="MainForm" runat="server">
                        <uc:StateControl id="TestStateControl" runat="server"/>
                        <uc:StateControl id="AnotherTestStateControl" runat="server"/>
                </form>
        </body>
</html>
На главной странице помещаем 2 экземпляра нашего контрола, чтобы убедиться в их полной автономности друг от друга и возможности спокойно переживать обновления страницы, если работа происходит с другими контролами.

Код: (C#) Model.cs
using System;

[Serializable]
public class Model
{
        private int state1Count;
        private int state2Count;

        public Model()
        {
                this.state1Count = 0;
                this.state2Count = 0;
        }

        public int State1Count
        {
                get
                {
                        return this.state1Count;
                }
        }

        public int State2Count
        {
                get
                {
                        return this.state2Count;
                }
        }

        public void IncrementState1Count()
        {
                this.state1Count += 1;
        }

        public void IncrementState2Count()
        {
                this.state2Count += 1;
        }
}
Описываем модель, в которой будем хранить интересующие нас данные. Модель общая для всех состояний. Модель объявляется сериализуемой, чтобы сохраняться во ViewState контрола.

Код: (Text) StateControl.ascx
<%@ Control Language="C#" ClassName="StateControl" %>
<script runat="server">
        // Storage ///////////////////////////////////////////////////////////////////

        private Button state1Button;
        private Button state2Button;

        private enum States
        {
                State1,
                State2
        }

        private States State
        {
                get
                {
                        if(this.ViewState["State"] == null)
                        {
                                this.State = States.State1;
                        }
                        return (States)this.ViewState["State"];
                }
                set
                {
                        this.ViewState["State"] = value;
                }
        }

        private Model Model
        {
                get
                {
                        if(this.ViewState["Model"] == null)
                        {
                                this.ViewState["Model"] = new Model();
                        }
                        return this.ViewState["Model"] as Model;
                }
        }

        // States logic //////////////////////////////////////////////////////////////

        private void Load()
        {
                switch(this.State)
                {
                        case States.State1:
                                this.LoadState1();
                                this.state1Button.Visible = true;
                                break;
                        case States.State2:
                                this.LoadState2();
                                this.state2Button.Visible = true;
                                break;
                }
        }

        private void Unload()
        {
                switch(this.State)
                {
                        case States.State1:
                                this.SaveState1();
                                this.state1Button.Visible = false;
                                break;
                        case States.State2:
                                this.SaveState2();
                                this.state2Button.Visible = false;
                                break;
                }
        }

        private void Change()
        {
                this.Unload();
                switch(this.State)
                {
                        case States.State1:
                                this.ChangeState1To2();
                                this.State = States.State2;
                                break;
                        case States.State2:
                                this.ChangeState2To1();
                                this.State = States.State1;
                                break;
                }
                this.Load();
        }
   
        // States handlers ///////////////////////////////////////////////////////////

        private void InitState1()
        {
                this.state1Button = new Button();
                this.state1Button.BackColor = System.Drawing.Color.Red;
                this.state1Button.Click += new EventHandler(stateButton_Click);
                this.StateContainer.Controls.Add(this.state1Button);
                this.state1Button.Visible = false;
        }
   
        private void LoadState1()
        {
                this.state1Button.Text = this.Model.State1Count.ToString();
        }

        private void ChangeState1To2()
        {
                this.Model.IncrementState1Count();
        }

        private void SaveState1()
        {
        }

        private void InitState2()
        {
                this.state2Button = new Button();
                this.state2Button.BackColor = System.Drawing.Color.Blue;
                this.state2Button.Click += new EventHandler(stateButton_Click);
                this.StateContainer.Controls.Add(this.state2Button);
                this.state2Button.Visible = false;
        }

        private void LoadState2()
        {
                this.state2Button.Text = this.Model.State2Count.ToString();
        }

        private void ChangeState2To1()
        {
                this.Model.IncrementState2Count();
        }

        private void SaveState2()
        {
        }

        // Control handlers //////////////////////////////////////////////////////////

        private void Page_Init(object sender, EventArgs args)
        {
                this.InitState1();
                this.InitState2();
        }

        private void Page_Load(object sender, EventArgs args)
        {
                this.Load();
        }

        private void stateButton_Click(object sender, EventArgs args)
        {
                this.Change();
        }
</script>
<div id="StateContainer" runat="server"></div>
Здесь у нас сложный контрол, связанный с моделью. Контрол определяет 2 режима работы с моделью, которые представляют собой его состояния. Каждое состояние описывается своим множеством контролов.

Для управления внешним видом мы все контролы всех состояний держим загруженными и лишь изменяем их признаки видимости.

Естественно, есть соблазн избежать загрузки контролов лишних состояний, но мы связаны обязательством при PostBack восстанавливать коллекцию контролов страницы в том виде, в котором она была до PostBack, причём делать это до того момента, когда загрузится ControlState и ViewState, в противном случае внутренние состояния контролов не восстанавливаются правильным образом, и мы можем получать различные нежелательные эффекты: от потери контролами событий до ошибок загрузки состояния страницы. Происходит это потому, что для восстановления ControlState и ViewState уже нужно иметь те дочерние контролы, в которые восстанавливается состояние. Проблему можно обойти, если использовать в качестве хранилища сессию или родительский контрол, подгружающий данный динамически в Page_Load - в тот момент, когда его собственный ViewState уже загружен. Во втором случае фактическим хранилищем будет являться ViewState родительского контрола. Но поскольку у нас нет добавляемых и удаляемых в процессе работы контролов, мы такую конструкцию создавать не будем.

Во ViewState мы храним идентификатор текущего состояния, осуществляющий управление состояниями, и модель. Идентификатор состояния мы не храним в модели, поскольку наличие состояний - это особенность контрола, а не модели.

Общая логика поведения реализована в методах Load, Unload и Change. Load отвечает за включение группы контролов, ответственных за текущее состояние, а также заполнение их данными из модели. Unload отвечает за обратные действия: сохранение данных в модели и за выключение контролов. В Change реализована логика перехода между состояниями, зависящая от текущего состояния контрола (можно добавить зависимость от текущего состояния модели).

Каждое состояние описывается вспомогательными методами: Init, Load, Change, Save. Первый отвечает за инициализацию нужных дочерних контролов, второй - за загрузку данных из модели, третий - за действия, происходящие при смене состояний, четвёртый - за сохранение данных в модель.


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

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
neco
Участник

kz
Offline Offline

« Ответ #8 : 04-04-2010 16:03 » 

Хотелось бы начать вот с каких пунктов:
1. Давай будем иметь два состояния - модель можно хранить во ViewState (если в ней предполагается хранить данные состояний), но вот часть данных (таких как текущий State) лучше сразу хранить в ControlState. Иначе пользователь отключит EnableViewState и мы отгребём.
2. Давай всё-таки удалять контролы не принадлежащие к текущему состоянию из дерева, а не делать их невидимыми. Не помню конкретно что да как, но помнится мне, что контролы производят привязку к данным даже если они невидимы (или другие грабли) - в общем, когда-то я уже пришёл к выводу, что лучше всё же удалять.
3. Опять же не уверен, но насколько мне помнится, создание контролов должно производиться в хэндлере CreateChildControls. Ни раньше не позже. Page_Init, по-моему, как-то рановато, нет?
4. Давай делать не WebUserControl, а WebServerControl - я имею в виду не ascx, а просто класс в соседней сборке. Просто, чтобы потом не портировать.
5. К набору Init, Load, Change, Save предлагаю добавить Render, чтобы было где рендерить разметку.

Мне кажется это такие шаги, которые лучше предпринять прямо сейчас, прежде чем начинать что-то выносить в базовые контролы. А то потом, боюсь, замучаемся - как ты считаешь?
Записан

Всю ночь не ем, весь день не сплю - устаю
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« Ответ #9 : 04-04-2010 18:14 » 

Цитата: neco
Мне кажется это такие шаги, которые лучше предпринять прямо сейчас, прежде чем начинать что-то выносить в базовые контролы. А то потом, боюсь, замучаемся - как ты считаешь?
А я как раз считаю, что всё это - мелочи реализации, которые на хорошую архитектуру не влияют. Что это за универсальное решение, которое боится замены вида хранилища или способа переключения состояний? По хорошему оно вообще должно включать эти особенности как свои расширения или провайдеры услуг.

В начале должна быть идея, затем абстрактный каркас с его внутренней логикой, а потом все эти украшательства. Единственно, что сразу придётся учесть - пункт 4, так как форматы исходников различаются.
« Последнее редактирование: 04-04-2010 18:17 от Dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
neco
Участник

kz
Offline Offline

« Ответ #10 : 05-04-2010 15:15 » 

Я хотел уже было рефакторить к шаблону State, но потом посмотрел ещё раз на твои слова
Цитата
Теперь давай посмотрим, что отсюда можно вынести в базовые контролы и как именно можно обобщить множество состояний.
и решил обсудить.
Обобщить множество состояний, как мне кажется, можно путём введения интерфейса "провайдер состояний". Но поскольку enum он выдавать не будет, значит всё-таки первым делом надо перейти к шаблону State. Так?

Шаблон State я собираюсь реализовать следующим образом: создаю интерфейс по твоим методам Load, Unload, Save и т.п., выношу содержимое этих методов в соот-щие методы классов, создаю StateManager'а, который имеет свойство CurrentState и метод для смены состояний, о котором знают сами состояния и управляют через него переходами. В самом первоначальном контроле должны остаться только вызовы методов текущего состояния типа stateManager.CurrentState.Save и т.п.

Согласен с таким направлением?

К сообщению прилагаю архив с солюшеном.

* StatefulControl.rar (40.9 Кб - загружено 1037 раз.)
Записан

Всю ночь не ем, весь день не сплю - устаю
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« Ответ #11 : 05-04-2010 18:45 » 

Цитата: neco
Обобщить множество состояний, как мне кажется, можно путём введения интерфейса "провайдер состояний". Но поскольку enum он выдавать не будет, значит всё-таки первым делом надо перейти к шаблону State. Так?
Это какая-то середина мысли. Ты лучше сначала.

Цитата: neco
Согласен с таким направлением?
С направлением согласен.
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« Ответ #12 : 06-04-2010 18:34 » 

Отвлекаясь от ASP.NET специфики для данного случая, обобщение мне видится примерно таким:



Здесь пользователь может взять за основу класс "Автомат" и расширить его, подменив метод "СменитьСостояние" на собственную логику, и, чтобы не смешивать логику переключения с прочими действиями, в отдельном методе "ОбработатьСменуСостояния" может обработать как ему надо любой переход, поскольку имеет доступ к модели. Также пользователь обязуется взять за основу класс "Состояние", в котором он имеет доступ к модели внутри автомата и может послать автомату сообщение, требующее смены состояния. Всё остальное поведение состояния либо автономно, либо данного фреймворка не касается. Интерфейсы носят сугубо вспомогательную функцию и пользователю не нужны. Ещё пользователь обязуется в качестве модели подставить какой-нибудь класс.

Как это работает. Пользователь, написав свой автомат и соответствующие ему состояния, инициализирует эти состояния и передаёт их упорядоченное множество в автомат. Автомат первым делом в каждое состояние записывает ссылку на себя, позволяя состояниям обращаться к общей модели. Автомат рассматривает состояния как порядковые номера во множестве, и занимается переключением текущего состояния, выуживая его из множества по очередному номеру, определяемому логикой. Всё это позволяет целиком переложить на пользователя реализацию логики смены состояний: именно пользователь в производных классах задаёт порядок состояний при инициализации автомата и пишет соответствующую этому порядку логику в реализации метода "СменитьСостояние". При смене состояний старое состояние получает от автомата сообщение "ОбработатьВыключение", а новое - "ОбработатьВключение". С помощью этого состояния могут реализовываться по шаблону проектирования Lightweight, выполнять ленивую загрузку самих себя и/или освобождение ресурсов при выключении. Экземпляры состояний при этом всегда находятся в распоряжении автомата, и, собственно, именно по экземплярам состояний, их количеству и порядку автомат может принимать какие-то решения о переключениях.

P.S. Примечания по моим особенностям нотации. Если атрибут у меня помечен как открытый или защищённый, это надо понимать как свойство (property) с соответствующей областью видимости. Поэтому атрибутам в интерфейсах удивляться не надо.

* StateControlClasses.png (46.48 Кб - загружено 3392 раз.)
« Последнее редактирование: 06-04-2010 18:38 от Dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
neco
Участник

kz
Offline Offline

« Ответ #13 : 06-04-2010 20:50 » 

Ммм...
А я уже накодил.
Сразу скажу, что мало что удалось оставить из приведённого тобой примера - как-то оно всё не клеилось и само собой вернулось к тому к чему руки тянулись первоначально.
Но попробовал по максимально короткому пути придти к шаблону State, ни на что не отвлекаясь.
Первоначальный StatefulControl не удалял - чтобы было откуда подсматривать "как было и как стало".

Сейчас смотрю на твою диаграмму - в общих чертах похоже получилось. "Автомат" это у меня StateManager, "СостояниеДляАвтомата" - IState. Только я интерфейса для менеджера не делал - именно по причине минимализации шагов.

Цитата
Как это работает. Пользователь, написав свой автомат и соответствующие ему состояния, инициализирует эти состояния и передаёт их упорядоченное множество в автомат.
Так, а вот здесь уже непонятно. Т.е. это ты диаграмму привёл для готового решения, включающее всё что я хотел (минимальные телодвижения для создания контрола с состояниями), а не только State? Просто "пользователь" прозвучало как "тот кодер, который будет данные абстракции использовать".

В общем поясни, пожалуйста, на каком ты этапе. Я-то ещё в самом начале.

* StatefulControl.rar (51.17 Кб - загружено 1035 раз.)
Записан

Всю ночь не ем, весь день не сплю - устаю
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« Ответ #14 : 07-04-2010 12:00 » 

Цитата: neco
Так, а вот здесь уже непонятно. Т.е. это ты диаграмму привёл для готового решения, включающее всё что я хотел (минимальные телодвижения для создания контрола с состояниями), а не только State? Просто "пользователь" прозвучало как "тот кодер, который будет данные абстракции использовать".
Да, это целиком готовое решение. Я не очень понимаю, как можно проектировать State в отрыве от автомата. Поскольку они друг к другу имеют прямое отношение, это роли на концах ассоциации. Сам по себе вне контекста автомата класс состояния не является состоянием.


Здесь реализация фреймворка:
Код: (C#) BaseControls.cs
using System;
using System.Web.UI;
using System.Web.UI.WebControls;



// АвтоматДляСостояния
public interface IStateMachineProvider<ModelClass>
{
    // Модель
    ModelClass Model
    {
        get;
    }

    // СменитьСостояние(НомерНовогоСостояния)
    void ChangeState();
    void ChangeState(int newState);
}



// СостояниеДляАвтомата
public interface IStateProvider<ModelClass>
{
    // УстановитьАвтомат(Автомат)
    void SetStateMachineProvider(IStateMachineProvider<ModelClass> modelProvider);
   
    // Убраны "ОбработатьВключение" и "ОбработатьВыключение", поскольку WebControl и так имеет события Init, Load и Unload.
}



// Автомат
public abstract class StateMachineControl<ModelClass>:
    CompositeControl, INamingContainer,
    IStateMachineProvider<ModelClass> where ModelClass : class, new()
{
    // Значение по умолчанию для следующего состояния.
    public const int NO_NEW_STATE = -1;

    // Состояния
    private StateControl<ModelClass>[] stateControls;

    // НомерТекущегоСостояния
    private int CurrentStateIndex
    {
        get
        {
            if (this.ViewState[STATE_KEY] == null)
            {
                this.CurrentStateIndex = this.firstStateIndex;
            }
            return Convert.ToInt32(this.ViewState[STATE_KEY]);
        }
        set
        {
            if (value < 0 || value >= this.stateControls.Length)
            {
                throw new IndexOutOfRangeException("State with such number isn't exist.");
            }
            this.ViewState[STATE_KEY] = value;
        }
    }

    // Модель
    public ModelClass Model
    {
        get
        {
            if (this.ViewState[MODEL_KEY] == null)
            {
                this.ViewState[MODEL_KEY] = new ModelClass();
            }
            return this.ViewState[MODEL_KEY] as ModelClass;
        }
    }

    // Конструктор
    public StateMachineControl(StateControl<ModelClass>[] stateControls, int firstState) :
        base()
    {
        this.stateControls = stateControls;
        for (int i = 0; i < this.stateControls.Length; ++i)
        {
            StateControl<ModelClass> stateControl = this.stateControls[i];
            stateControl.SetStateMachineProvider(this);
            if (stateControl.ID == null)
            {
                stateControl.ID = string.Format("State{0}", i);
            }
        }
        this.firstStateIndex = firstState;
    }

    // СменитьСостояние(НомерНовогоСостояния)
    // Чистая логика переключения состояний выделена в отдельный метод для определения пользователем.
    public void ChangeState()
    {
        this.ChangeState(NO_NEW_STATE);
    }
    public void ChangeState(int newState)
    {
        this.Controls.Remove(this.CurrentState);
        int previousState = this.CurrentStateIndex;
        int nextState = this.GetNextState(previousState, newState);
        this.CurrentStateIndex = nextState;      
        this.OnStateChanged(previousState, nextState);
        this.Controls.Add(this.CurrentState);
    }
    protected virtual int GetNextState(int previousState, int newState)
    {
        return newState;
    }

    // ОбработатьСменуСостояния(НомерПрежнегоСостояния, НомерНовогоСостояния)
    protected virtual void OnStateChanged(int previousState, int nextState)
    {
    }


    private const string STATE_KEY = "State";
    private const string MODEL_KEY = "Model";

    private int firstStateIndex;

    private StateControl<ModelClass> CurrentState
    {
        get
        {
            return this.stateControls[this.CurrentStateIndex];
        }
    }    
   
    protected override void CreateChildControls()
    {
        base.CreateChildControls();
        this.Controls.Add(this.CurrentState);
    }
}



// Состояние
public abstract class StateControl<ModelClass>:
    CompositeControl,
    IStateProvider<ModelClass>
{
    // Автомат
    private IStateMachineProvider<ModelClass> stateMachineProvider;
    protected IStateMachineProvider<ModelClass> StateMachine
    {
        get
        {
            if (this.stateMachineProvider == null)
            {
                throw new InvalidOperationException("StateMachineProvider isn't initialized. Call SetStateMachineProvider method.");
            }
            return this.stateMachineProvider;
        }
    }

    // УстановитьАвтомат(Автомат)
    public void SetStateMachineProvider(IStateMachineProvider<ModelClass> stateMachineProvider)
    {
        this.stateMachineProvider = stateMachineProvider;
    }


    public StateControl() :
        base()
    {
        this.stateMachineProvider = null;
    }
}

Здесь пример пользовательской реализации (в соответствии с моим вышеприведённым примером):
Код: (C#) SampleControls.cs
using System;
using System.Drawing;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;



// Пример реализации Модели
[Serializable]
public class SampleModel
{
    private int state1Count;
    private int state2Count;

    public SampleModel()
    {
        this.state1Count = 0;
        this.state2Count = 0;
    }

    public int State1Count
    {
        get
        {
            return this.state1Count;
        }
    }

    public int State2Count
    {
        get
        {
            return this.state2Count;
        }
    }

    public void IncrementState1Count()
    {
        this.state1Count += 1;
    }

    public void IncrementState2Count()
    {
        this.state2Count += 1;
    }
}



// Пример реализации Автомата
public class SampleStateMachineControl:
    StateMachineControl<SampleModel>
{
    public enum States : int
    {
        State1 = 0,
        State2
    }

    public static int ConvertState(States state)
    {
        return Convert.ToInt32(state);
    }

    public SampleStateMachineControl() :
        base(
            new StateControl<SampleModel>[]
            {
                new Sample1StateControl(),
                new Sample2StateControl()
            },
            SampleStateMachineControl.ConvertState(States.State1))
    {
    }

    protected override int GetNextState(int previousState, int newState)
    {
        int defaultNextState = base.GetNextState(previousState, newState);
        if (newState == NO_NEW_STATE)
        {
            switch (previousState)
            {
                case 0:
                    return 1;
                case 1:
                    return 0;
                default:
                    return defaultNextState;
            }
        }
        return defaultNextState;
    }
}



// Пример 1 реализации Состояния
public class Sample1StateControl:
    StateControl<SampleModel>
{
    private Button button;

    public Sample1StateControl()
    {
        this.button = null;
    }

    private void LoadFromModel()
    {
        this.button.Text = this.StateMachine.Model.State1Count.ToString();
    }

    protected override void CreateChildControls()
    {
        base.CreateChildControls();
        this.button = new Button();
        this.button.BackColor = Color.Red;
        this.button.Click += new EventHandler(button_Click);
        this.Controls.Add(this.button);
        this.LoadFromModel();
    }

    private void button_Click(object sender, EventArgs e)
    {
        this.StateMachine.Model.IncrementState1Count();
        this.StateMachine.ChangeState();
    }
}



// Пример 2 реализации Состояния
public class Sample2StateControl :
    StateControl<SampleModel>
{
    private Button button;

    public Sample2StateControl()
    {
        this.button = null;
    }

    private void LoadFromModel()
    {
        this.button.Text = this.StateMachine.Model.State2Count.ToString();
    }

    protected override void CreateChildControls()
    {
        base.CreateChildControls();
        this.button = new Button();
        this.button.BackColor = Color.Blue;
        this.button.Click += new EventHandler(button_Click);
        this.Controls.Add(this.button);
        this.LoadFromModel();
    }

    private void button_Click(object sender, EventArgs e)
    {
        this.StateMachine.Model.IncrementState2Count();
        this.StateMachine.ChangeState();
    }
}

Ну и страница веб-сайта:
Код: (Text) Default.aspx
<%@ Page Language="C#" AutoEventWireup="true" ClassName="MainPage" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>State Control Test</title>
        <script type="text/C#" runat="server">
            private void Page_Init(object sender, EventArgs args)
            {
                this.TestStateControl.Controls.Add(new SampleStateMachineControl());
                this.AnotherStateControl.Controls.Add(new SampleStateMachineControl());
            }
        </script>
    </head>
    <body>
        <form id="MainForm" runat="server">
            <div id="TestStateControl" runat="server"></div>
            <div id="AnotherStateControl" runat="server"></div>
        </form>
    </body>
</html>
« Последнее редактирование: 07-04-2010 12:09 от Dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
neco
Участник

kz
Offline Offline

« Ответ #15 : 10-04-2010 20:52 » new

Нет, всё-таки не могу допереть как надо начать мыслить так, чтобы писать понятный код.
Вот, Dimka, ты написал действительно проще (а значит и лучше), чем пытался я. Т.е. я как бы сразу встал не неправильный путь - вводил слишком много классов и в них запутался.
Но с другой стороны - не всегда же можно будет так упростить.
В общем, спасибо за помощь, но, видимо, всё же это только с опытом придёт.
Записан

Всю ночь не ем, весь день не сплю - устаю
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« Ответ #16 : 11-04-2010 10:18 » 

Цитата: neco
Вначале начал создавать диаграммку, потом нервы сдали и стал писать сразу кодом, поскольку фантазии не хватает, представить какие проблемы будут в коде - как-то проще представить диаграмму по коду.
Ну во-первых, интересно, что именно ты пытался рисовать на диаграмме. Если сразу веб-контролы с их состояниями, то, действительно, и у меня ни фантазии, ни терпения не хватит всё это тщательно прорисовать. Однако у Microsoft уже есть схемы с описанием жизненного цикла контрола, т.е. это нужно не столько придумать и нарисовать, сколько изучить. Поэтому я в таких случаях абстрагируюсь от внутреннего устройства контрола, рассматривая контролы как атомарные единицы вида:
Код:
class Control { }
Всё относящееся к жизненному циклу контрола мысленно инкапсулируется внутри него и убирается из моей головы.

Дальше я начинаю думать, что хочу вот в этом пустом классе создать нечто, обладающее сложными и во многом автономными состояниями - вспоминается шаблон проектирования "Состояние". Раз мы говорим слово "состояние", то подразумеваем какие-то данные. В этом случае состояния не обладают каждое в отдельности какими-то специфическими данными, в чём тогда разница? Разница в поведении, в отображении данных пользователю, в получаемых от пользователя командах и т.п. - вспоминается другой шаблон проектирования "Стратегия", который по сути отличается от состояния именно тем, что акцентирует внимание на поведении, а не на данных. Тем не менее, эти шаблоны очень близки по архитектуре решения: есть центральный элемент, с которым работает клиент, и этот центральный элемент является фасадом, инкапсулирующим от клиента то обстоятельство, что внутри всей конструкции вложенные вспомогательные элементы (состояния или стратегии) подменяют друг друга в зависимости от условий, режима работы; также центральный элемент может являеться прокси для доступа клиента к конкретному вспомогательному элементу. Всё это, пока не имеющее конкретных деталей, выглядит примерно так:
Код:
interface IStrategy {}
class Strategy1: IStrategy {}
class Strategy2: IStrategy {}

class Strategy: IStrategy
{
   private IStrategy CurrentStrategy;
   public void ChangeCurrentStrategy() {}
}
Начинаю вспоминать аналогичные по устройству контролы, состоящие из центрального и меняющихся вспомогательных - это вкладки (tabs) и мастера (wizards).

Теперь надо подумать о данных. Данные общие, а стратегии разные. Значит нужно: во-первых, отделить данные от моих стратегий, во-вторых, организовать доступ стратегий к общим данным. Вспоминается архитектура MVC - и действительно, контролы нужны лишь для отображения данных (view) и приёма команд пользователя (controller), данные можно хранить отдельно. Тогда заводим модель. Совершенно естественно поместить модель в центральный элемент нашей структуры, т.к. он один, он центральный, он постояенен по сравнению со вспомогательными элементам. Далее нужно предоставить вспомогательным элементам доступ к модели в центральном элементе - можно завести соответствующий интерфейс и предоставить его вспомогательным элементам.
Код:
class Model {}
interface IModelProvider
{
   Model GetModel();
}

interface IStrategy {}
class StrategyVariant: IStrategy
{
   private IModelProvider ModelProvider;
}
class Strategy1: StrategyVariant {}
class Strategy2: StrategyVariant {}

class Strategy: IStrategy, IModelProvider
{
   private IStrategy CurrentStrategy;
   private Model Model;
   public void ChangeCurrentStrategy() {}
   public Model GetModel() {}
}
Теперь стратегии обеспечены данными. Ещё нужно подумать, каким образом стратегии будут подменять друг друга в процессе работы. Тут есть два варианта. Согласно первому варианту (как, например, в шаблоне проектирования "Интерпретатор") каждая стратегия сама определяет, когда она должна замениться на другую, и на какую именно другую стратегию ей нужно меняться. Согласно второму варианту (как, например, в шаблоне проектирования "Посредник" (Mediator)) каждая стратегия лишь сообщает посреднику, что её работа окончена, и посредник сам решает, какую следующую стратегию загружать. У первого варианта есть преимущество, заключающееся в удобной расширяемости на очень большое количество состояний/стратегий, но на него же наложено ограничение - все переходы должны быть древовидно упорядочены (как в грамматике распознаваемого языка), их перебор укладывается в стек, иначе возникнет путаница. Второй вариант обладает тем преимуществом, что способен разрешить любую путаницу за счёт централизации, и поэтому он годится для работы с любыми графами состояний и переходов любой сложности; однако же количество состояний/стратегий там не может возрастать слишком сильно - разумно ограничиться несколькими десятками, иначе алгоритм переключения окажется сильно запутанным. В принципе, ничто не мешает в особо сложных случаях комбинировать эти методы, укладывая в стек посредников или реализуя посредников через стратегии. В нашем случае количество состояний будет мало (обычно до 10), а логика их переключения в общем случае заранее непредсказуема. Поэтому предпочтительнее второй вариант. Для его реализации нужно, чтобы вспомогательный элемент мог послать центральному элементу сообщение, по которому центральный элемент запускает алгоритм выбора нового состояния. При желании можно реализовать и первый вариант, если вместе с сообщением передавать информацию о новой стратегии, которую нужно загрузить. Получается примерно так:
Код:
class Model {}

interface IMediatorProvider
{
   Model GetModel();
   void ChangeStrategy();
}
interface IStrategy {}
class StrategyVariant: IStrategy
{
   private IMediatorProvider MediatorProvider;
   protected void OnChangeStrategy()
   {
      this.MediatorProvider.ChangeStrategy();
   }
}
class Strategy1: StrategyVariant {}
class Strategy2: StrategyVariant {}

class Strategy: IStrategy, IMediatorProvider
{
   private IStrategy CurrentStrategy;
   private Model Model;
   public ChangeStrategy() {}
   public Model GetModel()
   {
      return this.Model;
   }
}
Наметили. Остаётся открытым вопрос, как это всё собрать и связать между собой. Центральный элемент должен иметь информацию о вспомогательных, вспомогательные должны иметь доступ к центральному через интерфейс. Чтобы создавать связи, либо заводят специальные методы, либо передают данные в конструкторе. Первый способ используют, когда связи не зависят от времени жизни объекта, второй способ - когда сущность объекта не имеет смысла без связей. В нашем случае имеется централизованный механизм переключения состояний/стратегий. Мы можем предположить, что он будет простым для небольшого заранее определённого набора состояний/стратегий; либо, что он будет сложным для неопределёёного, меняющегося во времени набора состояний/стратегий. Вспоминая, что мы пишем контролы, и перебирая в памяти известные контролы, которые хотим реализовать этим способом, по здравому рассуждению мы приходим к выводу, что второй вариант в данном случае - ненужное усложнение. Значит у нас будет заранее заданное множество состояний/стратегий, известное центральному элементу, внутри которого будет простой алгоритм их переключения. Тогда все состояния/стратегии мы можем описать прямо в центральном элементе и там их же создавать. А поскольку доступ к центральному элементу нужен всякому состоянию/стратегии, то ссылку на интерфейс можно поместить в конструкторе состояния/стратегии. Следует отметить, что хотя перечень состояний/стратегий известен центральному элементу, тем не менее, он не делает никаких предположений об особенностях каждого состояния/стратегии, т.е. с его точки зрения они все одинаковы. Модель тоже привязана к центральному элементу и без него не имеет смысла, поэтому её создаёт центральный элемент. Получается примерно так:
Код:
class Model {}

interface IMediatorProvider
{
   Model GetModel();
   void ChangeStrategy();
}
interface IStrategy {}
class StrategyVariant: IStrategy
{
   private IMediatorProvider MediatorProvider;
   public StrategyVariant(IMediatorProvider MediatorProvider)
   {
      this.MediatorProvider = MediatorProvider;
   }
   protected void OnChangeStrategy()
   {
      this.MediatorProvider.ChangeStrategy();
   }
}
class Strategy1: StrategyVariant {}
class Strategy2: StrategyVariant {}

class Strategy: IStrategy, IMediatorProvider
{
   private IStrategy[] Strategies;
   private IStrategy CurrentStrategy;
   private Model Model;
   public Strategy()
   {
      this.Model = new Model();
      this.Strategies = new IStrategy
      {
         new Strategy1(this),
         new Strategy2(this)
      }
      this.CurrentStrategy = this.Strategies[0];
   }
   public ChangeStrategy() {}
   public Model GetModel()
   {
      return this.Model;
   }
}
Теперь нужно вспомнить, что мы пишем обобщённое решение, которое затем хотим повторно использовать. Что у нас является неопределённым на общем уровне? Набор состояний, логика их переключения, модель.

Набор состояний - это объекты, имеющие общий интерфейс и с точки зрения центрального элемента одинаковые. Поэтому разумно будет вынести их создание из центрального элемента куда-то ещё. На этот случай имеется шаблон проектирования "Фабричный метод". Однако этот шаблон подразумевает наличие разных фабрик для общего места их применения на одном уроне абстракции. У нас же общее место находится на одном, а фабрика - на другом уроне абстракции. Если же посмотреть на каждый уровень абстракции в отдельности, мы увидим лишь одну фабрику, связанную с одним же центральным элементом. Значит шаблон можно упростить следующим образом: источником конкретных состояний для абстрактного центрального элемента будет конкретный центральный элемент (т.е. потомок будет являться неким аналогом фабрики для предка). Городить здесь дополнительные классы избыточно. При этом нужно будет пересмотреть логику связывания элементов в общее целое. У нас центральный элемент сам подставляет себя в качестве источника модели и посредника для состояний - это нужно сохранить на общем уровне, иначе потребуется включать этот необходимый шаг в каждую конкретную реализацию и полагаться на то, что пользователь это напишет. Однако создание состояний уходит из обобщённого центрального элемента, следовательно, выполнить настройку сразу в конструкторах состояний уже нельзя. Поэтому нам нужна некоторая реализация обобщённого состояния, включающая в себя: метод установления связи с центральным элементом, проверки того, что при выполнении обращений к центральному элементу тот уже установлен, и обращаться к нему можно.

Логика переключения состояний - это алгоритм. Достаточно в обобщённом решении завести абстрактный метод, который обязуются реализовать конкретные решения.

Модель - это класс. Причём его интерфейс заранее не определён. В каноническом ООП здесь неизбежна потеря информации о типе и необходимость приведения типов через RTTI. Чтобы этого избежать, мы можем воспользоваться параметризацией. Если сделать обобщённое решение параметризируемым классом конкретной модели, то в конкретной реализации всегда можно будет поставить эту конкретную модель и получить порождённый тип с нужной моделью. С другой стороны, модель - это объект. По аналогии с набором состояний, разумно поручить его создание потомку.

Ещё нужно подумать об интерфейсе между обобщённым и конкретным решением. В обобщённом состоянии нужно предоставить потомкам защищённый аксессор к центральному элементу. В обобщённом центральном элементе нужно предоставить для его потомков доступ к состояниям/стратегиям. Если внимательно посмотреть на интерфейс состояния/стратегии, то мы увидим, что конкретный центральный элемент не может получить от предка ничего особо интересного - потомок и так знает, какие состояния/стратегии у него есть, так как он сам их передавал предку. Потомку от предка нужна лишь информация для реализации логики переключения состояний/стратегий: узнать текущее состояние, сказать, в какое следующее перейти. Это можно реализовать посредством ассоциативных ключей, связанных с каждым состоянием - в простейшем случае такими ключами будут являться порядковые номера или индексы массива с состояниями/стратегиями. Значит потомок должен получить от предка индекс текущего состояния и передать ему индекс нового состояния. В результате получается примерно так:
Код:
// Обобщённое решение

interface IMediatorProvider<M>
{
   M GetModel();
   void ChangeStrategy();
}
interface IStrategy<M>
{
   void SetMediatorProvider(IMediatorProvider<M> MediatorProvider);
}
class StrategyVariant<M>: IStrategy<M>
{
   private IMediatorProvider<M> MediatorProvider;
   public StrategyVariant()
   {
      this.MediatorProvider = null;
   }
   public SetMediatorProvider(IMediatorProvider<M> MediatorProvider)
   {
      this.MediatorProvider = MediatorProvider;
   }
   protected void OnChangeStrategy()
   {
      if(this.MediatorProvider == null)
      {
         throw new Exception();
      }
      this.MediatorProvider.ChangeStrategy();
   }
   protected IMediatorProvider<M> GetMediatorProvider()
   {
      if(this.MediatorProvider == null)
      {
         throw new Exception();
      }
      return this.MediatorProvider;
   }
}

class BaseStrategy<M>: IMediatorProvider<M>
{
   private IStrategy<M>[] Strategies;
   private int CurrentStrategyIndex;
   private M Model;
   public BaseStrategy(M Model, IStrategy<M>[] Strategies)
   {
      this.Model = Model;
      this.Strategies = Strategies;
      foreach(IStrategy strategy in this.Strategies)
      {
         strategy.SetMediatorProvider(this);
      }
      this.CurrentStrategyIndex = 0;
   }
   public ChangeStrategy()
   {
      this.CurrentStrategyIndex = this.GetNextStrategyIndex();
   }
   public M GetModel()
   {
      return this.Model;
   }
   protected int GetCurrentStrategyIndex()
   {
      return this.CurrentStrategyIndex;
   }
   protected abstract int GetNextStrategyIndex();
}

// Конкретное решение

interface ICleanStrategy {}
class Model {}
class Strategy1: StrategyVariant<Model>, ICleanStrategy {}
class Strategy2: StrategyVariant<Model>, ICleanStrategy {}

class Strategy: BaseStrategy<Model>, ICleanStrategy
{
   public Strategy():
      base(new Model(), new IStrategy<Model>
         {
            new Strategy1(),
            new Strategy2()
         }
   {
   }
   protected override int GetNextStrategyIndex()
   {
      switch(this.GetCurrentStrategyIndex())
      {
         case 0:
            return 1;
         case 1:
            return 0;
         default:
            return 0;
      }
   }
}
В принципе, у меня на диаграмме выше представлено сильно похожее решение с незначительными вариациями. Здесь не учитывается специфика веб-разработки и не учитывается размер состояний, целесообразность их одновременной загрузки. Для решения последнего вопроса можно взять шаблон проектирования Lightweight.

Теперь осталось всё это наложить на контролы. Состояние/стратегия - это отдельный контрол, центральный элемент - тоже контрол, контейнер для состояний/стратегий. Нужно добавить загрузку/выгрузку контролов состояний/стратегий при обновлении страницы и при смене состояний. Нужно обеспечить передачу данных из/в модель. Нужно обеспечить сохранение модели между запросами. И т.д., и т.п. Ещё можно обратить внимание, что у всей конструкции нет внешнего программного клиента, поэтому интерфейс ICleanStrategy можно выбросить. Но все эти вещи по отношению к вышеописанной архитектуре - мелкие частности, которые не вносят в неё существенных изменений, а значит полученное обобщённое решение годно к многократному применению.
« Последнее редактирование: 11-04-2010 10:36 от Dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« Ответ #17 : 11-04-2010 11:19 » 

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

Всякий раз берётся какая-то одна конкретная небольшая проблема, отбрасывается вся информация, не относящаяся к делу, внимание концентрируется только на этой проблеме и на связанной с ней информации, проблема решается.

Ситуации, когда проблема не может быть решена, потому что для этого нужно решить другую проблему, не возникают. Если такая ситуация возникла в мыслях, значит нужно тут же выбросить из головы старую проблему и всю связанную с ней информацию. В одно время заниматься только одним делом, не пытаясь охватить сразу всё и вся. Попытки охватить всё сразу обычно связаны с мыслями о всяких "оптимальных" решениях, всяких "а вдруг понадобится" - это всё преждевременная оптимизация. Сначала нужно получить решение, и только потом думать про "а вдруг" - сопоставлять решение с каждым этим "вдруг" и оценивать вероятности. Только если решение не подходит к какому-то важному случаю, нужно искать новое решение.

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

Если какое-либо типовое решение (шаблон) для данной проблемы обладает лишними свойствами, при выборе нужно адаптировать шаблон под проблему, а не проблему под шаблон. Не нужно следовать букве шаблона.

Отделение главного от второстепенного невозможно, когда нет чёткого понимания цели деятельности.

Всякая решаемая проблема должна быть небольшой - описываться одним предложением. Большие проблемы нужно делить на малые (если известно как), либо переформулировать и обобщать до такого уровня, когда они становятся маленькими.

Всякая проблема имеет контекст. Если для формулировки проблемы нужно очень большое введение, такая проблема является узкой. Узкие проблемы лучше решать после "широких", которые очевидны без всяких введений.
Цитата: В. И. Ленин "Полн. собр. соч.", т. 15, стр. 368
...Кто берется за частные вопросы без предварительного решения общих, тот неминуемо будет на каждом шагу бессознательно для себя «натыкаться» на эти общие вопросы
« Последнее редактирование: 11-04-2010 17:33 от Dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« Ответ #18 : 11-04-2010 17:25 » 

Небольшое лирическое отступление.

Цитата: Dimka
Модель - это класс. Причём его интерфейс заранее не определён. В каноническом ООП здесь неизбежна потеря информации о типе и необходимость приведения типов через RTTI. Чтобы этого избежать, мы можем воспользоваться параметризацией. Если сделать обобщённое решение параметризируемым классом конкретной модели, то в конкретной реализации всегда можно будет поставить эту конкретную модель и получить порождённый тип с нужной моделью.
Хорошо подумал и понял, что неправ по поводу канонического ООП.

Если писать таким образом:
Код:
interface IX {}
class Y
{
   protected IX x;
   protected Y(IX x)
   {
      this.x = x;
   }
}

class X: IX {}
class Z: Y
{
   public Z(): base(new X()) {}
   protected X GetX()
   {
      return this.x as X;
   }
}
то, действительно, нужно RTTI (использование оператора as). Поэтому и было предложено неканоническое решение с порождающими типами:
Код:
class Y<T>
{
   protected T x;
   protected Y(T x)
   {
      this.x = x;
   }
}

class X {}
class Z: Y<X>
{
  public Z(): base(new X()) {}
}
где RTTI не используется. Однако можно написать и каноническое решение без использования RTTI, но нужно будет перенести атрибут в потомок, а аксессор - в предок:
Код:
interface IX {}
class Y
{
   protected abstract IX GetX();
}

class X: IX {}
class Z: Y
{
   private X x;
   public Z()
   {
      this.x = new X();
   }
   protected override IX GetX()
   {
      return this.x;
   }
}
Правда, в вышеприведённом случае это не подходит, поскольку доступ к модели вынесен на обобщённый уровень, и конкретное состояние получает модель не от конкретного центрального элемента, а через обобщённое состояние. Иначе пришлось бы установку связи между состояниями и центральным элементом выносить в конкретное решение, полагаясь на аккуратность пользователя, что нежелательно - проще привести типы или использовать порождающие типы.
« Последнее редактирование: 11-04-2010 17:30 от Dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines