Applying the DRY Principle To Attribute Setting From Textual Data

by Finnian Reilly (modified: 2018 Apr 29)

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?

class MY_WET_CLASS create make feature {NONE} -- Initialization make (data_import: like data_export) do set_from_table (data_import) end feature -- Access boolean: BOOLEAN data_export: HASH_TABLE [STRING_32, STRING] do create Result.make (5) Result [Field_boolean] := boolean.out Result [Field_double] := double.out Result [Field_string] := string Result [Field_string_32] := string_32 Result [Field_integer] := integer.out Result [Field_natural] := natural.out Result [Field_real] := real.out end double: DOUBLE integer: INTEGER natural: NATURAL real: REAL string: STRING string_32: STRING_32 feature -- Element change reset_fields do boolean := False double := 0 natural := 0 integer := 0 real := 0 string.wipe_out string_32.wipe_out end set_from_table (data_import: like data_export) do if data_import.has_key (Field_boolean) then boolean := data_import.found_item.to_boolean end if data_import.has_key (Field_double) then double := data_import.found_item.to_double end if data_import.has_key (Field_integer) then integer := data_import.found_item.to_integer end if data_import.has_key (Field_natural) then natural := data_import.found_item.to_natural end if data_import.has_key (Field_real) then real := data_import.found_item.to_real_32 end if data_import.has_key (Field_string) then string := data_import.found_item else create string.make_empty end if data_import.has_key (Field_string_32) then string_32 := data_import.found_item else create string_32.make_empty end end feature -- Basic operations print_fields (lio: EL_LOGGABLE) do across data_export as field loop lio.put_labeled_string (field.key, field.item) lio.put_new_line end end feature {NONE} -- Variables Field_boolean: STRING = "boolean" Field_double: STRING = "double" Field_integer: STRING = "integer" Field_natural: STRING = "natural" Field_real: STRING = "real" Field_string: STRING = "string" Field_string_32: STRING = "string_32" end

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:

boolean: STRING Field_boolean: STRING = "boolean"

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 inherit EL_REFLECTIVELY_SETTABLE rename field_included as is_any_field, export_name as export_default, import_name as import_default end EL_SETTABLE_FROM_STRING_32 rename make_from_table as make, to_table as data_export end create make feature -- Access boolean: BOOLEAN double: DOUBLE integer: INTEGER natural: NATURAL real: REAL string: STRING string_32: STRING_32 end

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:

inherit EL_REFLECTIVELY_SETTABLE rename field_included as is_any_field, export_name as to_camel_case, import_name as from_camel_case end

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:

feature {NONE} -- Implementation export_to_english (name_in, english_out: STRING) do Naming.to_english (name_in, english_out, Upper_case_words) end feature {NONE} -- Constants Upper_case_words: ARRAY [STRING] once Result := << "http", "uri", "ok" >> end

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. class EL_HTTP_STATUS_ENUM inherit EL_ENUMERATION [NATURAL_16] rename export_name as to_english, import_name as import_default redefine initialize_fields, export_to_english end create make

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:

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.

feature {NONE} -- Implementation default_values: ARRAY [ANY] do Result := << create {PP_DATE_TIME}.make_now >> end

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.inherit EL_REFLECTIVELY_SETTABLE rename field_included as is_any_field end

Negative filtering

You can exclude specific fields by over-riding the once routine Except_fields as in this example:feature {NONE} -- Constants Except_fields: STRING once Result := Precursor + ", new_response" end

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.

test_set_country local transaction: PP_TRANSACTION do create transaction.make_default transaction.set_field_from_nvp ("address.country: Russia", ':') end

The set_field_from_nvp call is equivalent to the following Eiffel call:

transaction.address.set_country ("Russia")

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.