Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Java

functionExtensions Techniques 3: Repository

4.67/5 (2 votes)
9 May 2018CPOL17 min read 5.1K  
This is the third of three episodes to introduce considerations and techniques applied in Repository classes in my open-sourced library functionExtensions.

Introduction

functionExtentions is a Java library with Throwable Functional Interfaces, Tuples and Repositories implemented to expedite Functional Programming with JAVA 8. It is released on Maven, with following goals accomplished:

  • Declares a rich set of functional interfaces throwing Exceptions, that can be converted to conventional ones with Checked Exceptions handled with shared exceptionHandlers, thus allow developers to define the concerned business logic only within the lambda expressions.
  • Implements an immutable data structure to keep and retrieve up to first 20 strong-typed values as a set of Tuple classes.
  • Provides Repositories as a kind of Map-based intelligent utility with pre-defined business logic to evaluate a given key or keys (upto 7 strong-typed values as a single Tuple key) to get corresponding value or values (upto 7 strong-typed values as a single Tuple value), buffer and return them if no Exception happened.
  • Multiple powerful generic utilities to support above three types utilities, mainly build with Repositories, that support various combinations of primitive and objective types and arrays. For example:
    • Object getNewArray(Class clazz, int length): new an array of ANY type with element type and length of the newed instance.
    • String deepToString(Object obj): Returns a string representation of the "deep contents" of the specified array.
    • Object copyOfRange(Object array, int from, int to): Copy all or part of the array as a new array of the same type, no matter if the array is composed by primitive values or not.
    • T convert(Object obj, Class<T> toClass): convert the object to any equivalent or assignable types.
    • boolean valueEquals(Object obj1, Object obj2): comparing any two objects. If both are arrays, comparing them by treating primitive values equal to their wrappers, null and empty array elements with predefined default strategies.

This serial of posts include:

  1. Throwable Functional Interfaces
  2. Tuples
  3. Repositories

Introduction

In this post, I would discuss the design goals, considerations, implementations of Repository classes that use a generic function as the core of a JAVA Map to evaluate, cache and retrieve a collection of data as the value resulting from another collection of data as the key.

It starts with Lazy<T>, followed by Repository<TKey, TValue> and more strong-typed TupleRepository classes, and finally some powerful utilities backed by them as example of how to implement complex utilities with this library.

Background

The Lazy Initialization is defined on Wiki as: lazy initialization is the tactic of delaying the creation of an object, the calculation of a value, or some other expensive process until the first time it is needed. It is a kind of lazy evaluation that refers specifically to the instantiation of objects or other resources.

In my opinion, deferring initialization to use time to speed up launching of the application is just one of the benefits we can achieve from the late evaluation, usually it is possible to:

  • Wrap the data retrieval within the Lazy instances, thus users of the concerned objects can focus on using them, instead of creating them.
  • Caching, when combined with the underlying memorization mechanism, to enhance processing efficiency.
  • Potentially get the involved resources managed easily if AutoCloseable is implemented properly.
  • Similar ideas could be applied with Map and Tuple to make a set of data Lazy evaluable.

Lazy<T>

The Lazy<T> implementation is based on the techniques discussed in my first post of Throwable Functional Interfaces: by saving the business logic of getting the interested data, instead of the data to construct itself, then evaluate the business logic only when the data is about to be used.

Notice: In the current version, the Lazy<T> implementation is not thread-safe.

The implementation of Lazy<T> is quite simple, by keeping an instance of final SupplierThrowable<T> supplier, which plays the same role of Func<T> of Lazy(T) in C#, the Lazy<T> allows throwing Exceptions to make the users focus on the business logic.

Then, there are four supportive fields to keep strong-typed data caching and AutoCloseable support:

Java
private boolean isInitialized = false;
private T value;
private boolean isClosed = false;
protected List<AutoCloseable> dependencies;

