Blog Datasheets Home About me Clients My work Services Contact

G2Labs Grzegorz Grzęda

Abstract factory pattern

April 28, 2023

The main idea behind the abstract factory is to obscure the creation of objects. Why to complicate this basic concept? Because of the potential ‘multi-platform code compilation’.

When our code needs to be recompiled with multiple wariants of classes implementing same interfaces, there is a high risk of spreading #ifdef...#elseif...#endif macros with the new and delete operators all over the application code.

Abstract Factory prevents from such spread offering a collective object creation approach. We actually get a factory, producing objects implementing same interfaces, but don’t need to worry (on a certain level) about the details of which concrete class should be instantiated.

Two variant example

On my GitHub page i published the following example project. The project is compiled using a simple Makefile.

This is a simple application, where we create two types of objects:

Also, we have an App class, which is platform agnostic. This means, that the code inside does know nothing about implementation details of the Doer and Waiter. The only parts ‘aware’ of those details would be: main() and factories producing those details.

The project will consist of factories:

The code

The whole code is just a presentation of what we can achieve with the abstract factories. The ‘doers’ and ‘waiters’ can be everything in real life: DB connectors, hardware drivers, TDD mocks, etc.

The Doers and Waiters:

1
2
3
4
5
6
// IDoer
struct IDoer
{
	virtual ~IDoer() {}
	virtual void doIt() = 0;
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// SmallDoer
#include "IDoer.hpp"
#include <iostream>

struct SmallDoer : IDoer
{
	void doIt()
	{
		std::cout << "[SMALL DOER]: doing... something small" << std::endl;
	}
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// OtherDoer
#include "IDoer.hpp"
#include <iostream>

struct OtherDoer : IDoer
{
	void doIt()
	{
		std::cout << "[OTHER DOER]: doing... something else" << std::endl;
	}
};
1
2
3
4
5
6
// IWaiter
struct IWaiter
{
	virtual ~IWaiter() {}
	virtual void waitForIt() = 0;
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// SmallWaiter
#include "IWaiter.hpp"
#include <iostream>

struct SmallWaiter : IWaiter
{
	void waitForIt()
	{
		std::cout << "[SMALL WAITER]: waiting for... something small" << std::endl;
	}
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// OtherWaiter
#include "IWaiter.hpp"
#include <iostream>

struct OtherWaiter : IWaiter
{
	void waitForIt()
	{
		std::cout << "[OTHER WAITER]: waiting for... something else" << std::endl;
	}
};

Those classes simply print their behavior onto stdout, so that you can see which implementation is currently being instantiated.

The factories:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// IFactory
#include "IDoer.hpp"
#include "IWaiter.hpp"

struct IFactory
{
	virtual ~IFactory() {}
	virtual IDoer *getNewDoer() = 0;
	virtual IWaiter *getNewWaiter() = 0;
};

The Factory interface tells us, that a factory will deliver both, the IDoer and IWaiter objects from the same architecture/approach. The SmallFactory will deliver the SmallDoer and SmallWaiter and the OtherFactory accordingly the Other... architecture.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// SmallFactory
#include "SmallDoer.hpp"
#include "SmallWaiter.hpp"
#include "IFactory.hpp"

struct SmallFactory : IFactory
{
	IDoer *getNewDoer()
	{
		return new SmallDoer();
	}
	IWaiter *getNewWaiter()
	{
		return new SmallWaiter();
	}
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// OtherFactory
#include "OtherDoer.hpp"
#include "OtherWaiter.hpp"
#include "IFactory.hpp"

struct OtherFactory : IFactory
{
	IDoer *getNewDoer()
	{
		return new OtherDoer();
	}
	IWaiter *getNewWaiter()
	{
		return new OtherWaiter();
	}
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// SystemFactory
#include "SmallFactory.hpp"
#include "OtherFactory.hpp"

struct SystemFactory : IFactory
{
	SystemFactory(bool isSmallArchitecture)
	{
		if (isSmallArchitecture)
			f = new SmallFactory;
		else
			f = new OtherFactory;
	}
	~SystemFactory() { delete f; }

	IDoer *getNewDoer()
	{
		return f->getNewDoer();
	}

	IWaiter *getNewWaiter()
	{
		return f->getNewWaiter();
	}

private:
	IFactory *f;
};

The SystemFactory is where the magic happens! Here, depending on what we provided for the constructor, the according factory is created. Next, this module will create objects aligned to the initial setting throughout the application.

The app and main():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//app
#include "IDoer.hpp"
#include "IWaiter.hpp"

struct App
{
	App(IDoer *d, IWaiter *w) : d(d), w(w) {}

	void run()
	{
		d->doIt();
		w->waitForIt();
		w->waitForIt();
		d->doIt();
		w->waitForIt();
	}

private:
	IDoer *d;
	IWaiter *w;
};

The App doesn’t know (or care) what was the precise architecture of the supplied IDoer and IWaiter objects. It just operates on them as needed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//main()
#include "SystemFactory.hpp"
#include "App.hpp"

int main(int argc, char *argv[])
{
	auto factory = new SystemFactory(true);
	auto doer = factory->getNewDoer();
	auto waiter = factory->getNewWaiter();

	auto app = new App(doer, waiter);

	app->run();

	delete app;
	delete waiter;
	delete doer;
	delete factory;

	return 0;
}

The main() creates the system factory, with the concrete setting. Currently it is set on providing the Small... architecture to the App object. Notice that here, both the doer and waiter pointers are indeed IDoer* and IWaiter* hiding under the auto keyword (check it on your own).

When you compile and run the app, you should see:

1
2
3
4
5
6
>abstract-factory-pattern-example-cpp.exe
[SMALL DOER]: doing... something small
[SMALL WAITER]: waiting for... something small
[SMALL WAITER]: waiting for... something small
[SMALL DOER]: doing... something small
[SMALL WAITER]: waiting for... something small

When you change the true to false, the program changes its behavior to:

1
2
3
4
5
6
>abstract-factory-pattern-example-cpp.exe
[OTHER DOER]: doing... something else
[OTHER WAITER]: waiting for... something else
[OTHER WAITER]: waiting for... something else
[OTHER DOER]: doing... something else
[OTHER WAITER]: waiting for... something else

Benefits

The Abstract Factory pattern gives us:

Traps

You need to take care:

Conclusion

Abstract Factory is a great way to group and manage your dependency injections with ease. No more spreaded if's with custom object creation. No more rabbit chases about those if's you’ve forgotten!


➡️ Implementing the Null Object Pattern in C++ and C


⬅️ Design patterns intro


Go back to Posts.