Factory method - delegating the creation of different types of payments

The Factory method pattern (or simply, Factory) is probably the second-best known and used design pattern in the industry. Its purpose is to abstract the user from the knowledge of the struct he needs to achieve for a specific purpose, such as retrieving some value, maybe from a web service or a database. The user only needs an interface that provides him this value. By delegating this decision to a Factory, this Factory can provide an interface that fits the user needs. It also eases the process of downgrading or upgrading of the implementation of the underlying type if needed.

Description

When using the Factory method design pattern, we gain an extra layer of encapsulation so that our program can grow in a controlled environment. With the Factory method, we delegate the creation of families of objects to a different package or object to abstract us from the knowledge of the pool of possible objects we could use. Imagine that you want to organize your holidays using a trip agency. You don't deal with hotels and traveling and you just tell the agency the destination you are interested in so that they provide you with everything you need. The trip agency represents a Factory of trips.

Objectives

After the previous description, the following objectives of the Factory Method design pattern must be clear to you:

  • Delegating the creation of new instances of structures to a different part of the program
  • Working at the interface level instead of with concrete implementations
  • Grouping families of objects to obtain a family object creator

The example - a factory of payment methods for a shop

For our example, we are going to implement a payments method Factory, which is going to provide us with different ways of paying at a shop. In the beginning, we will have two methods of paying--cash and credit card. We'll also have an interface with the method, Pay, which every struct that wants to be used as a payment method must implement.

Acceptance criteria

Using the previous description, the requirements for the acceptance criteria are the following:

  • To have a common method for every payment method called Pay
  • To be able to delegate the creation of payments methods to the Factory
  • To be able to add more payment methods to the library by just adding it to the factory method

First unit test

A Factory method has a very simple structure; we just need to identify how many implementations of our interface we are storing, and then provide a method, GetPaymentMethod, where you can pass a type of payment as an argument:

type PaymentMethod interface { 
    Pay(amount float32) string 
} 

The preceding lines define the interface of the payment method. They define a way of making a payment at the shop. The Factory method will return instances of types that implement this interface:

const ( 
    Cash      = 1 
    DebitCard = 2 
) 

We have to define the identified payment methods of the Factory as constants so that we can call and check the possible payment methods from outside of the package.

func GetPaymentMethod(m int) (PaymentMethod, error) { 
    return nil, errors.New("Not implemented yet") 
} 

The preceding code is the function that will create the objects for us. It returns a pointer, which must have an object that implements the PaymentMethod interface, and an error if asked for a method which is not registered.

type CashPM struct{} 
type DebitCardPM struct{} 
 
func (c *CashPM) Pay(amount float32) string { 
    return "" 
} 
 
func (c *DebitCardPM) Pay(amount float32) string { 
    return "" 
} 

To finish the declaration of the Factory, we create the two payment methods. As you can see, the CashPM and DebitCardPM structs implement the PaymentMethod interface by declaring a method, Pay(amount float32) string. The returned string will contain information about the payment.

With this declaration, we will start by writing the tests for the first acceptance criteria: to have a common method to retrieve objects that implement the PaymentMethod interface:

package creational 

import ( 
    "strings" 
    "testing" 
) 
 
func TestCreatePaymentMethodCash(t *testing.T) { 
    payment, err := GetPaymentMethod(Cash) 
    if err != nil { 
        t.Fatal("A payment method of type 'Cash' must exist") 
    } 
 
    msg := payment.Pay(10.30) 
    if !strings.Contains(msg, "paid using cash") { 
        t.Error("The cash payment method message wasn't correct") 
    } 
    t.Log("LOG:", msg) 
} 

Now we'll have to separate the tests among a few of the test functions. GetPaymentMethod is a common method to retrieve methods of payment. We use the constant Cash, which we have defined in the implementation file (if we were using this constant outside for the scope of the package, we would call it using the name of the package as the prefix, so the syntax would be creational.Cash). We also check that we have not received an error when asking for a payment method. Observe that if we receive the error when asking for a payment method, we call t.Fatal to stop the execution of the tests; if we called just t.Error like in the previous tests, we would have a problem in the next lines when trying to access the Pay method of a nil object, and our tests would crash execution. We continue by using the Pay method of the interface by passing 10.30 as the amount. The returned message will have to contain the text paid using cash. The t.Log(string) method is a special method in testing. This struct allows us to write some logs when we run the tests if we pass the -v flag.