The idea behind is quite simple, keeping the method to get concerned value, evaluate only when its users really need the value by calling T getValue() public method, then:

  • Usually the business logic kept in supplier shall be triggered successfully to get the actual data, that would be cached by the value field for later use.
  • If there is any Exception happen, the Exception Handling mechanism defined by the Functions.Default would be used to:
    • either throw RuntimeException
    • or return default value when any Exception happens

As Tuple classes discussed in my previous post, the Lazy<T> can release resources of its value by implementing AutoCloseable interface.

Repository<TKey, TValue>

Lazy<T> can be used as a cache of a single value, that is apparently not sufficient to support Recursive Programming (Recursion in computer science is a method where the solution to a problem depends on solutions to smaller instances of the same problem).

As a collection of KeyValuePairs where one unique Key has an associated Value, the super-efficient hash function and collision resolution of JAVA Map ensure the quick access of values of any given keys. When treating the keys as conditions provided to some business logic to evaluate, and the values as the evaluation results, Map is much faster and extensible than the JAVA conditional operators of switch and If+Else. The Repository classes are designed to embed business logic with this generic and well-organised data structure to achieve even higher productivities.

Background

To solve real-life problems with JAVA, Map is my favourite media to keep interim results, especially when these results are obtained by a single method with a fixed pattern as illustrated with the following pseudo codes:

Java
Map<String, SomeClass> cache = new HashMap();

public SomeClass get(String key){
  if(!cache.containsKey(key)){
     SomeClass newInstance = doSomethingWith(key);  //Use the concerned business logic
                                                    //to retrieve value of key
     cache.put(key, newInstance);
     return newInstance;
  }
  return cache.get(key);
}

From my perspective, all the above codes are duplicated except the business logic being called, SomeClass doSomethingWith(key) in the above example, thus shall be replaced with a more generic caching utility - Repository<TKey, TValue>. The naming follows my C# project DataCenter when cache is not available, please advise me if you have a better name for this utility.

Implementation

As a very important concept in functional programming, immutability is assumed when using Repository classes discussed in this section, and it means:

  • The keys to be evaluated shall be immutable;
  • The value evaluated from a given key can never be different at different time.

The Repository<TKey, TValue> class is designed as an extension of FunctionThrowable<TKey, TValue>. Under the hood, it depends on the final Map instance to locate and buffer KeyValuePairs, but use its own intelligence to calculate values of given keys.

The intelligence is a pre-defined method of final FunctionThrowable<TKey, TValue>, that can be called directly if there is no recursive calling happen, is provided when constructing the Repository instance. As discussed in Throwable Functional Interface, defining only the kernel business logic to get value from key, this method could throw Exceptions, it would:

  • If the keys have been evaluated successfully before: then retrieve the cached values and return directly;
  • Otherwise, the given keys would be evaluated with the pre-defined business logic:
    • If the evaluation is successful, then the result would be saved as the value of the given key before returning it back to the caller;
    • If the evaluation is failed with some Exception, then the caller needs to handle the thrown Exception by either:
      • Handling them directly in try{}catch{} blocks when querying the key directly by calling TValue apply(TKey tKey) throws Exception.
      • Alternatively, by providing a default value when Exception happened, the Exception would be ignored when calling TValue get(TKey tKey, TValue defaultValue).

When there are some keys that cannot be evaluated conveniently by the pre-defined method, it is advised to use a Map with pre-populated KeyValuePairs as applied by many utilities in this library that would be discussed later.

It is also possible to update the Map to associate a key with new value to replace old value by calling TValue update(TKey tKey, TValue existingValue, TValue newValue) where existingValue is null if the key has not been evaluated.

To remove certain KeyValuePairs, the int clear(BiPredicate<TKey, TValue> keyValuePredicate) shall be called with the keyValuePredicate indicating one or multiple pairs to remove.

There are no special techniques applied with the implementation of Repository<TKey, TValue>, a simple test would show uses of its operators:

Java
Repository<String, Integer> repository = new Repository<>(s -> s.length());
assertEquals(Integer.valueOf(0), repository.apply(""));
assertEquals(Integer.valueOf(1), repository.apply("a"));
assertEquals(Integer.valueOf(3), repository.apply("abc"));

