Model View Presenter (MVP) design pattern close look - Part 1 (Supervising Controller)
Usually, usage of design patterns is associated with code executing in the lower tiers, but design patterns can be very usefully used in solving problems of the UI world too. Two of my architecture blog posts are covering the examples related to patterns usage in solving the Web problems: Although young,Model View Presenter pattern has been retired and two variations from original MVP pattern came up from it. This little series of blog posts would cover in depth architecture and implementation of the both MVP design pattern successors: Passive View and Supervising Controller. All of the blog posts would use the same use case and the same Model domain entities, so the downsides and upsides of the different approaches can be easier notable. What is Model View Presenter?If you are new to Model View Presenter pattern at all, you should check the Model View Presenter entry level post which explains the most important concepts of wiring the presenter and view in step by step manner. Once you would chew that, come back to this post and continue reading :)
As we can see from this diagram:
View responsibility is to show the data provided by presenter, and purpose of the presenter is to reach to model, retrieve the needed data, performs required processing and returns the UI prepared data to the view. The biggest difference between the Model View Controller (MVC) and Model View Presenter (MVP) pattern is in the fact that (in MVC) View is aware of model too. I personally prefer more the MVP pattern because it leads to easier testability and decouples completely UI from the data sources, making it by that very testable. Use caseToday‘s use case would be that we got request to build a simple web page which would allow us searching for a desired user and presenting its data retrieved from DB on web page. User data which should be resented are: first name, last name and collection of his addresses. Web page is supposed to provide a way of informing the user when there is no user to be retrieved with a given use name. UI interface should offer the possibility of editing the first name and last name data, but presenter should persist the data only if there would be some changes. In case UI didn‘t modify data retrieved from DB, presenter shouldn‘t make DB call at all. The implementation design should allow testing the UI layer without using UI specific testing frameworks, preferably with most of the code logic extracted from a page level ModelBoth patterns today (passive view and supervising controller) would share the same model in their examples. Model for this use case would be represented with a component called UserRepositry which static class diagram looks something like this:
The User class is a domain data carrier class which has 4 properties. Three of them are required to be presented to web page user:Name, Surname and Address where the Address is collection of UserAddress objects with City and Street properties. Credit card number is user attribute which we don‘t want to present to web page user. It represents the example of domain attribute not required by UI. The IUserService interface defines methods:
UserServiceStub is just a stub class stubbing the operation of user data retrieval from a database. I‘m aware that in unit tests I could use dynamic mock objects but for purpose of this blog post treat UserServiceStub as a class retrieving the data from a DB. The class itself implements IUserService like this: 1: public class UserServiceStub:IUserService 2: {
3: #region IUserService Members 4:
5: public User GetUser(string userName) 6: {
7: if (userName == "Nikola") 8: {
9: User newUser = new User(); 10:
11: newUser.Name = "Nikola"; 12: newUser.Surname = "Malovic"; 13: newUser.Addresses.Add(new UserAddress("Vaclavske Namesti 11", "Prague, CZE")); 14: newUser.Addresses.Add(new UserAddress("5 Clock Tower", "Maynard, MA, USA")); 15: return newUser; 16: }
17: return null; 18: }
19:
20: public void SaveUser(User user) 21: {
22: // perform saving to DB 23: }
24:
25: #endregion 26: }
In case that the given user name would be Nikola, the method would return user object with first name, last name and two addresses. In case of different user name the method would return null. Save method is just stubbing the functionality which saves the user data into the database View markupBoth examples would share the same page html markup code definition which would look something like this: 1: <div> 2: Enter desired user name:<asp:TextBox ID="SearchCriteriaTextBox" runat="server"></asp:TextBox> 3: <asp:Button ID="SearchButton" runat="server" Text="Find user" OnClick="OnUserSearchButtonClicked" /> 4: <br /> 5: <asp:Label ID="ResultLabel" runat="server" /> 6: <br /> 7: <div id="resultPanel" runat="server" visible="false"> 8: <asp:Label ID="FirstNameLabel" runat="server" text="First name" /> 9: <asp:TextBox ID="FirstNameTextBox" runat="server" OnTextChanged="FirstName_TextChanged" /> 10: <br /> 11: <asp:Label ID="LastNameLabel" runat="server" text="Last name" /> 12: <asp:TextBox ID="LastNameTextBox" runat="server" OnTextChanged="LastName_TextChanged"/> 13: <br /> 14: <asp:Repeater ID="AdressesRepeater" runat="server"> 15: <HeaderTemplate> 16: <strong>Adresses</strong><br /> 17: </HeaderTemplate> 18: <ItemTemplate> 19: <%# Container.DataItem.ToString() %> <br /> 20: </ItemTemplate> 21: </asp:Repeater> 22: <br /> 23: <asp:Button ID="SaveButton" runat="server" Text="Save changes" OnClick="OnUserSaveButtonClicked" /> 24: </div> 25: </div> On the top of the web form there is a SearchCriteriaTextBox where the user would enter the user name of the user which data he wants to see. Once he would enter that, he would click on a SearchButton which click event would be handled in OnSearchUser_Click event handler ResultLabel purpose is to show the informative messages to web page user if there is no user with a given user name etc.. User data successfully retrieved would be presented inside of the result pannel:
Inside of the result panel there is also Save changes button which click instantiates user persisting action The page in design mode would look something like this:
Supervising controllerSupervising controller is a type of MVP design pattern based on a concept of data context binding where the view is implementing some "binding" logic on a object received from presenter which contains certain group of business data needed for the whole view. The view is therefore performing some functionality on it‘s own. The biggest advantage of supervising controller is in the fact that presenter is not aware of the view internals, which allows reuse of one presenter with multiple views using the same context data source but with different UI elements. Also presenters are in general much simpler comparing with the Passive view presenters which need to take care about every single aspect of the view. View interface
Interface contains next abstract representations:
View DTO (data transfer object)Purpose of the DTO (Data transfer object) is to be information mule, without any logic inside which has a collection of properties in which data is carried in encapsulated manner. DTO pattern is essential one in implementing SOA systems, but I‘ll skip that aspect for now. The biggest question related to View DTO is when we should use it at all? The downside of DTO is that we have to map domain object to the DTO which introduces one more level of indirection ("There is no problem which can not be solved with one more level abstraction") and additional coding. In our little example the IUserService.Get method returns the User
object so why not pass that object to the view and skip the mapping
hustle?
So, in this example UserDTO looking like this:
As we can see the DTO object has first name and last name string
properties and collection of string addresses. There is no credit card
number property, because it is not required to be presented in UI. Data mappingData mapper design pattern default interpretation includes into the set of data mapper responsibilities beside mapping the data also the data retrieval activities. Although, I agree that is the official and correct implementation, I don‘t like that small functional add on because it breaks the separation of concern principles. IMHO, purpose of the data mapper is just transformation of the already loaded data. Mapper should only know "how to map". So, our little example therefore contains a DataMapper static helper class which class diagram looks like this:
We have overloaded Translate method which performs appropriate data translation for a given method parameter type Let‘s take a quick look at the implementation of the first one 1: public static UserDTO Translate(User user)
2: {
3: UserDTO userDTO = new UserDTO();
4: userDTO.FirstName = user.Name;
5: userDTO.LastName = user.Surname;
6: userDTO.Addresses = MapUserAddress(user.Addresses);
7: return userDTO;
8: }
9:
10: private static IEnumerable<string> MapUserAddress(IEnumerable<UserAddress> adresses) 11: {
12: foreach (UserAddress address in adresses)
13: {
14: yield return string.Format("{0},{1}", address.Street, address.City);
15: }
16: }
This translate method accepts the User type instance as parameter. View implementationPresenter - View wire upThe pattern used for establishing wire up between the view and presenter is dependency injection - constructor type. The key concepts of this wire up are:
In code that would look like this: 1: private UserDetailsPresenter _presenter; 2:
3: protected void Page_Load(object sender, EventArgs e)
4: {
5: // injecting the view into the presenter
6: _presenter = new UserDetailsPresenter(this);
7: }
View interface implementationThe view is a web page implementing the view interface and wiring and mapping the UI elements properties to the certain view interface members, something like this: 1: #region IUserDetailsView Members
2:
3: public UserDTO ContextData
4: {
5: get { return Session["Context"] as UserDTO; }
6: set
7: {
8: Session["Context"] = value;
9: OnContextDataBound();
10: }
11: }
12:
13: public string StatusMessage
14: {
15: set { ResultLabel.Text = value; }
16: }
17:
18: public event EventHandler<SearchEventArgs> Search; 19: public event EventHandler<EventArgs> Save; 20:
21: #endregion
In line 13, we implement the IView.StatusMessage property on a way that setting that property would end with setting the Web.UI.Label text property with the same value. ContextData property implementation in this example stores a interface property value to a session variable (to prevent it from post backs) and calls the OnContextDataBound method where View would update itself with new context data, on a way similar to this: 1: private void OnContextDataBound()
2: {
3: FirstNameTextBox.Text = ContextData.FirstName;
4: LastNameTextBox.Text = ContextData.LastName;
5: AdressesRepeater.DataSource = ContextData.Addresses;
6: AdressesRepeater.DataBind();
7: resultPanel.Visible = true;
8: }
So, in lines 3 and 4 we just set the text boxes text property with context object data values. View interface events are implemented in lines 18 and 19, and they have just trivial event handlers to raise those events, something like this: 1: protected void OnUserSearchButtonClicked(object sender, EventArgs e)
2: {
3: if (Search != null)
4: Search(this, new SearchEventArgs(SearchCriteriaTextBox.Text));
5: }
6:
7: protected void OnUserSaveButtonClicked(object sender, EventArgs e)
8: {
9: if (Save != null)
10: Save(this, new EventArgs());
11: }
View routing logic is implementing the functionality of reflecting the UI specific changes to the appropriate context data object members. 1: protected void FirstName_TextChanged(object sender, EventArgs e) 2: {
3: ContextData.FirstName = FirstNameTextBox.Text;
4: }
5:
6: protected void LastName_TextChanged(object sender, EventArgs e) 7: {
8: ContextData.LastName = LastNameTextBox.Text;
9: }
At the end of the view code implementation, the view needs to wire up itself with presenter (MVP diagram shows that view contains the instance of presenter) and we can do that by doing something like this: 1: UserDetailsPresenter _userDetailsPresenter;
2: protected void Page_Load(object sender, EventArgs e)
3: {
4: // injecting the view into the presenter
5: _userDetailsPresenter=new UserDetailsPresenter(this);
6: }
As we can see in line 5, the view constructs presenter instance and throws itself to the presenter constructor parameter so the presenter could have a pointer to the view interface defined members. PresenterClass diagram of the presenter looks like this:
As we can see from the diagram, presenter has two public constructors and two private methods handling the appropriate view interface events. Presenter has also two fields which hold the presenter pointers to the view and service layers. Presenter initializationAll the business logic of controlling,presenting and updating model and interaction with view should be encapsulated in the presenter. In this example Presenter is getting pointer to the view interface and model services through the utilization of constructor type of dependency injection design pattern. 1: public UserDetailsPresenter(IUserDetailsView view, IUserService userService)
2: {
3: _view = view;
4: _userService = userService;
5: _view.Search += (OnUserSearch);
6: _view.Save += new EventHandler<EventArgs>(OnUserSave); 7: }
8:
9: public UserDetailsPresenter(IUserDetailsView view)
10: : this(view, new UserServiceStub())
11: {
12: }
I won‘t detail here too much about how DI works (if you need that, check out the Model View Presenter post) but summarized whatever the classes implementing the view and service interfaces would be, its pointers would be stored inside of the presenter object and the presenter would use them without having a knowledge what are the exact classes implementing the interfaces. That‘s how the presenter would know only about the view interface (which abstracts the UI concepts) and not the view. In line 5 and 6 we can see that the presenter is subscribing to the view save and search events. The purpose of the first constructor is primary to enable appropriate unit testing of the presenter, but it won‘t be used in real world use cases. The reason why is like that lies in the fact that the view should be ignorant about the model in MVP pattern, so the view shouldn‘t know which model service class should inject to the presenter. Second constructor is the one used by a view and we can se that it is enough for a view to pass a reference to itself to a constructor and the Poor man dependency injection would occur in this(view, new UserServiceStub()) so the UserServiceStub instance pointer would be by default injected into the presenter. One additional benefit of having those two constructors could be that the first one could be used (together with mocking) in implementing unit tests and the second one could be called in integration tests (where we need tests to talk with real, non mocked model representations like database for e.g.) Presenter implementing view required logicWe have two methods in presenter which perform certain actions for a view, when view requests them. We saw that to request something from a presenter the view would raise an event and due to the fact that presenter is in its constructor subscribing to the those events, when the view would send event presenter private method would be executed NOTE: This is standard ‘Fowler like‘ event based
implementation of the view presenter communication. The other
(simpler) way how this could be implemented is that presenter could
define as public methods implementing the logic required by a view and
the view could directlly call them with something like:
_presenter.OnUserSave() without having the need for defining events and
related code concepts. The price of simplicity is that the presenter
broke his encapsulation from the view and that the view implementation
is now tied to the specific presenter implementation. For a lot of
people, this is fair trade off, so in my personal development I am
implementing it like that. OnUserSave method implementation 1: private void OnUserSave(object sender, EventArgs e)
2: {
3: if (_view.ContextData.IsDirty)
4: {
5: User user=new User();
6:
7: User domainUser = DataMapper.Translate(_view.ContextData);
8: _userService.SaveUser(domainUser);
9: }
10: }
11:
In line 3, we see the presenter reads the context object IsDirty property value to determine if data is changed from the loaded data and if persisting is needed and if the IsDirty is true, in line 7 the DTO objects is been mapped (translated) to the domain type and the service persist method is called after OnUserSearch method implementation 1: private void OnUserSearch(object sender, SearchEventArgs e)
2: {
3: if (string.IsNullOrEmpty(e.SearchCriteria))
4: {
5: _view.StatusMessage = "User name can not be null";
6: return;
7: }
8:
9: User user = _userService.GetUser(e.SearchCriteria);
10: if (user == null)
11: {
12: _view.StatusMessage = String.Format(
13: "There‘s no user found for user name:{0}", e.SearchCriteria);
14: return;
15: }
16:
17: UserDTO userDTO = DataMapper.Translate(user);
18: _view.ContextData = userDTO;
19: }
In line 3 we examine the value of the event parameter caring the lookup user name data and in case of null or empty value in line 5 we set the view interface StatusMessage property value (which would cause on view level setting up the status message) to appropriate error message. In line 9, presenter is accessing the Model through the UserService class GetUser method and in case of not existing user in line 12 sets the view interface message property to appropriate error message. Finally, if a user is been successfully retrieved from a user service, presenter performs in line 17 mapping (translation) of the domain user data to the DTO type of user data which is then set to the interface view ContextData property (which would cause on view level OnDataBound method execution) SummarySupervising Controller pattern is a UI design pattern which takes most complex responsibilities from the back of the web pages but still leaving them fair amount of autonomy. That makes it more suitable for implementing more complex UI pages/controls because having oversized presenter and undersized view can sometimes cause in teams overwhelming of developers working on presenter and leaves underused the one working on the view Source code of today‘s example can be found here: Supervising Controller source code What is next?Next part of the MVP close look post series would continue the MVP saga dissecting the Passive View variation of MVP pattern. Some of the classes and concepts presented in this post would be just referenced in i, so it would definitely be much shorter :) Quote of the day: |
|
来自: xiao huan > 《Patterns》