Iniciar Sesión

Ver la Versión Completa : [Sintaxis] Clases abstractas (Abstract Factory)


Kuk
22 de febrero de 2017, 14:59
Abstract factory

Lets imagine that you have a POS (Point Of Sale) application that supports hardware from a well known supplier. Your most important customer decided to use a different supplier and asked you to adapt your application to use it. You realize that you may need to add support to a third and even a fourth supplier in the future, but you don't want to increase the complexity of your application. What you need is to raise the abstraction bar, isolating the business logic from classes that deal with POS hardware such as keyboards, screens, card readers, receipt printers, barcode scanners etc. A very good solution would be to get rid of "NEW" operator so your application would not be so tighly coupled with any specific class that deals with hardware! How? Meet the Abstract Factory!



This is the Abstract Factory classic definition: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

Don't let this intimidate you. Abstract Factories are not only simple, but pretty flexible solutions that comes in handy very often. Once you master that concept you will not help yourself imagining new and creative uses for it.

The checkout

Checkouts usually comprises:

computer
screen
keyboard
barcode scanner
receipt printer
pin pad with Integrated Card Swipe
cash drawer
credit card reader


Our application should support many preconfigured checkouts, so we would be able to replace a checkout brand with another one without any impact in the business rules. The key here is to keep business logic as simples as possible and completely unaware of which device brand is being used, as long is provides the expected services.

First let see what we are trying to avoid:

CLASS-ID. ProcessOrder AS "NotSoCoolThings.ProcessOrder".

*> ...

method-id. ProcessNewOrder as "ProcessNewOrder".
*> ...
procedure division.
*> ...
*> creating checkout support classes
*> (THAT'S EXACTLY WHAT WE DO NOT WANT TO DO!)

if checkoutType = "HP"
invoke HPFK182AT "NEW" returning cashDrawerHP
invoke HPFK224AT "NEW" returning receiptPrinterHP
else
invoke TeamPos1054258002 "NEW" returning cashDrawerFJ
invoke TeamPoSFD21 "NEW" returning receiptPrinterFJ
end-if

*> Loop getting items and printing

*> ...
*> Just imagine how would be to have another hardware supplier
if chechoutType = "HP"

invoke receiptPrinterHP "printItem" using item, quantity
else
invoke receiptPrinterFJ "printItem" using item, quantity
end-if

*> What if you could mix parts?...there would be endless combinations!

if chechoutType = "HP"

invoke cashDrawerHP "OpenDrawer"
else
invoke cashDrawerFJ "OpenDrawer"
end-if

*> ...
*> There must be a better way to do this!


end method ProcessNewOrder.
end object.
END CLASS ProcessOrder.


The code above clearly shows the high dependency of classes that deal with checkout parts. If we add support to a different hardware supplier, our code would suffer with lots of "ifs" to check which equipament is in use. You could make your life easier by forcing those classes to implement a common interface, but still you would need to code a lot of methods to test the interaction between parts and class creation and all that code would go along business rules...creepy!

These will be our checkout systems:

Fujitsu
. Cash drawer - model TeamPos1054258002
. Receipt printer - model TeamPoSFD21

HP
. Cash drawer - model HPFK182AT
. Receipt printer - model HPFK224AT

In addition to get rid of high dependency (and the mess in the business logic) we would like also to define which set of parts our checkout is made of. We are going to use an XML file to define checkout configuration and which one our application is going to use.

APP.config (xml file)

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<checkout>
<currentSupplier>/configuration/checkout/suppliers/fujitsu</currentSupplier>
<suppliers>
<fujitsu>
<currentSet>/configuration/checkout/suppliers/fujitsu/sets/TeamPos500</currentSet>
<sets>
<TeamPos500>
<cashDrawer>TeamPos1054258002</cashDrawer>
<receiptPrinter>TeamPoSFD21</receiptPrinter>
</TeamPos500>
<TeamPos1000>
<cashDrawer>TeamPos10PB60014</cashDrawer>
<receiptPrinter>TeamPoSFD22</receiptPrinter>
</TeamPos1000>
</sets>
</fujitsu>
<hp>
<currentSet>/configuration/checkout/suppliers/hp/sets/HPPOS4SmallBusiness</currentSet>
<sets>
<HPPOS4SmallBusiness>
<cashDrawer>HPFK182AT</cashDrawer>
<receiptPrinter>HPFK224AT</receiptPrinter>
</HPPOS4SmallBusiness>
</sets>
</hp>
</suppliers>
</checkout>
<db>
<connection>
<oracle>
<DataSource>Data Source=TORCL</DataSource>
<UserId>admin</UserId>
<Password>myPassword</Password>
</oracle>
</connection>
</db>
</configuration>


The XML currently defines two suppliers: Fujitsu and HP. Each supplier could have multiple sets, but only one active. A global setting defines what would be the current supplier used by the app.


The abstract factory

The main interface of this design pattern is the abstract factory interface. This is the interface that will be used by the client app to create concrete factories. Lets call it ICheckoutAbstractFactory:


INTERFACE-ID. ICheckoutAbstractFactory as "AFP.ICheckoutAbstractFactory".
environment division.
configuration section.
repository.
interface ICashDrawer as "AFP.ICashDrawerAbstractProduct"
interface IReceiptPrinter as "AFP.IReceiptPrinterAbstractProduct".
*> here would enter any other abstract product that is part of a checkout.

procedure division.
method-id. CreateCashDrawer as "CreateCashDrawer".
data division.
linkage section.
01 cashDrawer usage object reference ICashDrawer.

procedure division returning cashDrawer.
*> Interfaces do not contain any logic, just the desired interface

end method CreateCashDrawer.
method-id. CreateReceiptPrinter as "CreateReceiptPrinter".
data division.
linkage section.
01 receiptPrinter usage object reference IReceiptPrinter.

procedure division returning receiptPrinter.
*> Interfaces do not contain any logic, just the desired interface

end method CreateReceiptPrinter.

END INTERFACE ICheckoutAbstractFactory.

Kuk
22 de febrero de 2017, 15:03
The concrete factory

A concrete factory implements the abstract factory "create" methods. It will be the only point in the code that we need to update after add a new checkout system. This is a very specialized class which only responsibility is to create the proper parts. Our application has two concrete factories: FujitsuConcreteFactory and HPConcrete Factory. If you analyse the code carefully you maybe realize that we could implement only one factory ;)


