13

Let's say I am working with bicycles and I need to group them by different criterias.

class Bicycle {
    String brand { get; set; }
    Date purchaseDate { get; set; }
}

class GroupedBicycles {
    String groupingCriteria { get; set; }

    /*
        will contain all bikes with this criteria
        for example, if grouped by brand, all bikes in this list will have current brand
    */
    List<Bicycle> bicycles { get; set; }
}

class BicyclesByBrand extends GroupedBicycles { 

}

class BicyclesByPurchaseDate extends GroupedBicycles {
    Date groupingCriteria { get; set; } // will contain purchase date   
}

List<BicyclesByBrand> groupBicyclesByBrand(List<Bicycle> bicycles) {
    List<BicyclesByBrand> result = new List<BicyclesByBrand>();

    // Here is my own logic for grouping bicycles by criteria
    for (Bicycle bike : bicycles) {
        Boolean thisBikeIsGrouped = false;

        for (BicyclesByBrand bikesByBrand : result) {
            if (bikesByBrand.groupingCriteria == bike.brand) { // **MARK1 will explain below**
                bikesByBrand.bicycles.add(bike);
                thisBikeIsGrouped = true;
                break;
            }
        }

        if (!thisBikeIsGrouped) {
            BicyclesByBrand bikesByBrand = new BicyclesByBrand();
            bikesByBrand.groupingCriteria = bike.brand;
            bikesByBrand.bikes = new List<Bicycle> { bike };
        }
    }

    return result;
}

the issue for me is that for grouping bikes by purchase date, I need to copy the same method and change only line marked as MARK1 Field needs to be changed from brand to purchaseDate.

Is there a way in apex to create this method only once, but pass field for grouping as parameter? I believe this is possible in java by using reflection.

I expect this method to have following signature:

List<GroupedBicycles> groupBicyclesList<Bicycle> bicycles, String fieldCriteria)

I have already implemented displaying this data in visualforce, I have one apex:repeat that uses base class as var attribute, and in controller different implementations are assigned depending on grouping selected, so I am aiming to do minimal changes of data structure.

  • You'd have different groupingCriteria for BicyclesByPurchaseDate. Perhaps that boolean If conditions needs to be abstracted into another method. As in a Criteria Method or subclass. Even as an interface perhaps? – crmprogdev May 13 '16 at 15:10
  • Just a quick word. Why use classes when Map<String,List<Bicycle>> or Map<Date,List<Bicycle>> would do the exact same thing? – Sebastian Kessel May 13 '16 at 15:15
  • @SebastianKessel I store other data in that class, it has comparable method in real code. I simplified it for this example – vladdihoney May 13 '16 at 15:16
  • Point well taken. :) – Sebastian Kessel May 13 '16 at 15:17
  • @crmprogdev if I understood you, yes, criterias can be of different types. I can also pass type as parameter for final method – vladdihoney May 13 '16 at 15:20
14

Not entirely sure I follow what you are aiming to do but this approach comes to mind...

I suggest you move the selection to a separate class that implements a Filter interface:

public class Bikes {

    public interface Filter {
        Boolean accept(Bicycle b);
    }

    public static Bicycle[] filter(Bicycle[] bicycles, Filter filter) {
        Bicycle[] results = new Bicycle[] {};
        for (Bicycle b : bicycles) {
            if (filter.accept(b)) results.add(b);
        }
        return results;
    }
}

You can the write filters as required e.g.:

public class BrandFilter implements Bikes.Filter {
    private String brand;
    public BrandFilter(String brand) {
        this.brand = brand;
    }
    public Boolean accept(Bicycle b) {
        return b.brand == brand;
    }
    public override String toString() {
        return 'Brand ' + brand;
    }
}

or:

public class PurchaseDateFilter implements Bikes.Filter {
    private Date purchaseDate;
    public PurchaseDateFilter(Date purchaseDate) {
        this.purchaseDate = purchaseDate;
    }
    public Boolean accept(Bicycle b) {
        return b.purchaseDate == purchaseDate;
    }
    public override String toString() {
        return 'Purchase Date ' + purchaseDate.format();
    }
}

or if you are using sub-classing you can use instanceof and casts as needed. Or you can write one filter class that accepts many parameters; the key point is to separate out the filtering from other logic.

So to find all bikes of a brand:

Bicycle[] input = ...
Bicycle[] output = Bikes.filter(input, new BrandFilter('Cinelli'));

