lundi 20 mai 2019

When to use member variables vs design by composition?

I am trying to wrap my head around how to best design the system I am working on.

Let’s say it’s an application similar to a pawn store. I have abstructed the flow of purchasing and reselling into something called an ExecutionStrategy. There are four ExecutionStrategy implementations in this application: registering a customer, bidding and purchasing, pricing, and posting to store.

There a basic steps that each one of the strategies follow including the main execution workflow and recording what we’ve done in a data store.

in addition to these bidding and purchasing as well as pricing require a consultation of an expert before we can do anything in the execution workflow.

this is where I am a little bit confused on the decision that I would like to make in terms of design. it appears as if there are three options to go with and I’m not too sure which one is most correct. 1) Extending Execution strategy with something like ExecutionStrategy with ExecutionStrategyWithConsultation, which wraps the execution workflow of the strategy with a consultation phase. 2) Creating a Decorator pattern for ExecutionStrategy and extends that with something like ConsultationServiceDecorator. 3) create a member variable in the implementation of purchasing/bidding and pricing to call the consultation service at any time with an interface around the service.

I will outline the design below.

Some considerations:

  • ConsultationService is very, very slow. Caching is not really an option here as the data is very loosely formed and we do not want to create a document datastore just for this.
  • ConsultationService returns an object that matches what was given. So it ends up having 1 method that looks like T consultOn(T item)
  • There could be some desire to call ConsultationService at any time in the execution workflow. Currently the only use case is to call the service before the main flow, but this is not necessarily the only use case right now.

Pros/cons of each approach above:

  1. Extending ExecutionStrategy directly:

    • PRO: We can have access to a protected ConsultationService variable in the code
    • PRO: We have an understanding from reading the code that a, say, PurchasingExecutionStrategy extends ExecutionStrategyWithConsultation, so we know a bit about what kind of workflow it is just from that.
    • CON: This seems to break the "composition over inheritance" pattern. We are using inheritance for storing member variables.
    • CON: The service returns a whole new object, so after the first line of code when we make the call to the service, we are dealing with an entirely different object than the one passed originally.
  2. Creating a Decorator:

    • PRO: We are more closely conforming with the composition over inheritance principal.
    • PRO: We can enforce that the service is called first, and pass that new object to the main workflow, so it only executes its main workflow on the object passed.
    • CON: I have not figured out a way to design this in a way to allow for potentially multiple or any time service calls.
    • CON: When looking at the code, we lose the knowledge gained from PurchasingExecutionStrategy extends ExecutionStrategyWithConsultation, unless we look at where PurchasingExecutionStrategy is actually being instantiated as a constructor arg of ConsultationServiceDecorator
  3. Create a member variable with interface:

    • PRO: Same pros as #1. Easy to understand fully what code is doing without digging.
    • CON: Same cons as #1. Cannot enforce order. Execution deals with inheritenly different object than the one passed.
    • CON: If we need to make multiple calls in the same workflow, this would be very slow due to service speed and no cache.

Examples of each:

//Number 1
public interface ExecutionStrategy<T> {

    /**
    * Perform the main execution workflow
    */
    public T execute(T item);
}

public interface ConsultationService {

    public StoreItem consultOn (StoreItem item);
}

public abstract class ExecutionStrategyWithConsultation extends ExecutionStrategy<StoreItem> {

    protected ConsultationService consultationService;

}

public class ListingExecutionStrategy extends ExecutionStrategyWithConsultation {

    public StoreItem execute(StoreItem item) {
      if (item.hasDirectBuyer()) { //hasDirectBuyer is populated by ConsultationService
        item.sellTo = buyer.getId();
        return item;
      } else {
        //no direct buyer
        SuggestedPriceRange priceRange = item.getConsultationPriceRange(); //consultationPriceRange is populated by ConsultationService
        item.priceRange = priceRange;
        item.listToWebsite = true;
        return item;
      }
    }
}


//Number 2
public interface ExecutionStrategy<T> {

    /**
    * Perform the main execution workflow
    */
    public T execute(T item);
}

public abstract class ExecutionStrategyDecorator<T> extends ExecutionStrategy<T>{

    protected final ExecutionStrategy<T> executionStrategy;
    public ExecutionStrategyDecorator(ExecutionStrategy<T> execStrategy) {
      executionStrategy = execStrategy;
    };
}

public class ExecutionStrategyWithConsultation extends ExecutionStrategyDecorator<StoreItem> {

    protected ConsultationService consultationService;

    public ExecutionStrategyWithConsultation(ExecutionStrategy<StoreItem> execStrat, ConsultationService service) {
      super(execStrat);
      consultationService = service;
    }

    public StoreItem execute(StoreItem item) {
      StoreItem itemAfterConsultation = consultationService.consultOn(item);
      return execStrategy.execute(itemAfterConsultation);
    }

}

public class ListingExecutionStrategy extends ExecutionStrategy<StoreItem> {

    public StoreItem execute(StoreItem item) {
      if (item.hasDirectBuyer()) { //hasDirectBuyer is populated by ConsultationService
        item.sellTo = buyer.getId();
        return item;
      } else {
        //no direct buyer
        SuggestedPriceRange priceRange = item.getConsultationPriceRange(); //consultationPriceRange is populated by ConsultationService
        item.priceRange = priceRange;
        item.listToWebsite = true;
        return item;
      }
    }
}

public class ListingExecutionStrategyFactory {

    public ExecutionStrategy instantiate() {
      return new ExecutionStrategyWithConsultation(new ListingExecutionStrategy(), new ConsultationServiceImpl());
    }
}


//Number 3
public interface ExecutionStrategy<T> {

    /**
    * Perform the main execution workflow
    */
    public T execute(T item);
}

public interface ConsultationService {

    public DirectBuyer getDirectBuyerIfExists(StoreItemType itemType);
    public SuggestedPriceRange getSuggestedPriceRange(StoreItem item);
}

public class ListingExecutionStrategy extends ExecutionStrategy<StoreItem> {

    ConsultationService service;

    public PurchasingExecutionStrategy(ConsultationService consultService) {
        service = ConsultationService;
    }

    public StoreItem execute(StoreItem item) {
      DirectBuyer buyer = service.getDirectBuyerIfExists(item.getItemType())
      if (Optional.ofNullable(buyer).isPresent()) {
        item.sellTo = buyer.getId();
        return item;
      } else {
        //no direct buyer
        SuggestedPriceRange priceRange = service.getSuggestedPriceRange(item);
        item.priceRange = priceRange;
        item.listToWebsite = true;
        return item;
      }
    }
}

Thanks for the input. Appreciate the help.

Aucun commentaire:

Enregistrer un commentaire