CLASS-ID. FujitsuConcreteFactory as "AFP.FujitsuConcreteFactory".
environment division.
configuration section.
repository.
interface ICheckoutAbstractFactory as "AFP.ICheckoutAbstractFactory"
interface ICashDrawerAbstractProduct as "AFP.ICashDrawerAbstractProduct"
interface IReceiptPrinterAbstractProduct as "AFP.IReceiptPrinterAbstractProduct"

*>Cash drawers classes
class ClassTeamPos1054258002 as "AFP.Drivers.CashDrawers.TeamPoS105428002"
class ClassTeamPos10PB60014 as "AFP.Drivers.CashDrawers.TeamPoS0PB60014"

*>Receipt printers classes
class ClassTeamPOSFD21 as "AFP.Drivers.ReceiptPrinters.TeamPOSFD21"
class ClassTeamPOSFD22 as "AFP.Drivers.ReceiptPrinters.TeamPOSFD22"

class ClassUtils as "AFP.Utils"
class CollectionHashTable as "System.Collections.Hashtable"
class ?SystemString as "System.String"
class ?SystemObject as "System.Object".
object. implements ICheckoutAbstractFactory.
data division.
working-storage section.
01 checkoutHardware usage object reference SystemString.
01 receiptPrinterName pic n(80) value spaces.
01 cashDrawerName pic n(80) value spaces.
01 driver usage object reference SystemObject.
01 drivers ?usage object reference CollectionHashTable.

procedure division.
method-id. NEW.
procedure division.
*> Getting all drivers at once
invoke ClassUtils "GetCheckoutDrivers" returning drivers.

end method NEW.

method-id. CreateCashDrawer as "CreateCashDrawer".
data division.
linkage section.
01 cashDrawer usage object reference ICashDrawerAbstractProduct.

procedure division returning cashDrawer.

*> Get cashDrawer and receipt drivers

set driver to drivers::"get_Item"("CashDrawer")
set cashDrawerName to driver as SystemString

*> creating the cash drawer class defined in the configuration file
evaluate cashDrawerName
when n"TeamPos1054258002"
invoke ClassTeamPos1054258002 "NEW" returning cashDrawer
when n"TeamPos10PB60014"
invoke ClassTeamPos10PB60014 "NEW" returning cashDrawer
end-evaluate

end method CreateCashDrawer.
method-id. CreateReceiptPrinter as "CreateReceiptPrinter".
data division.
linkage section.
01 receiptPrinter usage object reference IReceiptPrinterAbstractProduct.

procedure division returning receiptPrinter.
*> Get cashDrawer and receipt drivers

set driver to drivers::"get_Item"("ReceiptPrinter")
set receiptPrinterName to driver as SystemString