You can also create and and or filters so combinations can be applied.

PS

If you want to keep your GroupedBicycles class:

public class GroupedBicycles {
    public String groupingCriteria { get; set; }
    public List<Bicycle> bicycles { get; set; }
    public GroupedBicycles(Bicycle[] allBicycles, Bicycles.Filter filter) {
        groupingCriteria = filter.toString();
        bicycles = Bikes.filter(allBicycles, filter);
    }
}

public GroupedBicycles[] grouped {get; set;}

private void init() {
    Bicycle[] allBicycles = ...
    grouped = new GroupedBicycles[] {
            new GroupedBicycles(allBicycles, new BrandFilter('Cinelli')),
            new GroupedBicycles(allBicycles, new BrandFilter('Condor')),
            new GroupedBicycles(allBicycles, new PorchaseDateFilter(Date.today())),
            ...
            };
}
  • I actually thought about this approach, too, but took it out of my own answer. This code will probably run faster for larger objects at the expense of more code required, while my version is more compact at the expense of more CPU time for larger objects (but more linear execution time for larger lists). OP can choose which they want: speed or size. Although I wonder if you couldn't make some sort of interface that uses a Map instead...? – sfdcfox May 13 '16 at 15:33
  • @sfdcfox I'm not too clear what the OP is ultimately aiming to achieve... But this pattern of separating the selection from the iteration usually helps; and also an example of composition portentially working better than inheritance. – Keith C May 13 '16 at 15:42
  • @KeithC please, look at changes at my question. One more detail about it - displaying data in visualforce. It now is made via inheritance to minify markup code – vladdihoney May 13 '16 at 15:49
  • @VladyslavKushney See the PS. – Keith C May 13 '16 at 16:15
  • @VladyslavKushney Thank-you and I do understand making the least change may be the right choice. Just bear in mind that most code goes through change over time and often by people other than the original author. So if you write something that is a bit tricky for you to follow (or change) today, imagine how hard it will be for someone else tomorrow. So simplicity, clarity and flexibility are characteristics to always try and build in. – Keith C May 13 '16 at 16:31
9

You can use JSON to mimic reflection for simple objects:

class GroupedBicycles {
    Object groupValue;
    Bicycle[] bikes = new Bicycle[0];
    GroupedBicycles(Object value) {
        groupValue = value;
    }
}

GroupedBicycles[] groupBicycles(Bicycle[] bikes, String field) {
    Map<Object, GroupedBicycles> results = new Map<Object, GroupedBicycles>();
    for(Bicycle bike: bikes) {
        String bikeJson = JSON.serialize(bike);
        Object fieldValue = ((Map<String, Object>) JSON.deserializeUntyped(bikeJson)).get(field);
        if(!results.containsKey(fieldValue)) {
            results.put(value, new GroupedBicycles(fieldValue));
        }
        results.get(fieldValue).bikes.add(bike);
    }
    return results.values();
}

Since we don't have "real" reflection, this is about the best you can do.

Note that if you use Object instead of specific data types, you can get away with not having to have specific subclasses of GroupedBicycles. You'd just have to find a way to process the groupValue based on type later, perhaps by implementing a custom sort algorithm, etc, if you needed to.

  • @VladyslavKushney Thanks for the edit. I'd knew I'd forgotten something when I was reformatting the code. – sfdcfox May 13 '16 at 16:18
  • It does not work for relation with nested objects like Bike__c.Someobj__r.Somefield__c because in JSON they are objects and I cannot get nested values by using only one map.get() method – vladdihoney May 13 '16 at 16:54
  • Your idea of using maps instead of custom algorithm is good – vladdihoney May 13 '16 at 17:12
  • You're right. If you're using nested objects, you will need to enhance parsing. For example, you could split the string and recursively evaluate the values. Your original question didn't include nested parameters, so this answer didn't suggest a way to deal with those. – sfdcfox May 13 '16 at 17:12
0

For what its worth, I think having every possible grouping be an separate class is overkill. Your classes could grow to match the number of descriptive fields on your object, and you'll need to write new classes whenever your object is extended. Instead, I would recommend a data-driven approach, that references the fields of the bicycle by name, like bikeInstance.get("color").
The the "groupBicyclesByBrand" method would become more generic, like, "groupBicyclesByField". Pass in a fieldname, and build the output list. I would buidl a map, where keys are vocabulary of values of the field coming in, and values for each key are list.

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.