assertEquals(Integer.valueOf(33),
   repository.update("abc", Integer.valueOf(3), Integer.valueOf(33)));
assertEquals(Integer.valueOf(33), repository.apply("abc"));

//Insert new value with oldValue as null
assertEquals(Integer.valueOf(-1), repository.update("xyz", null, -1));
assertEquals(Integer.valueOf(-1), repository.apply("xyz"));

TupleRepositories

The Generic KeyValuePairs stored by the JAVA Map, by default, is composed by a strong-typed Key and a strong-typed value associated with the key and that exposed two inconveniences:

  1. Most JAVA methods would accept multiple input arguments to return a single value as result, the Map cannot keep all input arguments as a single key directly.
  2. The JAVA methods can only return 0 or 1 value as the evaluation result, thus multiple Maps with identical set of Keys are needed to keep more than one associated value.

The immutable, highly efficient comparable by actual values, composed by any number of elements with strong-typed accessors, the Tuples classes are ideal to:

  1. Keep a set of one or many elements as a single strong-typed Tuple key of the Map;
  2. Preserve the associated one or many elements as a single strong-typed Tuple value returned by the given strong-typed function combining all evaluated results as a single Tuple.

By extending the Repository class, the TupleRepository<TKey> provides a partial implementation of the above objective:

Java
public class TupleRepository<TKey> extends Repository<TKey, Tuple> {
    protected TupleRepository(FunctionThrowable<TKey, Tuple> valueFunction){
        this(new HashMap(), null, valueFunction);
    }

    public Tuple retrieve(TKey key){
        return get(key, null);
    }

    @Override
    public Tuple update(TKey tKey, Tuple existingValue, Tuple newValue) throws Exception {
        Objects.requireNonNull(newValue);
        return super.update(tKey, existingValue, newValue);
    }
...
}

There is only one generic type that shall be provided to specify the key, and the values are just type-rased Tuple type that can be of any length and any types of elements. Though the flexibility accompanied with such ambiguous could be exploited in some special scenarios, Tuple based Repository types supporting multi-keys and multi-values is more practical for most applications.

Map of Multi-Keys to Multi-Values

In this version (1.0.1) of the library, there are upto 7 elements that can be used to compose the keys of the Map, and upto 7 elements can be used to compose the values: there are at least 7 * 7 = 49 generic classes that need to be defined along with numerous strong-typed accessors to retrieve the elements at position 1 to 7 of the associated values.

That would be a nightmare even worse than what I have faced to enable strong-typed accessors of Tuples. Fortunately, just as sharing duplicated accessors between classes as default methods of inherited interfaces as a chain, the same technique is used first to share them within the TupleRepositories.

To start with the simple case to tackle a single generic type as the Key, the implementation looks like:

Java
public interface TupleValues<TKey> {
    Tuple retrieve(TKey key);
    boolean containsKey(Object key);
}
public interface TupleValues1<TKey, T> extends TupleValues<TKey> {
    default T getFirstValue(TKey key) {
        Tuple tuple = retrieve(key);
        if(tuple == null)
            return null;
        return (T) tuple.getValueAt(0);
    }
}
...
public interface TupleValues7<TKey, T,U,V,W,X,Y,Z> extends TupleValues6<TKey, T,U,V,W,X,Y> {
    default Z getSeventhValue(TKey key) {
        Tuple tuple = retrieve(key);
        if(tuple == null)
            return null;
        return (Z) tuple.getValueAt(6);
    }
}

That is almost identical to the implementation of Tuples. However, it does not help to retrieve the first, second ... seventh element with multiple inputs as the keys. For example, when the keys are composed by two elements, how can I access the elements of associated values with both elements as input? Apparently, with JAVA, there must be some methods defined like T getFirst(K1, K2), U getSecond(K1, K2) ... Z getSeventh(K1, K2) to make the corresponding Repositories easy to use.

Again, it is enabled by default methods of inherited interfaces. To save the needs to name these interfaces, as well as to make the codes easy to copy+update+paste, TupleKeys is defined first as a simple interface:

Java
public interface TupleKeys <TKey> {
    boolean containsKey(Object key);
}

Then TupleKeys1, TupleKeys2, ... TupleKeys7 are defined by extending TupleKeys with their detail types of the key elements to cope with Tuples composed by 1, 2 ... 7 elements respectively. Taken TupleKeys2 for example, as well as its only two default methods are as below:

Java
public interface TupleKeys2<K1,K2> extends TupleKeys<Tuple2<K1,K2>> {
    default Tuple2<K1, K2> getKey(K1 k1, K2 k2){
        return Tuple.create(k1, k2);
    }
    default boolean containsKeyOf(K1 k1, K2 k2){
        return containsKey(getKey(k1, k2));
    }...

Then similar to the above inherited interfaces of TupleValues1, TupleValues2, ... TupleValues7, another chain of its internal interfaces sharing the same name are defined with all generic types of elements composing both keys and values:

Java
interface TupleValues1<K1,K2, T> extends TupleKeys2<K1,K2>,
        io.github.cruisoring.repository.TupleValues1<Tuple2<K1, K2>, T> {

    default Tuple retrieve(K1 k1, K2 k2){
        return retrieve(getKey(k1, k2));
    }
    default T getFirst(K1 k1, K2 k2) {
        return getFirstValue(getKey(k1, k2));
    }
}

interface TupleValues2<K1,K2, T,U> extends TupleValues1<K1,K2, T>,
        io.github.cruisoring.repository.TupleValues2<Tuple2<K1, K2>, T,U> {
    default U getSecond(K1 k1, K2 k2) {
        return getSecondValue(Tuple.create(k1, k2));
    }
}
...
interface TupleValues7<K1,K2, T,U,V,W,X,Y,Z> extends TupleValues6<K1,K2, T,U,V,W,X,Y>,
        io.github.cruisoring.repository.TupleValues7<Tuple2<K1,K2>, T,U,V,W,X,Y,Z> {
    default Z getSeventh(K1 k1, K2 k2) {
        return getSeventhValue(getKey(k1, k2));
    }
}

The same happened with corresponding internal interfaces of TupleKeys1, TupleKeys2, ... TupleKeys7 to form a matrix of 1-7 inputs and 1-7 outputs.

There are several interesting points to be noticed:

