The client domain objects (aka business entity - BE) must be related to the data contracts provided by the web-services. Still, they will need to be enhanced with e.g. dirty-tracking, validation, state, property conversions (xs:date), business rules, and other aspects needed to produce a fully working client application. This article describes how to achieve this when sharing domain objects across layers is not an option (i.e. interop with JEE services), and the same applies to AOP.
One option for implementing the BE objects, described in this article, is to apply the Mapper pattern to the WSDL scema-based DTO objects to transform them into domain objects. Read ‘Design of Client Domain Objects’ for an intro this option and the related Decorator option.
Service Proxy
The mapper option has no special requirements for the service proxy or its message and data contracts, as there are no shared objects between the proxy and the business entities, i.e. the proxy is totally self-contained. The mapper objects do all transformation to/from the DTO objects and the BE objects.
CAB EntityTranslatorService, CAB EntityMapperTranslator
The EntityTranslatorService class is a service that provides a registry of translators and translation services between two classes. The user must implement the translators and register them with the service.
For more information see: ms-help://MS.VSCC.v80/MS.VSIPCC.v80/ms.practices.scsf.2007may/SCSF/html/03-01-090-How_to_Translate_Between_Business_Entities_and_Service_Entities.htm
The EntityMapperTranslator class is a base helper implementation of an IEntityTranslator that provides placeholders to translate from business entities to service and viceversa.
For more information see: ms-help://MS.VSCC.v80/MS.VSIPCC.v80/ms.practices.scsf.2007may/SCSF/html/03-01-090-How_to_Translate_Between_Business_Entities_and_Service_Entities.htm
Mappers and the Translator Service
A mapper must be derived from the CAB class EntityMapperTranslator<BE, DTO> and implement two methods:
DTO = BusinessToService(BE)
BE = ServiceToBusiness(DTO)
A mapper must traverse the complete object graph to translate all child collections and objects when the mapped object is a DDD "aggregate root".
Always add a mapper for collections of the mapped DTO and BE objects. A list mapper must iterate the collection and use the applicable object translator to map each item in the list.
Each module must register its own mappers with the CAB translator service from the CAB module AddServices:
public override void AddServices()
{
AddEntityTranslators();
_workItem.RootWorkItem.Services.AddNew<MyService, IMyService>();
}
private void AddEntityTranslators()
{
IEntityTranslatorService translator = _workItem.RootWorkItem.Services.Get<IEntityTranslatorService>();
translator.RegisterEntityTranslator(new MyTranslator());
translator.RegisterEntityTranslator(new MyListTranslator());
. . .
}
The mappers are used by the service agents to map DTO objects to BE objects and vice verca. Each CAB service that depends on a service agent must have a [ServiceDependency] on IEntityTranslatorService:
[InjectionConstructor]
public MyService([ServiceDependency]IEntityTranslatorService translator)
{
_translator = translator;
_myServiceAgent = new MyServiceAgent(_translator);
}
MyService is a CAB service and will thus get the EntityTranslatorService injected by the DI-container. It then creates the required service agents using the translator service reference. MyServiceAgent is not a CAB service, thus it is not subject to any DI-container dependency resolving magic.
Service Agents Perform the Mapping
The service agents use the EntityTranslatorService to map the DTO objects returned by the service proxy into BE objects:
AllocationTypeList allocationTypeList;
NominationTypeList nominationTypeList = _proxy.GetNominationAndAllocation (periodType, out allocationTypeList);
nominationList = _translator.Translate <List<Nomination>>(nominationTypeList);
allocationList = _translator.Translate <List<Allocation>>(allocationTypeList);
The reverse mapping must be performed on all BE objects passed as DTO objects into the service proxies.
NEVER LET A DTO OBJECT BE USED OUTSIDE SERVICE AGENTS.
Mapping from DTO to BE
Your BE class must have a constructor to initialize the object for normal use. The constructor must initialize all properties that must have a defined initial value. In addition, the c'tor must initialize all child objects that are exposed as public properties, to avoid null reference exceptions when using the BE (make it simple to use the BE). This also applies to child collections; create the collection to make it ready for use. Don't break the DRY principle by adding the initialization code multiple places.
public Nomination()
{
//initialize all contained valueobjects with members
this.NominatedValueField = new UnitValueType();
this.NominatedValueField.Unit = String.Empty;
. . .
}
The mapper code will create a new instance of the BE class and initialize it based on values from the DTO object. This can involve the whole specter from simple value copying to complex conversion logic.
Mapping from BE to DTO
Your mapper class must initialize the created DTO object before using it. The mapper must initialize all properties that must have a defined initial value. In addition, the mapper must initialize all child objects that are exposed as (de-normalized) public properties, to avoid null reference exceptions when mapping the BE. This also applies to child collections; create the collection to make it ready for use.
//initialize all normalized valueobjects with members
dto.NominatedValueField = new UnitValueType();
dto.NominatedValueField.Unit = String.Empty;
Also initialize all DTO fields that are required by the XSD schema, failure to initialize these fields will give serialization errors:
[DataMemberAttribute(IsRequired=true, . . .)]
The mapper code will create a new instance of the DTO class and initialize it based on values from the BE object. This can involve the whole specter from simple value copying to complex conversion logic.
Converting XML Data Types to/from Strongly Typed Properties
Note that xs:date becomes string in .NET as there is no Date only type, just DateTime. DO NOT LET THIS KIND OF WEAK TYPE DESIGN LEAK INTO YOUR BE OBJECTS. Contain the weak typing in your DTO object by converting to/from strongly typed properties in the mapper.
dto.DemandDate = value.DemandDate.ToString(WSDateFormat);
entity.DemandDate = DateTime.ParseExact(value.DemandDate, WSDateFormat, null);
This keeps your BE object within the "Simple vs Easy" principle.
Dirty Tracking
If your BE implements INotifyPropertyChanged to enable data-binding, then the event is a good place to add dirty-tracking: Simple Dirty Tracking in Domain Objects.
Tuesday, April 15, 2008
Monday, April 14, 2008
Simple Dirty Tracking in Domain Objects
This article shows how to add dirty-tracking to domain objects (aka business entity - BE) by changing their code and adding a public bool IsDirty property. This design is simple and intrusive, and is best suited when you have to implement your BE objects yourself anyway. It requires no usage of BE interfaces, deriving and overriding, AOP, nor any DI-container.
If you cannot change the BE code, then consider implementing non-intrusive logic to compare a unit-of-work BE with its original values; either by reading the original BE from the repository and perform a recursive Equals(originalEntity) check of the BE's object graph, or by using an extended identity map that has original copies of the unit-of-work BE objects and performing the same Equals(originalEntity) check. The latter approach is better for client-side dirty-tracking; while the former is better suited for server-side implementations (get - compare - save).
Never implement dirty-tracking in client applications by using click events and the like, to set the dirty flag from view or presenter code. Implementing dirty-tracking in client code breaks both the SRP and DRY principles, first by making dirty-tracking a responsibility of the view/presenter instead of the BE object, and then by repeating dirty-tracking code across all places where the BE object is used.
If your BE objects implements INotifyPropertyChanged to enable data-binding, then the event is a good place to add dirty-tracking. Note that this dirty-tracking mechanism depends on all changes to the BE state being done through the property setters. Luckily, the _isDirty flag can also be set directly from anywhere within the BE code if necessary.
protected void RaisePropertyChanged(string propertyName)
{
//NOTE: must be here to run even if no handlers are registered
this._isDirty = true;
PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
If your BE is a DDD "aggregate root" and thus contains a complex object graph, then your BE should implement an IsDirty method, in addition to the private _isDirty member, that iterates through all child objects and checks their IsDirty status. Each object within the aggregate root must implement its own dirty tracking mechanism; either as just an IsDirty property for simple objects, or both an _isDirty flag and an IsDirty method for iterating its children also for complex objects.
public bool IsDirty()
{
if (this._isDirty) return true;
bool isDirty = this._isDirty;
foreach (Allocation allocation in _allocationList)
{
if (allocation.IsDirty) isDirty = true;
break;
}
return isDirty;
}
You could add event handlers to listen for the INotifyPropertyChanged event from contained child objects, but this is not recommended as you would need to add and remove a dynamic number of event handlers based on how many objects there are inside the aggregate root throughout its complex object graph - also when objects are added or removed from child collections.
If you cannot change the BE code, then consider implementing non-intrusive logic to compare a unit-of-work BE with its original values; either by reading the original BE from the repository and perform a recursive Equals(originalEntity) check of the BE's object graph, or by using an extended identity map that has original copies of the unit-of-work BE objects and performing the same Equals(originalEntity) check. The latter approach is better for client-side dirty-tracking; while the former is better suited for server-side implementations (get - compare - save).
Never implement dirty-tracking in client applications by using click events and the like, to set the dirty flag from view or presenter code. Implementing dirty-tracking in client code breaks both the SRP and DRY principles, first by making dirty-tracking a responsibility of the view/presenter instead of the BE object, and then by repeating dirty-tracking code across all places where the BE object is used.
If your BE objects implements INotifyPropertyChanged to enable data-binding, then the event is a good place to add dirty-tracking. Note that this dirty-tracking mechanism depends on all changes to the BE state being done through the property setters. Luckily, the _isDirty flag can also be set directly from anywhere within the BE code if necessary.
protected void RaisePropertyChanged(string propertyName)
{
//NOTE: must be here to run even if no handlers are registered
this._isDirty = true;
PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
If your BE is a DDD "aggregate root" and thus contains a complex object graph, then your BE should implement an IsDirty method, in addition to the private _isDirty member, that iterates through all child objects and checks their IsDirty status. Each object within the aggregate root must implement its own dirty tracking mechanism; either as just an IsDirty property for simple objects, or both an _isDirty flag and an IsDirty method for iterating its children also for complex objects.
public bool IsDirty()
{
if (this._isDirty) return true;
bool isDirty = this._isDirty;
foreach (Allocation allocation in _allocationList)
{
if (allocation.IsDirty) isDirty = true;
break;
}
return isDirty;
}
You could add event handlers to listen for the INotifyPropertyChanged event from contained child objects, but this is not recommended as you would need to add and remove a dynamic number of event handlers based on how many objects there are inside the aggregate root throughout its complex object graph - also when objects are added or removed from child collections.
Friday, April 11, 2008
DELL Support, XPS Motherboard
I have a rather new DELL XPS which motherboard broke a few weeks ago, a rather typical failure for XPS desktops according to DELL's support forum. Alas, DELL Norge will not help me since it is an XPS, even if I bought it from them using their site. Even better, you can only chat with DELL here in Norway if you want to buy something, not if you need support.
I called DELL's XPS premier support hotline, and an engineer and the required parts was dispatched to come to my home last friday. Due to this I had to be at home, waiting for the guy from DELL's Norwegian hardware service provider InfoCare. The day passed by, no engineer, no call, no information whatsoever. I contacted DELL premier XPS support again, and got a call this monday: "unfortunately, there had been a problem with dispatching the parts - sorry for not informing you". Yeah, right.
As I am on business travel three days a week currently, the engineer was dispatched to come this friday. This morning the InfoCare engineer called to say they were coming just after lunch. Guess what, at 15:30 he called again to tell me that "something came up, hehe". I'm really feeling that DELL premier support is doing their best to solve my problem to my satisfaction. Afterall, InfoCare's slogan is "The Art of Service" (sic!).
So, next week I have to spend another day home from work, waiting for the engineer to come and repair my XPS. Two days home from work, so far :-(
I will buy my next PC in a shop where I can talk with real people; and deliver and pick up the PC for repair myself - even if it takes weeks to fix it.
I called DELL's XPS premier support hotline, and an engineer and the required parts was dispatched to come to my home last friday. Due to this I had to be at home, waiting for the guy from DELL's Norwegian hardware service provider InfoCare. The day passed by, no engineer, no call, no information whatsoever. I contacted DELL premier XPS support again, and got a call this monday: "unfortunately, there had been a problem with dispatching the parts - sorry for not informing you". Yeah, right.
As I am on business travel three days a week currently, the engineer was dispatched to come this friday. This morning the InfoCare engineer called to say they were coming just after lunch. Guess what, at 15:30 he called again to tell me that "something came up, hehe". I'm really feeling that DELL premier support is doing their best to solve my problem to my satisfaction. Afterall, InfoCare's slogan is "The Art of Service" (sic!).
So, next week I have to spend another day home from work, waiting for the engineer to come and repair my XPS. Two days home from work, so far :-(
I will buy my next PC in a shop where I can talk with real people; and deliver and pick up the PC for repair myself - even if it takes weeks to fix it.
Thursday, April 10, 2008
Simple vs Easy Revisited
New client, new project, new developers - same old story on code monkey approach to designing and implementing objects.
I am again gently enforcing these two simple rules:
The objects must be simple to use, not (just) easy to make. E.g. do not expose a date as string even if XML xs:date types becomes strings in .NET and strings are easy to code for you - rather make it simple to use the object by converting the string to and from a real .NET DateTime value.
There is no such thing as "throw-away code" if it enters the source control system. You will not have time, due to time preasure, to come back at a later time to do things right in throw-away code that you write due to time preasure. Therefore all code that you check-in must be production quality code.
In addition, I try to enforce this principle: There should be no green code (comments) in the code. All comments should be considered a missed opportunity to use 'Refactor - Extract Method' and use of the comment as the basis for the new method name.
I am again gently enforcing these two simple rules:
The objects must be simple to use, not (just) easy to make. E.g. do not expose a date as string even if XML xs:date types becomes strings in .NET and strings are easy to code for you - rather make it simple to use the object by converting the string to and from a real .NET DateTime value.
There is no such thing as "throw-away code" if it enters the source control system. You will not have time, due to time preasure, to come back at a later time to do things right in throw-away code that you write due to time preasure. Therefore all code that you check-in must be production quality code.
In addition, I try to enforce this principle: There should be no green code (comments) in the code. All comments should be considered a missed opportunity to use 'Refactor - Extract Method' and use of the comment as the basis for the new method name.
Subscribe to:
Posts (Atom)