1

Say I have a customer class

class Customer(name, age)

somewhere in my code, I have a list of customers

List[Customers]

at some point, I need to transform this list of customers into a Map with Key = age, and value = List[Customer] like below

Map

15 -> List[Customer]

21 -> List[Customer]

33 -> List[Customer]

Though this is a bit simplified. In my actual code base I am performing some more complex aggregation. In that regard, I have 1 "transformer" function that is 50ish lines long that does some aggregating on the Customer class and adds it into a map like shown above. This function then returns that map. Unfortunatly I see this function as being somewhat complex and unreadable so I'd like to find a good way to design a better solution.

My question is, is there some form of pattern/principle/best practice I should follow when performing these types of operations? I seem to find myself doing these a lot even though each "transformer" function does some output. The thing is this "transformer" function has an awkward place to live. Does it make sense for me to create a class called "CustomerToAgeMapTransformer" and break up my functions into smaller functions inside CustomerToAgeMapTransformer? This class also seems poorly named and I feel I am heading down a bad road here. Any direction would be really greatly appreciated!

  • You might also want to post your actual code to codereview.stackexchange.com to get help with the "unreadable and complex code" part. – ZeroOne Mar 7 at 6:22
2

One of the biggest mistakes I see in projects is reluctance to make domain object specific collections. What I mean by mistake is, an object specific collection can be a great place to centralize code and business logic that is done to a specific entity.

A domain object specific collection is a class which represents a collection of a specific domain object, as opposed a generic collection which can contain different types of objects. Because it is a collection built for a specific type of entity, you can include useful methods that would apply only to that object type.

In your case, I would create a class called CustomerByAge. This class can inherit from a generic collection in your language. It can also have a static Create method that handles the complex transformation logic. You now have a central place for this logic to live and be tested. For extra credit, you can place that transformation logic into CustomerByAgeFactory class if you want to make things extra testable at the price of some extra typing.

Also, additional logic and queries can now live in methods of this collection, for example:

customersByAge.GetAdults()

The above is the greatest advantage to this approach because usually the logic to filter down groups of entities gets sprinkled all over a project or put in a ill-defined and hard to maintain service class.

  • 1
    Great comment and insight I like this direction. Do you mind just elaborating on what a "domain object specific collections" is? – alex Mar 7 at 15:30
  • 1
    @alex updated the answer – TheCatWhisperer Mar 7 at 18:49
  • 1
    I really like this! Just discovering now through your comment about Domain Driven Development. It definitely is an intuitive approach since our brains sort of work in a "domain driven" mindset. Mind me asking...why would using a CustomerAgeFactory make this more testable? – alex Mar 7 at 19:30
  • @alex it makes the code that consumes it more testable, since it's impossible to mock a static method. – TheCatWhisperer Mar 7 at 23:48
  • but in my unit test, wouldn't I just create CustomerByAge using a list of dummy data i.e. List[Customer] and check the output? I'm sorry if I'm misunderstanding but I don't see how it makes the class CustomerByAge untestable. – alex Mar 8 at 0:42
2

First, I would check your language's features. The standard libraries for many languages have a means of doing this. In C# you would use LINQ, in Java you would use object streams, Ruby has some pretty robust functions that work on enumerables that can do the same thing.

The languages that support lambdas or function pointers will allow you to set up a group by function that you pass in, and a selector function. The group by function determines the keys, and the selector defines the values.

The logic would then operate something like this (note this is just pseudo-code):

Map toMap(Enumerable list, function group_by, function select) {
    Map dictionary = new Map();

    foreach(var element in list) {
        var key = group_by(element);
        var value = select(element);
        List list = dictionary.get(key);

        if (list is null) {
            list = new List();
            dictionary.put(key, list);
        }

        list.add(value);
    }

    return dictionary;
}

That would allow you to reuse the same basic function like this:

Map lookup = toMap(customers, (e) => e.Age, (e) => e);

For languages without the ability to pass function pointers or use lambdas, the overall solution only changes a little bit. You would have an interface to define your group by and selector functions. Before Java got its object streams that's how old libraries would have to be designed. Generally it would work something like this:

public interface Grouper<K,E> {
    K groupBy(E element);
}

public interface Selector<V,E> {
    V select(E element);
}

The logic listed above is essentially untouched, other than where the key and value are selected:

var key = grouper.groupBy(element);
var value = selector.select(element);

You could have separate objects to implement your groupers and selectors, which would be used like this:

var map = toMap(custoemrs, new GroupByName(), new SelectCustomer());

But before you go and implement this, make sure you don't already have the functions in your standard library. It's a common problem, so many languages have a solution for it already.

  • Very cool I am using Scala so will check out Object Streams from Java. I am doing some time-series data analysis however and need to "pick" sections from the data set that match a pattern. I.e. Event starts at time A and ends at time B based on some logical conditions. Would this be a suitable problem to be solved with this pattern your provided above? – alex Mar 7 at 15:32
  • Your group by function has to return a single object that can be used for the key in the lookup. If you have a range object, that can work for you. – Berin Loritsch Mar 7 at 17:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.