  • The abstract method Tuple retrieve(TKey key) of TupleValues has been called by the default method of Tuple retrieve(K1 k1, K2 k2) to get the Tuple value.
  • The same is true with T getFirstValue(TKey key) of TupleValues1 by T getFirst(K1 k1, K2 k2) of TupleKeys2.TupleValues1, as well as getSecond()... getSeventh(): all rely on the default methods of TupleValues1, TupleValues2, ... TupleValues7 directly after extending them as well as the previous internal interface within TupleKeys2.
  • The codes represented a clear pattern, actually most these files are generated by updating one file with Regex and saving with other names.

The TupleRepository classes (TupleRepository1, TupleRepository2 ...TupleRepository7) are defined to use Tuple1<T>, Tuple2<T,U>...Tuple7<T,U,V,W,X,Y,Z> to keep 1-7 elements composed Tuple as values respectively. Then by following similar pattern, their internal classes use duplicated names of TupleKeys1, TupleKeys2, ... TupleKeys7 to hold the strong typed Tuples to keep 1-7 elements together as the keys. With the assumption of Tuple's immutability and comparability, any set of strong-typed values as an unique key, should always be matched with another strong-typed values as associated unique value.

Taken TupleRepository2 for example, which takes care Repository whose values are composed by two elements, and from any generic type of Key:

Java
public class TupleRepository2<TKey, T, U>
        extends Repository<TKey, Tuple2<T,U>>
        implements TupleValues2<TKey, T, U> {
@Override
public Tuple2<T,U> retrieve(TKey key) {
    return get(key, null);
}
@Override
public Tuple2<T,U> update(TKey tKey, Tuple2<T,U> existingValue,
       Tuple2<T,U> newValue) throws Exception {
    Objects.requireNonNull(newValue);
    return super.update(tKey, existingValue, newValue);
}
...
}

Its Tuple2<T,U> retrieve(TKey key) has actually overridden the abstract method Tuple retrieve(TKey key) of TupleValues thanks to Java Type Erasure, and by calling get(key, null) with the default value of null, the TupleRepository classes would always return null when evaluation got any Exception.

Then its static internal class TupleKeys3<K1,K2,K3, T,U> is implemented with more specific key elements type info to denote a Repository whose Keys are Tuples composed by 3 elements of type K1, K2, K3, and Values are Tuples composed by 2 elements of type T and U.

Java
public static class TupleKeys3<K1,K2,K3, T,U> extends TupleRepository2<Tuple3<K1,K2,K3>, T,U>
        implements io.github.cruisoring.repository.TupleKeys3.TupleValues2<K1,K2,K3, T,U> {

    protected TupleKeys3(Map<Tuple3<K1,K2,K3>, Tuple2<T,U>> map,
                         TriConsumerThrowable<Tuple3<K1,K2,K3>,
                         Tuple2<T,U>, Tuple2<T,U>> changesConsumer,
                         TriFunctionThrowable<K1, K2, K3, Tuple2<T, U>> valueFunction) {
        super(map, changesConsumer, triple -> valueFunction.apply(triple.getFirst(),
              triple.getSecond(), triple.getThird()));
    }
    protected TupleKeys3(TriFunctionThrowable<K1, K2, K3, Tuple2<T, U>> valueFunction) {
        this(new HashMap(), null, valueFunction);
    }
}

Even with only constructors defined, by simply extending TupleKeys3.TupleValues2 interface, the useful strong typed single value element accessors T getFirst(K1 k1, K2 k2, K3 k3) and U getSecond(K1 k1, K2 k2, K3 k3) , along with other default methods, are equipped for free.

All the TupleRepository classes are defined with protected constructor, and the factory methods shall be used to create the instances with the key-to-value retriever method, along with optional Map with pre-populated KeyValuePairs.

Organizing methods in this way, not only by defining them as default methods of inherited interfaces, but also contained by internal interfaces/classes as a matrix, there are some obvious benefits:

  • No duplicated codes for these dozens of classes
  • Expandable codes that can be expanded with the simplified naming schemes within the source files
  • No performance hit observed during compiling or running

Some possible disadvantages:

  • The methods are not easy to locate when reviewing the whole projects.
  • The complex inheritance structure might cause JavaDoc to consume 5.6G memory to run, I have reported it as a bug to Oracle.

Anyway, the techniques could be considered as an option to structure a group of classes sharing similar methods.

How to Use

The project is hosted at github.com, to include the library, add the following info to your Maven project:

XML
<dependency>
			<groupId>io.github.cruisoring</groupId>
			<artifactId>functionExtensions</artifactId>
			<version>1.0.1</version>
</dependency>

The Lazy<T> and Repository<TKey, TValue> are quite simple, using TupleRepository classes to keep simple sets of data is also quite straightforward as can be seen from many tests of the library.

Using TupleRepository classes to get multiple outputs from multiple inputs is much more challenging and interesting, and would be discussed in this post.

To begin with, defining and constructing of TupleRepository need to be planned with the concerned business logic to get evaluate a set of inputs to get another set of outputs:

