During last few days we are performing code reviews and making refactoring. Also we are working on TargetProcess:Migration tool that will simplify migration from TP 1.7 to TP 20. I think migration tool is not something worth to talk about, but some architecture problems might be interesting. The main point of review is Business Logic - Data Layer interactions. As you maybe know, TP 2.0 built on top of NHibernate, so we are trying to improve entity life-cycle by utilizing NHibernate power (that is why no posts during last week, we are sorry, the problem takes almost all time).
What we have right now.
As you see from diagram, the most important class in data layer is Portal (yes, it is biggest and thus looks pretty haughty). Portal is a single point of data access. It stands between business layer and NHibernate. Some things about Portal:
- It is a singleton
- It stores NHIbernate ISession
- It provides all operations for entities manipulation: Save, Delete, Retrieve.
Usage is pretty simple though:
UserStory story = UserStory.Create(); story.Name = "This is name " + index.ToString(); story.Description = "This is description " + index.ToString(); Portal.Instance.Save(entity);
The other important class is EntityObject - root class for all persistent entities in TP. It implements ILifeCycle interface and executes common operations on entities like validation.
Also we have TriggerInterceptor class that handles triggers invocations.
What else? We use common "session-per-request" pattern implemented via HttpModule. That's all about classes.
So, what we want to do is simplify entity life-cycle, since in current state it becomes quite weird.
What should we do with new entity before save into database? In general, several actions:
Check if entity is valid. For example, project should always have Name or EntityState object should not be final and initial at the same time. This performed in EntityObject.Validate() method which overloaded in every entity.
- Set Default/Known Values
If entity is new we should set CreatedDate, if entity is modified we should set ModifiedDate. This performed in Entity.OnUpdate() method which overloaded in every entity. For example, in General entity we have:
if (this.IsNew) this.CreateDate = DateTime.Now; if (!this.IsNew) this.ModifyDate = DateTime.Now;
- Execute Rules
Yes, we should execute some business rules in rare cases. For example, it is not possible to have two initial states for user story (this is business restriction at the moment). So if user add new initial state, we should find old initial state and set Initial = false; This performed in Entity.OnUpdate() as well.
- Execute Triggers
They are required for data integrity, but implemented in business layer to eliminate database dependency. For example, user story may contain several tasks and user story effort is a sum of all tasks effort. So when you add new task, you should recalculate user story effort. Triggers invoked in IInterceptor implementation in PostFlush method.
First of all, we want to make entity flow more explicit and transparent. We have both mechanisms for entity life-cycle management: ILifeCycle and IInterceptor, we want to leave only one mechanism.
Currently, we see several things that will simplify architecture. Remove ILifeCycle:
- Completely remove OnUpdate method. It takes too much responsibility.
- Move default values setting into entity constructor
- Move knows values setting into IInterceptor's OnFlushDirty method (in fact it is only two properties: ModifiedDate and LastEditor)
- Move rules invocation into IInterceptor's PostFlush if possible
- Move Validate invocation into IInterceptor
As a result, ILifeCycle interface implementation will be removed. The other solution is to remove IInterceptor implementation, but in this case we have to call triggers execution from Portal directly, which means that if entity will be saved by cascade by NHibernate, triggers will not be fired.
OK, it is midnight and I am slightly tired, so to be continued…