Overview
A classical way to develop data entry applications is to follow either an MVC or an MVP pattern. Using one of these patterns is a significant step forward from a pattern that most UI builders guide you towards, as it allows separation of business logic from presentation logic reducing complexity and enabling unit testing of the business logic separate from the UI. While these patterns are powerful, more structure is needed when building rich data entry applications. A typical data entry application has a few responsibilities and complexities.
Multiple entry portals for the same data( i.e. quick entry, detailed entry and API upload)
Similar fields appearing on multiple entry forms and having either identical or slightly different behavior
Field and model level validation rules that need to have a consistent look and feel
Context-based field calculation and defaulting rules that can vary in levels of complexity
To support these complexities, while maintaining consistent behavior between entry portals and between fields across multiple screens, we should create the following four layers of abstraction
Presentation field(s), responsible for defining datatypes, field level validations, calculation rules associated with the fields and mapping to domain objects
Product configuration(s), responsible for defining field list for a given product, relationship and dependencies between fields, product level validations as well as the relationship between product and domain objects
Presentation configuration(s), responsible for defining a subset of product fields made available for a particular data entry method. This will include display/presentation names of the fields.
Domain object(s), responsible for defining structure of the data as it’s stored in the database, one of it’s responsibilities would be to capture the core structure of the data normalizing diverse product-specific fields down to a relatively small set of core fields
Introduction to ReactiveFramework
In this section, I will introduce ReactiveFramework which will provide a strong definition around Fields, Product Model and interface for external systems( UI or API). To keep this article grounded in practice, I will use an example of FXTransaction entry where a user needs to specify two out of three fields, (primary amount, fx rate, and secondary amount) having the third field calculates automatically
First let’s define a few key components. Field class provides metadata definition behind the field and should uniquely represent a given field across multiple screens.
class Field:
def __init__(self, name, datatype, validation_method = None, calculation_method = None):
self.name = name
self.datatype = datatype
self.validation_method = validation_method
self.calculation_method = calculation_method
The Field class has four components
Field name that uniquely identifies the field.
Datatype, while python is flexible about datatypes of fields, having a strong notion of datatype for a given data entry field is key to improving overall quality and avoiding boilerplate code for ensuring type compatibility when performing calculations and validations of data. Developing in Python, we should ensure correct datatypes as the data is entering the system.
Validation and calculation methods will be bound to the relevant product model and used by the reactive framework to validate and calculate values of relevant fields.
The FieldFactory class is the repository of all of the fields in the system, in practice it should load field definition from a configuration source, but for the purpose of this example, I will include a very basic implementation to demonstrate field setup for FXTransaction example described above.
class FieldFactory:
FIELDS = [ Field(name = 'action', datatype = str, domain_name = 'action', validation_method = 'must_be_provided'),
Field(name = 'primary_amount', datatype = Decimal, domain_name = 'quantity', calculation_method = 'calc_primary_amount', validation_method = 'must_be_provided'),
Field(name = 'secondary_amount', datatype = Decimal, calculation_method = 'calc_secondary_amount', validation_method = 'must_be_provided'),
Field(name = 'deal_fx_rate', datatype = Decimal, calculation_method = 'calc_deal_fx_rate', validation_method = 'must_be_provided'),
Field(name = 'commission', datatype = Decimal, calculation_method = 'calc_commission', validation_method = 'must_be_provided'),
]
@classmethod
def getField(cls, field_name):
for field in cls.FIELDS:
if field.name == field_name:
return field
The ReactiveFramework class defines Public API and interactions supported by the Framework. For now, public interface will have a constructor which accepts Product Model to define fields used by the product and contain implementation of calculation and validation methods. It will have a set_field method which needs to be called to set value for a particular field, validate method which will return a list of validation errors and get_value method which will allow user to query value of fields defined in the model. Below is an example usage of the ReactiveFramework for construction and validation of FXTransaction.
>>>fxTrade = ReactiveFramework(FXTransaction())
>>>fxTrade.set_value('action', 'Buy')
[]
>>> fxTrade.set_value('primary_amount', 100)
["commission"]
>>>fxTrade.set_value('deal_fx_rate', 1.5)
["secondary_amount"]
>>> fxTrade.get_value("secondary_amount" )
150.0
>>> fxTrade.get_value("commission")
1.00
>>> fxTrade.validate()
{}
We construct an instance of the ReactiveFramework by passing in an instance of the Product Model, in this case FXTransaction. We then call set_value on the action field and back and an empty list in return. Empty list indicates that no other field depends on the action, implying that no calculations took place. Next we set_value on primary_amount and get back a list containing commission, this indicates that the commission field has been calculated based on the new value of primary_amount. Next we set deal_fx_rate and get back indication that secondary_amount was calculated. Then we check values of commission and secondary_amount to verify that they have been set. Finally, we call the validate method and get back and empty dict indicating that there are no validation errors.
The FXTransaction class defining the the Product Model is below.
class FXTransaction:
FIELD_DEPENDS = {
'action': [],
'primary_amount': ['secondary_amount', 'deal_fx_rate'],
'secondary_amount': ['primary_amount', 'deal_fx_rate'],
'deal_fx_rate' : ['primary_amount', 'secondary_amount'],
'commission' : ['primary_amount']
}
def bind_fields(self):
for field_name in self.FIELD_DEPENDS:
setattr(self, field_name, BoundField(FieldFactory.getField(field_name), self) )
def calc_commission(self):
return self.primary_amount.value * Decimal("0.01")
def calc_secondary_amount(self):
return self.primary_amount.value * self.deal_fx_rate.value
def calc_primary_amount(self):
if self.deal_fx_rate.value:
return self.secondary_amount.value / self.deal_fx_rate.value
def calc_deal_fx_rate(self):
if self.primary_amount.value:
return self.secondary_amount.value / self.primary_amount.value
def must_be_provided(self, field):
return "" if field.has_value else "%s is missing" % field.definition.name
FXTransaction defines fields that are part of the Product Model along with calculation and validation methods required by these fields. FXTransaction is loosely coupled to Fields and FieldFactory, bind_fields method validates correct field setup and establishes binding between field definition and model state.
When working within the ReactiveFramework, Fields and Product Models are the primary concern of the developer. When adding new screens or new functionality to the screens, the developer would create new or reuse existing fields and create or modify Product Models which define field interactions and dependencies.
A note on implementation, while it’s possible in python to derive dependencies implicitly from the calculation methods, in the opinion of the author, stating them explicitly has the benefit of simplicity, readability and flexibility which outweigh convenience of implicit dependency calculation.
|
No comments:
Post a Comment