How to design a nested polymorphism in OOP?

2019-07-15 04:56发布

问题:

Let's assume we have a simple payment feature on an online shop. We want to manage different transactions with different processors of transactions:

  • A transaction can be a payment or a refund.
  • A processor of transactions can be Paypal or Payplug.

So we have the following classes:

class PaymentTransaction implements Transaction {

}

class RefundTransaction implements Transaction {

}

class PaypalProcessor implements Processor {

}

class PayplugProcessor implements Processor {

}

What should be a good understanding of OOP?

  1. Processor.process(transaction); or
  2. Transaction.process(processor);

For example, if we take the example 1, how to avoid the following switch statement?

class PaypalProcessor {

    function process(Transaction transaction) {
        switch(transaction.getType()) {
            case payment:
            //..
                break;
            case refund:
            //..
  }
}

In all cases, how to manage a "nested" polymorphism, a strategy or something else to be able to manage the different transactions with the different processors?

PS: if the title is not appropriate, tell me I will edit it.

回答1:

You seem to be on the right track. What you need is a third class for performing the operations (sample code in Java for the sake of discussion) :

class PaymentProcessor { 
     private Processor processor;
     private Transaction transaction;

     public PaymentProcessor(Processor processor, Transaction transaction) {
          this.processor = processor;
          this.transaction = transaction;
     }

     public void processPayment() {
         processor.process(transaction);
     }
 }

You can then inject the appropriate implementation of Processor and Transaction into the PaymentProcessor :

PaymentProcessor paymentProcessor = new PaymentProcessor(new PayPalDepositProcess(),new PaypalDepositTransaction());
PaymentProcessor refundProcessor = new PaymentProcessor(new PayPalRefundProcess(),new PayPalRefundTransaction());

Notice how you're PaymentProcessor can be passed different combinations of a Processor and Transaction without the need to have a switch statement.

Note that processor.process(transaction); sounds more intuitive than transaction.process(processor). Also note that you can consider using the Abstract Factory pattern here since you seem to be creating a family of related objects (different types of processors that process different types of transactions)



回答2:

I would make transaction object responsible for both processing payment and refunds.

interface Transaction
{

    public function pay();

    public function refund();
}

Then you'd have concrete ones like this:

class PaypalTransaction implements Transaction
{

    public function pay()
    {
        //process payment in a paypale way
    }

    public function refund()
    {
        //process refund in a paypale way
    }

}

class PayplugTransaction implements Transaction
{

    public function pay()
    {
        //process payment the way Payplug do
    }

    public function refund()
    {
        //process refund the way Payplug do
    }

}

Client code that would process transaction payment might look like this:

$customer = new Customer();

//concrete factory producing an abstract product (transaction)
$transaction = $customer->prepareTransaction('100 USD'); 

//process payment
$transaction->pay();

//in a similar fashion you would go about processing a refund
$transaction->refund();

Notice how the client code is kept in the dark about the concrete way money is transferred (implementation details, paypal or payplug). In other words the client is processing transactions polymorphically because it is coded to an abstract rather than a concrete object.

Customer in this example is the concrete factory for the client code:

class Customer implements Shopper
{

    /**
     * @param string $amount
     * @return Transaction
     */
    public function prepareTransaction($amount)
    {
       //use either paypal or payplug depending on the customer's preference 
    }

}

if the client was coded in a way it expects to work with an abstract object (e.g. "Shopper") then we'd have an appearance of an Abstract Factory pattern.

interface Shopper
{

    /**
     * @param string $amount
     * @return Transaction
     */
    public function prepareTransaction($amount);
}