Applying the DRY Principle To Attribute Setting From Textual Data
- Tags:
- attribute setting
- attribute getting
- DRY
- DRY principle
- don't repeat yourself
- importing text-data
- exporting data as text
- class reflection
- reflection
- object reflection
- serialize
- deserialize
Introduction
Question: how often to you find yourself writing code similar to this example, which set attributes in an object from a string table with corresponding names?
In this example the data source is a string table, but it can be thought of as being representative of any source of textual name-value pairs. Personally I have written code like this many times. The problem with this code is that it violates the DRY principle on multiple levels:
From devIQ.com
The Don’t Repeat Yourself (DRY) principle states that duplication in logic should be eliminated via abstraction; duplication in process should be eliminated via automation.
Not only is this code repeating each field name 3 times as for example:
But it is also repeating the logic of bi-directional string conversion and output to console.
DRY Attribute Setting
Eiffel-Loop offers a DRY approach to object attribute getting/setting from textual data sources. Since the Summer of 2017 I have been retroactively replacing instances of WET code shown above with this equivalent code:
Class MY_DRY_CLASS contains 59 fewer lines of code than it's equivalent MY_WET_CLASS. The classes EL_REFLECTIVELY_SETTABLE and EL_SETTABLE_FROM_STRING_32 are found in Eiffel-Loop.
Some real-world examples:
class EL_REFLECTIVELY_SETTABLE*
AIA_CREDENTIAL_ID AIA_AUTHORIZATION_HEADER AIA_RESPONSE AIA_GET_USER_ID_RESPONSE AIA_PURCHASE_RESPONSE AIA_REVOKE_RESPONSE AIA_REQUEST* AIA_PURCHASE_REQUEST AIA_REVOKE_REQUEST AIA_GET_USER_ID_REQUEST EL_REFLECTIVELY_SETTABLE_STORABLE* EL_ENUMERATION* [N -> {NUMERIC, HASHABLE}] AIA_RESPONSE_ENUM AIA_REASON_ENUM EL_CURRENCY_ENUM EL_HTTP_STATUS_ENUM PP_L_VARIABLE_ENUM PP_PAYMENT_PENDING_REASON_ENUM PP_PAYMENT_STATUS_ENUM PP_TRANSACTION_TYPE_ENUM EL_COOKIE_SETTABLE EL_DYNAMIC_MODULE_POINTERS EL_IMAGE_UTILS_API_POINTERS EL_CURL_API_POINTERS FCGI_REQUEST_PARAMETERS FCGI_HTTP_HEADERS FCGI_SETTABLE_FROM_SERVLET_REQUEST MY_DRY_CLASS PP_ADDRESS PP_BUTTON_DETAIL PP_CREDENTIALS PP_REFLECTIVELY_SETTABLE PP_BUTTON_META_DATA PP_BUTTON_OPTION PP_HTTP_RESPONSE PP_BUTTON_SEARCH_RESULTS PP_BUTTON_QUERY_RESULTS PP_BUTTON_DETAILS_QUERY_RESULTS PP_PRODUCT_INFO PP_TRANSACTION
Variations
We could also have used the ancestor class EL_REFLECTIVE instead of EL_REFLECTIVELY_SETTABLE in the above example, but it lacks a make_default routine that is able to set a default value for string and string_32. It also does not have field_table directly available as an attribute of the class but instead must be looked up from a global once variable.
Some examples:
class EL_REFLECTIVE*
EL_BOOLEAN_REF PP_ADDRESS_STATUS EL_REFLECTIVE_RSA_KEY* EL_RSA_PRIVATE_KEY EL_RSA_PUBLIC_KEY EL_REFLECTIVELY_SETTABLE*
Word Separation Conventions
One practical concern is that when matching Eiffel attribute names with external data sources, the external source will most likely not use the Eiffel snake_case word separation convention. The most common one is camelCase but other possibilities include kebab-case, UPPER_SNAKE_CASE, or even UPPERCAMELCASE.
Class EL_REFLECTIVELY_SETTABLE has the capability to manage automatic conversion to and from any of the above mentioned conventions using a rename of export_name and import_name to any of the predefined routines. To import and export camelCase for example, you would write:
Names to English
There is also the case where you might want to export the attribute names as English words with spaces as in this class: EL_HTTP_STATUS_ENUM which enumerates HTTP status codes. In this case we have specified which words we want to be upper-cased in the exported name by redefining the routine export_to_english:
Enumerations
This leads us to the topic of reflective enumerations as class EL_HTTP_STATUS_ENUM is an example of a class which inherits EL_ENUMERATION which is another application of class EL_REFLECTIVELY_SETTABLE.
If you do not supply the enumeration values yourself by redefining initialize_fields, EL_ENUMERATION will provide default values.
Examples:
class EL_ENUMERATION*
EL_CURRENCY_ENUM PP_PAYMENT_STATUS_ENUM PP_TRANSACTION_TYPE_ENUM PP_PAYMENT_PENDING_REASON_ENUM EL_HTTP_STATUS_ENUM PP_L_VARIABLE_ENUM
Conversion of Non-standard Fields
In addition to providing string conversion for all of the standard Eiffel types (plus EL_ZSTRING from Eiffel-Loop) as follows:
- expanded numeric types
- boolean
- pointer
- 32 bit and 8 bit strings
EL_REFLECTIVE also provides conversion support for reference types conforming to type EL_MAKEABLE_FROM_STRING.
Examples:
class EL_MAKEABLE_FROM_STRING*
EL_MAKEABLE_FROM_STRING_8* AIA_CREDENTIAL_ID EL_BOOLEAN_REF PP_ADDRESS_STATUS EL_ENUMERATION_VALUE* AIA_PURCHASE_REASON EL_CURRENCY_CODE PP_PAYMENT_PENDING_REASON PP_PAYMENT_STATUS PP_TRANSACTION_TYPE EL_ENCODING EL_UUID EL_MAKEABLE_FROM_STRING_32* EL_MAKEABLE_FROM_ZSTRING*
Reference Initialization
The class EL_REFLECTIVELY_SETTABLE is able to provide a default value for a reference field if the field meets any of these conditions:
- It's a string
- It conforms to EL_MAKEABLE
- It conforms to EL_MAKEABLE_FROM_STRING
- It conforms to EL_ENUMERATION_VALUE
Sometimes the default initialization will fail for one of the following 2 reasons:
1. a class invariant gets in the way of calling an initialization routine after the object has already been instantiated with {INTERNAL}.new_instance. (This is a general problem with Eiffel that is worthy of a discussion.)
2. The type is unknown to the reflection system
For cases where the default initialization fails you can do 1 or 2 things:
1. Redefine make_default (or initialize_fields) and create a default value for the field before calling the Precursor. The routine initialize_fields will not over-write fields that have already been initialized.
2. Redefine {EL_REFLECTIVE}.default_values and provide an array of default values for the types that are failing to initialize. See class PP_TRANSACTION for an example.
Field Filtering
There are two ways to specify which fields in a class you wish to be accessible via reflection.
Positive filtering
Routine field_included specifies positively the fields you wish to include and is usually set to 'is_any_field' in the renaming clause.
Negative filtering
You can exclude specific fields by over-riding the once routine Except_fields as in this example:
The Dot Member Operator
The family of classes conforming to EL_SETTABLE_FROM_STRING also offer the possibility to set an attribute in a nested reflective object using the standard dot notation. Take for example the Paypal transaction class PP_TRANSACTION. This class has an attribute address: PP_ADDRESS which is also a reflectively settable type. It is possible to write something like this to set the country attribute in the address.
The set_field_from_nvp call is equivalent to the following Eiffel call:
If you look at the routine {PP_TRANSACTION}.set_name_value you can see how variables prefixed with "address_" are mapped to the address attribute.
Application to properties file
Using this feature it would be easy to implement a Java style properties configuration file in Eiffel.
# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
String Templates
Class EL_REFLECTIVE is also useful in conjunction with the EL_SUBSTITUTION_TEMPLATE family of classes. See {AIA_AUTHORIZATION_HEADER}.as_string for an example of reflective template substitution.
Runtime Efficiency
The classes EL_REFLECTIVE and EL_REFLECTIVELY_SETTABLE have been carefully designed to be as efficient as possible. All the field meta information is cached in a global once variable so once an reflective-object has been initialized, no further objects are created requiring garbage collection. Especially no extra name string objects are created during adaptations for foreign word separation conventions.