  1. First of all, if there is an immutable relationship between the given inputs and their associated outputs? If not, like the evaluation of 7 arguments as input would get different result at different time, then the TupleRepository based on paradigm may not be the right choice.
  2. How many output values should be persisted? With 1 - 7 elements can be persisted together as a Tuple1, Tuple2... Tuple7 instance of the value of a given keyset, it is nature to choose strong-typed versions of TupleRepository1, TupleRepository2 ...TupleRepository7 to use Tuple1<T>, Tuple2<T,U>...Tuple7<T,U,V,W,X,Y,Z> to keep the 1-7 output values. For example, for a set of inputs, 2 values associated shall be persisted, then TupleRepository2 is the apparent choice.
  3. How many input arguments are needed?
    • If there is only one input argument and there is no other conditions to be considered, then the TupleRepository1, TupleRepository2 ...TupleRepository7 classes shall be chosen to save the cost of calling default Tuple1<K1> getKey(K1 k1) of TupleKeys1<K1>.
    • Otherwise, the internal classes of TupleRepositoryX determined in step 2) shall be chosen accordingly. Taken the case of getting 2 values associated with 3 input arguments for example, the TupleRepository2.TupleKeys3<K1,K2,K3, T,U> would support evaluating 3 inputs (of generic type K1,K2,K3) to 2 outputs (of generic type T, U).
  4. Is the method receiving multiple inputs and returning multiple outputs as a single strong typed Tuple defined?
    • Either a Lambda Expression, or a method following corresponding Throwable Functional Interface could be used, a method is usually preferred for complex logic to make it easy to debug and understand.
    • This method would still follow the rules of defining JAVA methods with a single return type, but that shall be a strong-typed Tuple identifying all types of its elements.
    • Instead of accepting a single strong-typed Tuple containing all input elements, the parameter list of input arguments makes the method easy to follow. For example,
    • This method doesn't need to handle exceptions thrown by the operations, the TupleRepository would simply return null if Exception is caught, that indicates a abnormal case when a strong-typed Tuple is expected.
    • For example, the makeBaseTypeConverters method of TypeHelper looks like below to extract 6 properties/operators of a specific Class:
    Java
    private static Tuple6<Boolean, Object, Class, Function<Object,Object>,
     Function<Object,Object>, Function<Object,Object>> makeBaseTypeConverters(Class clazz){
        Boolean isArray = clazz.isArray();
        if(!isArray && !clazz.isPrimitive())
            return Tuple.create(false, null, OBJECT_CLASS, 
                                returnsSelf, returnsSelf, returnsSelf);
    
        Class componentClass = clazz.getComponentType();
        Boolean isPrimitive = isPrimitive(componentClass);
    
        Object defaultValue = EMPTY_ARRAY_AS_DEFAULT ?
               ArrayHelper.getNewArray(componentClass, 0) : null;
        Class equivalentComponentClass = getEquivalentClass(componentClass);
    
        Class equivalentClass = classOperators.getThirdValue(equivalentComponentClass);
        Function<Object, Object> componentConverter =
                         getToEquivalentSerialConverter(componentClass);
        TriConsumerThrowable<Object, Integer, Object> equivalentSetter =
                         getArrayElementSetter(equivalentComponentClass);
    
        TriFunctionThrowable.TriFunction<Object, Object, Integer, Boolean>
                getExceptionWhileMapping =
                getExceptionWithMapping(equivalentSetter, componentConverter);
    
        Function<Object, Object> parallelConverter = ...;
    
        Function<Object, Object> serialConverter = ...;
    
        Function<Object, Object> defaultConverterr = ...;
    
        return Tuple.create(isPrimitive, defaultValue, equivalentClass,
                            parallelConverter, serialConverter, defaultConverterr);
    }
  5. Are there any KeyValuePairs that need to be provided directly?
    • Some time, these KeyValuePairs shall be provided as seeds for the above business logic to evaluate other keys to get the right values;
    • In some other case, the default values shall be overridden, then the provided pairs would prevent further evaluations.
    • These KeyValuePairs shall be populated to a Map instance matching the signature of the TupleRepository exactly.
    • For example, following Map instance is supplied directly to the constructor of the TupleRepository:
    Java
    new HashMap(){{
        //For primitive values, return itself as object would convert it
        //to the wrapper type automatically
        Function<Object,Object> convertWithCasting = returnsSelf;
        put(boolean.class, Tuple.create(
                true
                , false
                , Boolean.class
                , convertWithCasting
                , convertWithCasting
                , convertWithCasting));
        convertWithCasting = fromElement -> ((Boolean)fromElement).booleanValue();
        put(Boolean.class, Tuple.create(
                false
                , false
                , boolean.class
                , convertWithCasting
                , convertWithCasting
                , convertWithCasting));
        convertWithCasting = returnsSelf;
        put(byte.class, Tuple.create(
                true
                , (byte)0
                , Byte.class
                , convertWithCasting
                , convertWithCasting
                , convertWithCasting));
        convertWithCasting = fromElement -> Byte.class.cast(fromElement).byteValue();
    .......
    }
  6. Then bind the pre-populated Map with the business logic together to get the fully functional Repositories.
    • For the examples listed in steps 5) and 6), following pseudo codes would create a TupleRepository holding 6 properties related with a specific class.
    Java
    private static final TupleRepository6<
                            Class,      // original Class of the concerned object
    
                            Boolean     // isPrmitiveType, when the original class is
                                        // primitive type, or it is array of primitive type
                            , Object    // default value of the concerned class
                            , Class     // equivalent class: could be the wrapper
                                        // if original class is primitive,
                                        // or primitive type if original class is wrapper
                            , Function<Object, Object>  // convert the value of original class
                                                        // to equivalent class parallelly
                            , Function<Object, Object>  // convert the value of original class
                                                        // to equivalent class in serial
                            , Function<Object, Object>  // convert the value of original class
                                                        // to equivalent class either 
                                                        // parallelly or serially
                    > baseTypeConverters = TupleRepository6.fromKey(
            new HashMap(){{
               ...
            }},
            null,
            TypeHelper::makeBaseTypeConverters
    );
  7. Finally, to expose the properties/operators of any class, many accessor methods shall be created.
    • These methods make users of the TupleRepository consume the related properties/operators easier.
    • Make retrieval of any element of the stored value Tuples transparent to end users.
    • For the above TupleRepository example, the following methods are defined:
      • Boolean isPrimitive(Class clazz) by retrieving the first element of the value Tuple associated with a class: returns true for int.class, char[].class, but false for String.class, Double[].class.

      • Object getDefaultValue(Class clazz) by retrieving the second element of the value Tuple associated with a class: 0 for int.class, false for boolean.class, null (or new char[0]) for char[].class and null (or new Byte[0][]) for Byte[][].class based if the flag TypeHelper.EMPTY_ARRAY_AS_DEFAULT is set to true.

      • Class getEquivalentClass(Class clazz) by retrieving the third element of the value Tuple associated with a class: int.class for Integer.class, Character[].class for char[].class.
      • Then the other 3 accessors Function<Object, Object> getToEquivalentParallelConverter(Class clazz), Function<Object, Object> getToEquivalentSerialConverter(Class clazz) and Function<Object, Object> getToEquivalentConverter(Class clazz) are wrapped by other method, for example Object toEquivalent(Object obj) of TypeHelper as really practical utility: int[] instance would be converted to Integer[] instance, Character[][] instance to char[][] instance.
      • These methods can be wrapped further to get more friendly signatures. For example, the above Object toEquivalent(Object obj) of TypeHelper is used to back following methods of ArrayHelper:
        • Boolean[] toObject(boolean[] values)
        • Byte[] toObject(byte[] values)
        • Character[] toObject(char[] values)
        • ...
        • boolean[] toPrimitive(Boolean[] values)
        • byte[] toPrimitive(Byte[] values)

As a result, TupleRepository classes can be used as a platform to back higher-order functions to create multiple operators with following benefits:

  • For any set of input arguments, evaluation happens only once to get a batch of operators.
  • Remove the if{}else{} loops that is used widely but still insufficient to cover complicated scenarios.
  • The operators related with a set of inputs could be optimised during the evaluation: some attributes/methods related with these inputs would be retrieved once and merged with the generated operators directly.
  • Strong typed accessors to any elements of the evaluated values
  • Embedded Exception handling
  • Easy to extend or update

Point of Interest

There are quite some powerful utilities presented in TypeHelper and other helper classes that can help.

History

  • 10th May, 2018: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)