func TestGetPaymentMethodDebitCard(t *testing.T) { 
    payment, err = GetPaymentMethod(Debit9Card) 
 
    if err != nil { 
        t.Error("A payment method of type 'DebitCard' must exist")
    } 
 
    msg = payment.Pay(22.30) 
 
    if !strings.Contains(msg, "paid using debit card") { 
        t.Error("The debit card payment method message wasn't correct") 
    } 
 
    t.Log("LOG:", msg) 
}

We repeat the same operation with the debit card method. We ask for the payment method defined with the constant DebitCard, and the returned message, when paying with debit card, must contain the paid using debit card string.

 
func TestGetPaymentMethodNonExistent(t *testing.T) { 
    payment, err = GetPaymentMethod(20) 
 
    if err == nil { 
        t.Error("A payment method with ID 20 must return an error") 
    } 
    t.Log("LOG:", err) 
}

Finally, we are going to test the situation when we request a payment method that doesn´t exist (represented by the number 20, which doesn't match any recognized constant in the Factory). We will check if an error message (any) is returned when asking for an unknown payment method.

Let's check whether all tests are failing:

$ go test -v -run=GetPaymentMethod .
=== RUN   TestGetPaymentMethodCash
--- FAIL: TestGetPaymentMethodCash (0.00s)
        factory_test.go:11: A payment method of type 'Cash' must exist
=== RUN   TestGetPaymentMethodDebitCard
--- FAIL: TestGetPaymentMethodDebitCard (0.00s)
        factory_test.go:24: A payment method of type 'DebitCard' must exist
=== RUN   TestGetPaymentMethodNonExistent
--- PASS: TestGetPaymentMethodNonExistent (0.00s)
        factory_test.go:38: LOG: Not implemented yet
FAIL
exit status 1
FAIL

As you can see in this example, we can only see tests that return the PaymentMethod interfaces failing. In this case, we'll have to implement just a part of the code, and then test again before continuing.

Implementation

We will start with the GetPaymentMethod method. It must receive an integer that matches with one of the defined constants of the same file to know which implementation it should return.

package creational 
 
import ( 
    "errors" 
    "fmt" 
) 
 
type PaymentMethod interface { 
    Pay(amount float32) string 
} 
 
const ( 
    Cash      = 1 
    DebitCard = 2 
) 

type CashPM struct{} 
type DebitCardPM struct{} 
 
func GetPaymentMethod(m int) (PaymentMethod, error) { 
    switch m { 
        case Cash: 
        return new(CashPM), nil 
        case DebitCard: 
        return new(DebitCardPM), nil 
        default: 
        return nil, errors.New(fmt.Sprintf("Payment method %d not recognized
", m)) 
    } 
} 

We use a plain switch to check the contents of the argument m (method). If it matches any of the known methods--cash or debit card, it returns a new instance of them. Otherwise, it will return a nil and an error indicating that the payment method has not been recognized. Now we can run our tests again to check the second part of the unit tests:

$go test -v -run=GetPaymentMethod .
=== RUN   TestGetPaymentMethodCash
--- FAIL: TestGetPaymentMethodCash (0.00s)
        factory_test.go:16: The cash payment method message wasn't correct
        factory_test.go:18: LOG:
=== RUN   TestGetPaymentMethodDebitCard
--- FAIL: TestGetPaymentMethodDebitCard (0.00s)
        factory_test.go:28: The debit card payment method message wasn't correct
        factory_test.go:30: LOG:
=== RUN   TestGetPaymentMethodNonExistent
--- PASS: TestGetPaymentMethodNonExistent (0.00s)
        factory_test.go:38: LOG: Payment method 20 not recognized
FAIL
exit status 1
FAIL

Now we do not get the errors saying it couldn't find the type of payment methods. Instead, we receive a message not correct error when it tries to use any of the methods that it covers. We also got rid of the Not implemented message that was being returned when we asked for an unknown payment method. Let's implement the structs now:

type CashPM struct{} 
type DebitCardPM struct{} 
 
func (c *CashPM) Pay(amount float32) string { 
     return fmt.Sprintf("%0.2f paid using cash
", amount) 
} 
 
func (c *DebitCardPM) Pay(amount float32) string { 
     return fmt.Sprintf("%#0.2f paid using debit card
", amount) 
} 

We just get the amount, printing it in a nicely formatted message. With this implementation, the tests will all be passing now:

$ go test -v -run=GetPaymentMethod .
=== RUN   TestGetPaymentMethodCash
--- PASS: TestGetPaymentMethodCash (0.00s)
        factory_test.go:18: LOG: 10.30 paid using cash
=== RUN   TestGetPaymentMethodDebitCard
--- PASS: TestGetPaymentMethodDebitCard (0.00s)
        factory_test.go:30: LOG: 22.30 paid using debit card
=== RUN   TestGetPaymentMethodNonExistent
--- PASS: TestGetPaymentMethodNonExistent (0.00s)
        factory_test.go:38: LOG: Payment method 20 not recognized
PASS
ok

Do you see the LOG: messages? They aren't errors, we just print some information that we receive when using the package under test. These messages can be omitted unless you pass the -v flag to the test command:

$ go test -run=GetPaymentMethod .
ok

Upgrading the Debitcard method to a new platform

Now imagine that your DebitCard payment method has changed for some reason, and you need a new struct for it. To achieve this scenario, you will only need to create the new struct and replace the old one when the user asks for the DebitCard payment method:

type CreditCardPM struct {} 
 func (d *CreditCardPM) Pay(amount float32) string { 
   return fmt.Sprintf("%#0.2f paid using new credit card implementation
", amount) 
} 

This is our new type that will replace the DebitCardPM structure. The CreditCardPM implements the same PaymentMethod interface as the debit card. We haven't deleted the previous one in case we need it in the future. The only difference lies in the returned message that now contains the information about the new type. We also have to modify the method to retrieve the payment methods:

func GetPaymentMethod(m int) (PaymentMethod, error) { 
    switch m { 
        case Cash: 
        return new(CashPM), nil 
        case DebitCard: 
        return new(CreditCardPM), nil 
        default: 
        return nil, errors.New(fmt.Sprintf("Payment method %d not recognized
", m)) 
   } 
} 

The only modification is in the line where we create the new debit card that now points to the newly created struct. Let's run the tests to see if everything is still correct:

$ go test -v -run=GetPaymentMethod .
=== RUN   TestGetPaymentMethodCash
--- PASS: TestGetPaymentMethodCash (0.00s)
        factory_test.go:18: LOG: 10.30 paid using cash
=== RUN   TestGetPaymentMethodDebitCard
--- FAIL: TestGetPaymentMethodDebitCard (0.00s)
        factory_test.go:28: The debit card payment method message wasn't correct
        factory_test.go:30: LOG: 22.30 paid using new debit card implementation
=== RUN   TestGetPaymentMethodNonExistent
--- PASS: TestGetPaymentMethodNonExistent (0.00s)
        factory_test.go:38: LOG: Payment method 20 not recognized
FAIL
exit status 1
FAIL

Uh, oh! Something has gone wrong. The expected message when paying with a credit card does not match the returned message. Does it mean that our code isn't correct? Generally speaking, yes, you shouldn't modify your tests to make your program work. When defining tests, you should be also aware of not defining them too much because you could achieve some coupling in the tests that you didn't have in your code. With the message restriction, we have a few grammatically correct possibilities for the message, so we'll change it to the following:

return fmt.Sprintf("%#0.2f paid using debit card (new)
", amount) 

We run the tests again now:

$ go test -v -run=GetPaymentMethod .
=== RUN   TestGetPaymentMethodCash
--- PASS: TestGetPaymentMethodCash (0.00s)
        factory_test.go:18: LOG: 10.30 paid using cash
=== RUN   TestGetPaymentMethodDebitCard
--- PASS: TestGetPaymentMethodDebitCard (0.00s)
        factory_test.go:30: LOG: 22.30 paid using debit card (new)
=== RUN   TestGetPaymentMethodNonExistent
--- PASS: TestGetPaymentMethodNonExistent (0.00s)
        factory_test.go:38: LOG: Payment method 20 not recognized
PASS
ok

Everything is okay again. This was just a small example of how to write good unit tests, too. When we wanted to check that a debit card payment method returns a message that contains paid using debit card string, we were probably being a bit restrictive, and it would be better to check for those words separately or define a better formatting for the returned messages.

What we learned about the Factory method

With the Factory method pattern, we have learned how to group families of objects so that their implementation is outside of our scope. We have also learned what to do when we need to upgrade an implementation of a used structs. Finally, we have seen that tests must be written with care if you don't want to tie yourself to certain implementations that don't have anything to do with the tests directly.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.216.7