*> creating the receipt printer class defined in the configuration file

evaluate receiptPrinterName
when n"TeamPoSFD21"
invoke ClassTeamPoSFD21 "NEW" returning receiptPrinter
when n"TeamPoSFD22"
invoke ClassTeamPoSFD22 "NEW" returning receiptPrinter
end-evaluate

end method CreateReceiptPrinter.
end object.
END CLASS FujitsuConcreteFactory.


The abstract product

An abstract product is the interface that allow the concrete factory to load the proper product. Without that interface, the concrete factory would be useless (or much more complex than the necessary). Below the interface for Receipt Printers:


INTERFACE-ID. IReceiptPrinterAbstractProduct as "AFP.IReceiptPrinterAbstractProduct".
environment division.
configuration section.
repository.
interface ICashDrawerAbstractProduct as "AFP.ICashDrawerAbstractProduct"

class SystemString as "System.String"
class ClassUser as "AFP.User"
class ClassCompany as "AFP.Company"
class ClassProduct as "AFP.Product".

procedure division.
method-id. StartNewSale as "StartNewSale".
data division.
linkage section.
01 company ?usage object reference ClassCompany.
01 user usage object reference ClassUser.

procedure division using by value company, by value user.

end method StartNewSale.

method-id. RegisterItem as "RegisterItem".
data division.
linkage section.
01 product?usage object reference ClassProduct.
01 quantity usage binary-long signed.

procedure division using by value product, by value quantity.

end method RegisterItem.
method-id. PrintTotal as "PrintTotal".
data division.
linkage section.
01 cashDrawer usage object reference ICashDrawerAbstractProduct.

procedure division using by value cashDrawer.

end method PrintTotal.

*> ... and any other interfaces that you may need to implement
*> (e.g. CancelItem, CancelTransaction, PrintDiscount etc

END INTERFACE IReceiptPrinterAbstractProduct.


The concrete product

A concrete product in our checkout architecture take care of all communications with the hardware, so we are calling them drivers in this context. Here is the a concrete product for a Fujitsu Cash Drawer:


CLASS-ID. TeamPoS0PB60014 as "AFP.Drivers.CashDrawers.TeamPoS0PB60014".
environment division.
configuration section.
repository.
interface ICashDrawerAbstractProduct as "AFP.ICashDrawerAbstractProduct".
object. implements ICashDrawerAbstractProduct.

procedure division.
method-id. OpenDrawer as "OpenDrawer".

procedure division.

display "Fujitsu cash drawer TeamPoS0PB60014 opened..."

*> here would go specific cash drawer API calls...

end method OpenDrawer.

end object.
END CLASS TeamPoS0PB60014.


Using our factory

So, here is our abstract factory in action. You will notice that we are no longer directly instantiating the driver´s classes. Actually, that code has no idea which driver has been choosen. This has been delegated to the concrete factory. One very insteresting aspect is that our checkout parts can talk each other (for instance, when closing a sale, the receipt printer would open the cash drawer automatically).


CLASS-ID. ProcessOrder AS "NowYouAreTalking.ProcessOrder".
*> ...
method-id. ProcessNewOrder as "ProcessNewOrder".
*> ...
procedure division.

*> ...
*> Creating a concrete factory
invoke ClassFujitsuConcreteFactory "NEW" returning checkout

*> Creating cash drawer and receipt printer classes
invoke checkout "CreateCashDrawer"returning cashDrawer
invoke checkout "CreateReceiptPrinter" returning receiptPrinter
*> ...

*> Using receipt printer
invoke receiptPrinter "StartNewSale" using aCompany, aUser
invoke receiptPrinter "RegisterItem" using aProduct, 10

*> close order and open the cashDrawer
invoke receiptPrinter "PrintTotal" using cashDrawer
end method ProcessNewOrder.
end object.
END CLASS ProcessOrder.


Abstract Factories are eveywhere

ATMs, RTS games, DB connections, web frameworks and many more applications uses that useful pattern. Some examples:

Spring.Net (Web Framework)
.Net ADO.Net
Microsoft COM (yes, even the COM implements that pattern)


Download the source

Cool things with OO Cobol - Source Code (http://cobolrocks.codeplex.com/SourceControl/changeset/view/40357#)


Some references in the web

Abstract Factory .NET Design Pattern in C# and VB - dofactory.com (http://www.dofactory.com/Patterns/PatternAbstract.aspx)
Abstract factory pattern - Wikipedia (http://en.wikipedia.org/wiki/Abstract_factory_pattern)