0

I'm trying to up my OOP game and utilize good patterns where I can. Recently I wrote some classes that essentially take a custom object (a quote) and build a very long/complex api request body using a DTO (wrapper class)

the DTO is quite large (about 500 lines total, it's handler is only about half the length), but it could be argued that it is still single responsibility. I'm using constructors with additional methods inside the classes, something that I picked up from reading about Fluent interface I believe. For example, a snippet from that DTO ZuoraOrdersDTO

public class ChargeOverride {
    public String productRatePlanChargeId;
    public ChargePricing pricing;
    public SubscriptionRatePlanChargeCustomFields customFields;
    public EndDate endDate;

    public ChargeOverride withoutEndDate(String productRatePlanChargeId, ChargePricing pricing, SubscriptionRatePlanChargeCustomFields customFields) {
        this.productRatePlanChargeId = productRatePlanChargeId;
        this.pricing = pricing;
        this.customFields = customFields;
        return this;
    }

    public ChargeOverride withEndDate(String productRatePlanChargeId, ChargePricing pricing, SubscriptionRatePlanChargeCustomFields customFields, EndDate endDate) {
        this.productRatePlanChargeId = productRatePlanChargeId;
        this.pricing = pricing;
        this.customFields = customFields;
        this.endDate = endDate;
        return this;
    }
}

then, in my handler class, I essentially build the object from the inside out - and for the example above I would instantiate that object like this (some lines omitted for brevity):

if(prpc.zqu__Type__c == 'Recurring') { 
    switch on prpc.zqu__Model__c {
        when 'Flat Fee Pricing' {
            ZuoraOrdersDTO.RecurringFlatFee recurringFlatFee = new ZuoraOrdersDTO.RecurringFlatFee(qp.Effective_List_Price__c);
            pricing = new ZuoraOrdersDTO.ChargePricing().recurringFlatFee(recurringFlatFee);
        }
        when 'Discount-Percentage' {
            ZuoraOrdersDTO.Discount discount = new ZuoraOrdersDTO.Discount().discountPercentage((Integer)qp.Term_Discount__c);
            pricing = new ZuoraOrdersDTO.ChargePricing().discount(discount);
        }
        when 'Per Unit Pricing' {
            ZuoraOrdersDTO.RecurringPerUnit recurringPerUnit = new ZuoraOrdersDTO.RecurringPerUnit(qp.Effective_List_Price__c, (Integer)qp.Quantity__c);
            pricing = new ZuoraOrdersDTO.ChargePricing().recurringPerUnit(recurringPerUnit);
        }
        when 'Discount-Fixed Amount' {
            ZuoraOrdersDTO.Discount discount = new ZuoraOrdersDTO.Discount().discountAmount(qp.Additional_Discount__c);
            endDate = new ZuoraOrdersDTO.EndDate('Fixed_Period', (Integer)qp.Promo_Months__c);
            pricing = new ZuoraOrdersDTO.ChargePricing().discount(discount);
        }
    }

if(endDate != null) {
    chargeOverride = new ZuoraOrdersDTO.ChargeOverride().withEndDate(prpc.zqu__ZuoraId__c, pricing, customFields, endDate);
} else {
    chargeOverride = new ZuoraOrdersDTO.ChargeOverride().withoutEndDate(prpc.zqu__ZuoraId__c, pricing, customFields);
}

I have read that this is preferable to overloading the constructor itself.

as far as how to make this "better", I could definitely implement an interface so that the handler class is not directly coupled to the DTO, and then I could also split out the handler class into separate classes for different implementations - IE: the final structure of the DTO depends on what sort of "action" the quote is taking (whether it's for a new account, or an existing account, or whether a product is being added or removed) - but the current implementation of the handler class with the switch on the "action" isn't terrible either I guess, and also, a lot of the structure of the DTO is the same across all the "actions".

The main thing I can see is just the interface layer to de-couple the dependency but it almost seems unnecessary for this use case.

So my question is really, for someone more experienced with this - would you further abstract this? or do none of the major patterns really make any huge improvements?

1 Answer 1

0

One thought

public class ChargeOverride {
  public String productRatePlanChargeId;
  public ChargePricing pricing;
  public SubscriptionRatePlanChargeCustomFields customFields;
  public EndDate endDate;

  public ChargeOverride withProductRatePlan(String val) {this.productRatePlanChargeId = val; return this;}
  public ChargeOverride withChargePricing(ChargePricing val) {this.chargePricing = val; return this;}
  public ChargeOverride withSubscriptionRatePlan(String val) {this.subscriptionRatePlan = val; return this;}
  public ChargeOverride withEndDate(EndDate val) {this.endDate = val; return this;}
}

Why? to avoid three and four argument (and the temptation to add more arg) constructors. Calling code is self-documenting.

Each of your subclasses can also use this approach if they have multiple properties to set. Be sure each class defaults any value

Then you can call with

new ChargeOverride()
  .withProductRatePlan(someVal)
  .withChargePricing(new ChargePricing()
     .withProperty1(someVal)
     .withProperty2(someVal)
   )
  .withSubscriptionRatePlan(new SubscriptionRatePlan()
     .withXXX
     ...
   )
  .withEndDate(new EndDate()
     .withYYY
     ...
   )
  ;

if you implement equals() and hash() on each of these classes, you can also easily test expected values versus actual values using a simple:

System.assertEquals(expectedChargeOverride,
                 actualChargeOverride,msg);

this can be handy when mocking service layers and using the Test.StubApi framework for unit testing (see ApexMocks or Amoss for higher level examples).

1
  • I like it, looks kind of similar to the builder pattern but at the time i felt that was overkill, but I guess i dont necessary need a builder class/interface to use the concepts. makes a lot of sense how you've explained it. when I stumbled across that "withEndDate" i was like ah if the rest of the construction of the object was that self documenting that would be ideal haha Commented Nov 8, 2022 at 22:57

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .