0

I write the code in Java and I have a case where I need to get the status of an order depending on the product type. Each product type has the same base logic as written below.

protected String getStatus(ProductType productType, Result result) {
  if (result.isSuccess()) {
    return "SUCCESS";
  } else if (result.getPaymentMethod().equals("TRANSFER")) {
    return "WAITING_CONFIRMATION";
  } else {
    return "WAITING_PAYMENT";
  }
}

However, each product type can have custom logic. For example:

  • if product type is "A", no need to modify the logic above
  • if the status returned from the method above is "SUCCESS" and the product type is "B", I need to call another service to check the status of the product issuance
  • if the status returned from the method above is "SUCCESS" and the product type is "C", I need to call partner service.

My question is what is the best design pattern to implement?

The first idea is using something like template method pattern

public class BaseProductStatusService {

  public String getStatus(Result result, Product product) {
    String status;
    if (result.isSuccess()) {
      status = "SUCCESS";
    } else if (result.getPaymentMethod().equals("TRANSFER")) {
      status = "WAITING_CONFIRMATION";
    } else {
      status = "WAITING_PAYMENT";
    }

    return doGetStatus(status);
  }

  protected String doGetStatus(String status, Product product) {
    return status;
  }
}

// For product A, no need to have its own class since it can use the base class

public class ProductBStatusService extends BaseProductStatusService {

  @Override
  protected String doGetStatus(String status, Product product) {
    if (status.equals("SUCCESS")) {
      return this.checkProductIssuance(product);
    }

    return status;
  }
}

public class ProductCStatusService() extends BaseProductStatusService {

  @Override
  protected String doGetStatus(String status, Product product) {
    if (status.equals("SUCCESS")) {
      return this.checkStatusToPartner(product);
    }

    return status;
  }
}

Another alternative is using decorator pattern

public interface ProductStatusService() {
   String getStatus(Result result, Product product);
}

public class DefaultProductStatusService implements ProductStatusService {

  public String getStatus(Result result, Product product) {
    String status;
    if (result.isSuccess()) {
      status = "SUCCESS";
    } else if (result.getPaymentMethod().equals("TRANSFER")) {
      status = "WAITING_CONFIRMATION";
    } else {
      status = "WAITING_PAYMENT";
    }

    return doGetStatus(status);
  }
}

public abstract class ProductStatusServiceDecorator implements ProductStatusService {

  private ProductStatusService productStatusService;

  public ProductStatusServiceDecorator(ProductStatusService productStatusService) {
    this.productStatusService = productStatusService;
  }

  public String getStatus(Result result, Product product) {
    return this.productStatusService.getStatus();
  }
}

// For product A, no need to have its own class since it can use the DefaultProductStatusService class

public class ProductBStatusServiceDecorator extends ProductStatusServiceDecorator {

  public ProductStatusServiceDecorator(ProductStatusService productStatusService) {
    super(productStatusService);
  }

  public String getStatus(Result result, Product product) {
    String status = super.getStatus();

    if (status.equals("SUCCESS")) {
      return this.checkProductIssuance(product);
    }

    return status;
  }
}

public class ProductCStatusServiceDecorator extends ProductStatusServiceDecorator {

  public ProductStatusServiceDecorator(ProductStatusService productStatusService) {
    super(productStatusService);
  }

  public String getStatus(Result result, Product product) {
    String status = super.getStatus();

    if (status.equals("SUCCESS")) {
      return this.checkStatusToPartner(product);
    }

    return status;
  }
}

which one is better for the above case and what is the reason? or do you have other suggestion?

CherryBelle
  • 1,302
  • 7
  • 26
  • 46

2 Answers2

1

I'd go with the simplest solution for your problem and implement it at the lowest level possible. That means if it's just an additional call in one method that might be done I'd not use the decorator as this would require you to already select the correct instance early on. The "template" approach seems to be the simpler of the 2.

Also, if the product types are already known just adding some switch logic into the service itself might be sufficient.

If it needs to be extensible then I'd probably use a strategy approach which is similar to your template method approach, e.g. something like this:

interface SpecificProductStatusStrategy {
  String getSpecificStatus(String baseStatus, Product product);
}

public class ProductStatusService {
  //if the service is not a singleton you might want to put that into a singleton repository and use that
  private Map<String, SpecificProductStatusStrategy> strategies = ...;

  //simple registration, could also use DI etc.
  public void addStrategy(String productType, SpecificProductStatusStrategy strategy) {
    strategies.put(productType, strategy);
  }

  public String getStatus(Result result, Product product) {
    String baseStatus;
    if (result.isSuccess()) {
      baseStatus = "SUCCESS";
    } else if (result.getPaymentMethod().equals("TRANSFER")) {
      baseStatus = "WAITING_CONFIRMATION";
    } else {
      baseStatus = "WAITING_PAYMENT";
    }

    var strategy = strategies.get(product.getType());
    return strategy != null ? strategy.getSpecificStatus(baseStatus, product) : baseStatus;
 }
Thomas
  • 87,414
  • 12
  • 119
  • 157
  • 1
    @Michael you're right, the OP's approach is a template method. I removed that portion and will add some information on the strategy approach which doesn't necessarily require something to be passed as an argument but which could also be looked up. – Thomas Mar 22 '23 at 10:40
  • Looks good now! – Michael Mar 22 '23 at 10:59
  • actually I also plan to use strategy to determine which product specific service to use. but why did you recommend to put the base logic in the method that needs to get the result, instead of making it as a part of a base service that will be extended by each product? – CherryBelle Mar 22 '23 at 15:02
  • @CherryBelle there are different design options and I'm missing a lot of context. I'd separate products (data containers) and the product services (logic) especially if there is external communication. Thus for multiple different products I'd still try to use a common service that contains common logic and uses lightweight strategies for the parts that may be different for each product which comes in handy when dealing with larger batches of different products. – Thomas Mar 23 '23 at 07:31
-1

The idea of the decorator pattern is to be able to change method behavior at runtime. Which is not something you seem to need: product types are clearly defined from the begining (i.e. before compilation).

You can go with the template unless I misunderstood something.

These being said : each design pattern have been created to solve a problem. In your case, a big "if.. else...", while unelegant would have do just fine. This is a good thing you pactise these concepts, but beware not to end up with something tricky and overcomplicated.

Crospone
  • 1
  • 1
  • The first sentence is wrong. The goal of the decorator pattern is to separate responsibilities. Nothing says that those decorators have to be composed at runtime. – Michael Mar 22 '23 at 09:38
  • @Michael, the first line of the Decorator chapter in the GoF book states, "_Intent: Attach additional responsibilities to an object dynamically._" Dynamically == at runtime. So you're both correct. – jaco0646 Mar 22 '23 at 17:48
  • @jaco0646 "dynamically" doesn't necessarily mean at runtime. – Michael Mar 22 '23 at 21:18
  • @Michael, in the GoF book it does. "_Object composition is defined dynamically at run-time..._ (p.19)" This is arguably the central tenet of the book. Composition == dynamic == runtime. Inheritance == static == compile time. The two are compared & contrasted in nearly every pattern, culminating in the famous principle, "_Favor object composition over class inheritance._" The book repeatedly points out how the former is dynamic while the latter is static. Do you know of an instance where the GoF refer to something dynamic at compile time? I'm unaware of any example. – jaco0646 Mar 22 '23 at 23:51
  • 1
    @jaco0646 What GoF mean is that decorator instances are composed into a hierarchy at runtime. Of course they are, because you don't have any instances at compile-time. That doesn't mean I can't define a *fixed* hierarchy in my code, e.g. `Foo foo = new FooA(new FooB(new FooC())`. This answer makes the incorrect assertion that because the structure is "clearly defined from the begining", the decorator pattern is not applicable. That's wrong. The decorator pattern provides value for even fixed hierarchies, by separating responsibilities into distinct decorators. – Michael Mar 23 '23 at 10:18
  • @Michael, thank you. I see what you were getting at now. Inheritance would normally suffice for a fixed hierarchy; but on page 177 the GoF point out, "_Use Decorator when extension by subclassing is impractical. Sometimes a large number of independent extensions are possible and would produce an explosion of subclasses to support every combination. Or a class definition may be hidden or otherwise unavailable for subclassing._" Cheers! – jaco0646 Mar 23 '23 at 14:25
  • @jaco0646 inheritance doesn't allow for flexible composition. You can't, via inheritance, have the equivalent of my previous example plus `Foo f2 = new FooA(new FooC(new FooB()))`. It's one or the other. Decorators also prohibit you from sharing fields, which is usually error-prone. Every codebase I've worked on that heavily relied on inheritance was awful. It's bad in like 95% of cases it's used. – Michael Mar 23 '23 at 15:48