Design Patters: Abstract Factory

Credit: Concepts explained here are based on a book, “増補改訂版Java言語で学ぶデザインパターン入門”.

In an abstract factory pattern, different abstract components are combined to create an abstract product.

Let’s see a sample program which creates a HTML file. There are two packages: factory package and listfactory package. A factory package contains classes that create an abstract factory, components and products. A listfactory package contains classes that create a concrete factory, components and products.

Item class is a superclass of Link class and Tray class which are defined next. Its abstract method, make_html, is to return strings of HTML.

# item.py
from abc import ABC, abstractmethod

class Item(ABC):
    def __init__(self, caption):
        self._caption = caption

    @abstractmethod
    def make_html(self):
        pass

Link class represents a hyperlink in HTML.

# link.py
from factory.item import Item

class Link(Item):
    def __init__(self, caption, url):
        super().__init__(caption)
        self._url = url

Tray class represents a tray that holds a list of Link instances or Tray instances themselves.

# tray.py
from factory.item import Item 

class Tray(Item):
    def __init__(self, caption):
        super().__init__(caption)
        self._tray = []
    
    def add(self, item):
        self._tray.append(item)

A page class represents a HTML file. An add method appends an Item instance (Link instance or Tray instance) to a content attribute. An output method writes a content of HTML into a file.

# page.py
from abc import ABC, abstractmethod

class Page(ABC):
    def __init__(self, title, author):
        self._title = title
        self._author = author
        self._content = []

    def add(self, item):
        self._content.append(item)

    def output(self):
        filename = f"{self._title}.html"
        with open(filename, "w") as f:
            f.write(self.make_html())
        print(f"{filename} is created.")
    
    @abstractmethod
    def make_html(self):
        pass

Factory class returns a concrete factory instance.

# factory.py
from factory.helper import Class
from abc import ABC, abstractmethod

class Factory(ABC):
    @classmethod
    def get_factory(cls, class_name):
        try:
            factory = Class(class_name).new_instance()
        except Exception as e:
            raise Exception(e)
        return factory

    @abstractmethod
    def create_link(self, caption, url):
        pass

    @abstractmethod
    def create_tray(self, caption):
        pass

    @abstractmethod
    def create_page(self, title, author):
        pass

Now let’s define a main file which builds an abstract component and constructs an abstract product by using an abstract factory. For example, in oder to use ListFactory class, the following command is used: python main.py listfactory.ListFactory

# main.py
from factory import Factory
import sys

def main(*args):
    try:
        factory = Factory.get_factory(args[0][1])
    except IndexError:
        raise Exception("No class argument is given.")

    assert isinstance(factory, Factory)

    nytimes = factory.create_link("New York Times", "https://www.nytimes.com/")
    financial_times = factory.create_link("Financial Times", "https://www.ft.com/")
    
    yahoo_us = factory.create_link("Yahoo!", "https://www.yahoo.com/")
    yahoo_jp = factory.create_link("Yahoo!Japan", "https://www.yahoo.co.jp/")

    instagram = factory.create_link("Instagram", "https://www.instagram.com/")
    twitter = factory.create_link("Twitter", "https://twitter.com/")

    tray_news = factory.create_tray("News")
    tray_news.add(nytimes)
    tray_news.add(financial_times)

    tray_yahoo = factory.create_tray("Yahoo!")
    tray_yahoo.add(yahoo_us)
    tray_yahoo.add(yahoo_jp)

    tray_sns = factory.create_tray("SNS")
    tray_sns.add(instagram)
    tray_sns.add(twitter)

    page = factory.create_page("LinkPage", "Mr.X")
    page.add(tray_news)
    page.add(tray_yahoo)
    page.add(tray_sns)
    page.output()


if __name__ == "__main__":
    main(sys.argv)

We now define concrete classes. First one is ListFactory class, a subclass of Factory class. This class creates an instance of ListLink class, ListTray class and ListPage class.

# list_factory.py
from factory import Factory 
from listfactory.list_link import ListLink
from listfactory.list_page import ListPage
from listfactory.list_tray import ListTray


class ListFactory(Factory):
    def create_link(self, caption, url):
        return ListLink(caption, url)
    def create_tray(self, caption):
        return ListTray(caption)
    def create_page(self, title, author):
        return ListPage(title, author)

The second one is ListLink class which extends Link class and overwrites an abstract method, make_html, to create a segment of html.

# list_link.py
from factory import Link

class ListLink(Link):
    def __init__(self, caption, url):
        super().__init__(caption, url)
    def make_html(self):
        return f" <li><a href='{self._url}'>{self._caption}<a></li>\n"

The next one is ListTray class which makes HTML by combining each ListLink instance.

# list_tray.py
from factory import Tray
from listfactory.string_buffer import StringBuffer

class ListTray(Tray):
    def __init__(self, caption):
        super().__init__(caption)
    
    def make_html(self):
        buffer = StringBuffer()
        buffer.append("<li>\n")
        buffer.append(f"{self._caption}\n")
        buffer.append("<ul>\n")
        for item in self._tray:
            buffer.append(item.make_html())
        buffer.append("</ul>\n")
        buffer.append("</li>\n")
        return buffer.to_string()

The last one is ListPage class which combines a list of items and returns strings used in HTML.

# list_page.py
from factory import Page, Item
from listfactory.string_buffer import StringBuffer

class ListPage(Page):
    def __init__(self, title, author):
        super().__init__(title, author)
    
    def make_html(self):
        buffer = StringBuffer()
        buffer.append(f"<html><head><title>{self._title}</title></head>\n")
        buffer.append(f"<body>\n")
        buffer.append(f"<h1>{self._title}</h1>\n")
        buffer.append("<ul>\n")
        for item in self._content:
            assert isinstance(item, Item)
            buffer.append(item.make_html())
        buffer.append("</ul>\n")
        buffer.append(f"<hr><address>{self._author}</address>")
        buffer.append("</body></html>\n")
        return buffer.to_string()

When you run this program: python main.py listfactory.ListFactory , LinkPage.html is created.

# LinkPage.html
 Yahoo!
 <ul>
  <li><a href='https://www.yahoo.com/'>Yahoo!<a></li>
  <li><a href='https://www.yahoo.co.jp/'>Yahoo!Japan<a></li>
 </ul>
 </li>
 <li>
 SNS
 <ul>
  <li><a href='https://www.instagram.com/'>Instagram<a></li>
  <li><a href='https://twitter.com/'>Twitter<a></li>
 </ul>
 </li>
 </ul>
 <hr><address>Mr.X</address> </body></html>

One benefit of Abstract Factory pattern: it is easy to add a concrete factory. When you need to create a new concrete factory along with subclasses of Factory, Link Tray and Page classes, there is no need to modify any source code in either AbstractFactory and Main class (main.py).

Leave a comment