Responding to Effectus commentary
Jose was kind enough to post a review of my sample Effectus application. This post is a reply to that.
The first thing that Jose didn’t like was the fact that I didn’t put an abstraction layer in front of NHibernate’s usage in my application. There are several reasons for that, the first, and simplest, is that I was trying to explain how to use NHibernate, and for that, I wanted to deal with the lowest common denominator, not show off a particular wrapper implementation. Showing NHibernate usage directly means that even if you are using another library with it, you would still be able to take advantage of the information that I give you.
Second, and quite important, is that by using NHibernate directly I can take advantage of NHibernate features explicitly meant to support certain scenarios, but which are not likely to be expose when libraries wrap NHibernate. A good example of that is in Jose’s sample code, which make use of an ISession instead of IStatelessSession to load the data for the main screen. As explained in the article, the difference between the two is important, and in the context where it is used it introduce what is effectively a memory leak into Jose’s implementation, as well as the chance for some really interesting errors down the road if the session will run into an error.
Third, Jose bring up the following:
This presenter is GLUED to NHibernate. And this means for instance that you can’t test it without NHibernate, and this means that you can’t change your persistence layer.
Yes, and that is intentional. That is the very task that those presenters are for. Trying to abstract that away just means that I put an additional layer of abstraction that does absolutely nothing. For that matter, let us look at Jose’s implementation using an abstraction layer compared to mine.
Here is my implementation:
private void LoadPage(int page) { using (var tx = StatelessSession.BeginTransaction()) { var actions = StatelessSession.CreateCriteria<ToDoAction>() .SetFirstResult(page * PageSize) .SetMaxResults(PageSize) .List<ToDoAction>(); var total = StatelessSession.CreateCriteria<ToDoAction>() .SetProjection(Projections.RowCount()) .UniqueResult<int>(); this.NumberOfPages.Value = total / PageSize + (total % PageSize == 0 ? 0 : 1); this.Model = new Model { Actions = new ObservableCollection<ToDoAction>(actions), NumberOfPages = NumberOfPages, CurrentPage = CurrentPage + 1 }; this.CurrentPage.Value = page; tx.Commit(); } }
And here is Jose’s:
private void LoadPage(int page) { var actions = _toDoActionsDao.RetrieveAll() .Skip(page * PageSize) .Take(PageSize).ToList(); var total = _toDoActionsDao.RetrieveAll().Count(); NumberOfPages.Value = total / PageSize + (total % PageSize == 0 ? 0 : 1); Model = new Model { Actions = new ObservableCollection<ToDoAction>(actions), NumberOfPages = NumberOfPages, CurrentPage = CurrentPage + 1 }; CurrentPage.Value = page; }
The code is still doing the exact same thing, in other words, we haven’t moved any logic away, and most of the code is dealing with data access details. My approach for testing this is to make use of an in memory database, which result in a single test that make sure that the code works, instead of a series of tests that verifies that each piece work independently and then my single test. I find it much more effective in terms of time & effort.
As for the idea of changing your persistence layer, forget about it. It isn’t going to happen in any real world application without a lot of effort, so you might as well save yourself the effort of working with the lowest common denominator and take full advantage of the framework. Just to note, when I ported the NerdDinner code to NHibernate (an application that make use of a single table), I had to make major changes to the application.
Jose didn’t like this method:
public void OnSave() { bool successfulSave; try { using (var tx = Session.BeginTransaction()) { // this isn't strictly necessary, NHibernate will // automatically do it for us, but it make things // more explicit Session.Update(Model.Action); tx.Commit(); } successfulSave = true; } catch (StaleObjectStateException) { var mergeResult = Presenters.ShowDialog<MergeResult?>("Merge", Model.Action); successfulSave = mergeResult != null; ReplaceSessionAfterError(); } // we call ActionUpdated anyway, either we updated the value ourselves // or we encountered a concurrency conflict, in which case we _still_ // want other parts of the application to update themselves with the values // from the db EventPublisher.Publish(new ActionUpdated { Id = Model.Action.Id }, this); if (successfulSave) View.Close(); }
Because:
- ReplaceSessionAfterError, to much responsibility for a presenter.
- Session/Transaction again.
- There is a BUG, the publish mechanism work in sync with the rest of the code. This means… that this windows is not going to close until others have finished handling the event. For the given example, the Edit windows is not going to close until the main window finish to refresh the current page.
- Too much logic for this method = hard to test.
I fully agree that this is a complex method, and Jose’s refactoring for that is an improvement indeed.
[PersistenceConversation(ConversationEndMode = EndMode.End)]
public virtual void OnSave() { _toDoActionsDao.Update(Model.Action); EventPublisher.Enlist(new ActionUpdated { Id = Model.Action.Id }, this); View.Close(); } public override void OnException(Exception exception) { if(exception is StaleEntityException) { Presenters.ShowDialog<MergeResult?>("Merge", Model.Action); EventPublisher.Enlist(new ActionUpdated { Id = Model.Action.Id }, this); } }
Jose has implemented the following changes:
- I’ve defined a new convention, if a public method in the presenter throw an exception, the OnException method will be called. This is done by a castle interceptor, and this is the last line of defense for unhandled exceptions.
- I’m using “StaleEntityException” rather than “StaleObjectStateException”, this is MY exception. This is easily done by a CpBT artifact.
- I’m not calling “EventPublisher.Publish” anymore, this code use EventPublisher.Enlist. Here, I’ve split the “Publish” code in two different methods one for Enlist and other for Raise. The enlisted events will be raised right after the OnSave method is called and thus after the windows is closed.
- Also, notice that here is the conversation per business transaction pattern with all its splendor. The two former methods are conversation participants, with EndMode equals to continue. This means that the NH Session will remain opened. The OnSave method has EndMode equals to End, this means that right after the method finished, CpBT internally will flush the Unit of Work and close it.
This is better than the original implementation, but I think it can be made better still. First, OnException as a generic method is a bad idea. For the simple reason that the exception logic for different method is different, I would probably define a [MethodName]Error(Exception e) convention instead, which would make it easier to separate the error logic for different methods.
Again, I don’t find any usefulness in abstracting the underlying framework, I haven’t seen a single case where it was useful, but I have seen a lot of cases where it was hurting the team & the project.
The idea about splitting the publication and raising is really nice, I agree.
However, there is a problem in the code with regards to session handling in cases of error. There is a pretty good reason why I introduced the ReplaceSessionAfterError method. In general, I want to keep my session alive for the duration of the form, because I get a lot of benefits out of that. But, if the session has run into an error, I need to replace it and all the objects associated with it. Closing the session no matter what is going on is not a good solution, and you can’t really solve the problem in a generic way without calling back to the presenter that generated the error.
Comments
First off, thank you very much for answering my review.
My opinion:
A-About the abstraction of the framework, I see your point, and I can't add much more.
B-About the ISession/IStatelessSession, yes there is a difference, and yes in my code there is not an easy way to use IStatelessSession in the presenters (you can use stateless in the dao's) . However, I want to remark that the LoadPage method has EndMode = END, this means that the session is closed and disposed right after the method execution, and every call to that method will open a new session, use it, and dispose it. I don't keep the session opened in this presenter. Do you think that the memory leak is still considerable in this case?
C-I really like [MethodName]Error(Exception).
D-About the session handling in case of session errors, now I see the benefit. My example can handle this if we call Initialize in the OnException method (or OnSaveException). CpBT will close the session after the exception, and in the next call to Initialize will open a new one.
Thank you again for answering and for giving me the opportunity to learn. Which is the main reasons I run my blog.
A side note about Effectus.
The thing that impressed me is the convention used to wire up View to Presenter. That is amazing!
I don't like much the current implementation. But the idea is GREAT,
What do you think of writing a framework for that purpose?
I think that should include some annotations for methods (attributes) and compile time checking. So, IMO, PostSharp is the best choice at the beginning.
Thanks again for that idea.
P.S. Also I don't like the .View, .Presenter, .Model convention. MainView, MainPresenter, .MainModel much better but I don't like it too :)
Andrei,
Feel free to write it up, I currently don't need this sort of framework
@Rob an event publisher component like the one Ayende built for effectus will be nice for caliburn (if not already have).
One benefit that can be gained from the abstraction is applying business rules at the aggregate root level, and ensuring that only aggregate roots are saved.
If you only create repositories for aggregate roots, and don't allow direct access to nhibernate session from the presenter, then you can ensure that only aggregate wholes are persisted.
There is something to be said for the ddd practices
Kudos Ayende,
You are one person I know who doesn't code with dogmas but real pragmatism. Kudos again!
@Onur Ayende's approach is interesting but the life is not black and white. However, if you want to be a real pragmatic developer. Better you forget about the Separation of Concern dogma and start to develop in the event handler of a button.Click with ado.net.
Oren,
Chreian,
This isn't mine, but it looks like it would be appropriate.
github.com/.../Nerd-Dinner-with-Fluent-NHibernate
Comment preview