Object-oriented – Design pattern for similar classes that require different implementations

design-patternsobject-orientedobject-oriented-designpython

Edited: Update is at the bottom

There could be a common or best practice for this scenario, but I am unfamiliar with it. However, it could very easily be a matter of subjective opinion on how one wants to implement their classes. Either way I am hopefuly to get some opinion from the spectrum of class designers here.

I am currently working on a project that allows users to generate files for data visualization.

The library will support two file types that are formatted differently (binary and XML). Due to this, I am left with a dilemma on how I want to control class instantiation and API access:

  1. Create a separate class for each file type and visualization type
  2. Create a separate class for each visualization type and load with methods for each file type
  3. (Not demonstrated) The inverse of Option 2

Option 1:

class Base:
    # Stuff
class F1V1(Base):
    # Methods specific to file and visualization type one
class F1V2(Base):
    # Methods specific to file type one and visualization type two
class F1V3(Base):
    # Methods specific to file type one and visualization type three
class F2V1(Base):
    # Same logic as before but for file type two
class F2V2(Base):
    # ...
class F2V3(Base):
    # ...

Here a user of the library would call their direct class to perform operations specific to it without the need of setting keyword parameters to determine what it does (e.g. fv = F1V2())

What is great about this way is that its explicit, a user knows exactly what file and visualization type they are working with. But I think its cumbersome and not extensible in the event I want to add more file or visualization types; forcing me to write a new class for each possible combination.

Option 2:

class Base:
    # Stuff
class V1(Base):
    def __init__(self, ftype_1=True)
        self.ftype_1 = ftype_1
    def write(self):
        if self.ftype_1:
            # Write method specific to file type one
        # Write method specific to file type two
    # Assume 3 methods for each operation
    # One method such as `write()`
    # One method for each of the file types
class V2(Base):
    # Same logic as before
class V3(Base):
    # ...

What I don't like about this method is that I now have to define multiple methods for each class that execute based upon keywords provided at construction (i.e. fv = V2(ftype_1=False)). Ideally, what I would like is to supply a keyword argument that then determines which of the methods should belong to that class. For example:

fv = V2() # Only contains methods to file type one operations
fv = V2(ftype_1=False) # Only contains methods to file type two operations

As shown, there is nothing that prevents the following:

fv = V2() # Set for file type one
fv.write_ftype_2() # Will produce an invalid file type formatting

I am not sure of a way where I can dynamically bind/remove methods based upon keyword arguments. It would be great if I could simply write all the methods for each file type within each visualization type, then remove methods that are not relevant to the class anymore. Im not sure if this is even advisable, I can already think of a scenario like so:

def write(self):
    if self.ftype_1:
        # Write method specific to file type one
    elif self.type_2:
        # ...
    else:
        # ...

If I dynamically removed methods from a class based upon keyword arguments, what would the point of the conditions be if say the first one held?

Summary:

So which is a common or best practice? Which can be improved? Or am I missing another way?

An ideal example would be (in my mind):

fv = Hexagons(ftype='.abc', flat=True, area=3)
fv.insert(data)
fv.write(data) # Specifically writes for file types of '.abc'

I suppose I could make Hexagons() return a subclass via __new__ but I think that might be unclear as to what is happening. To call Hexagons() but receive a ABCHexagons object could lead to confusion when users inspect the code base.

A factory method is ideal for this, but that is simply class instantiation. But each visualization type may have a variety of different keyword parameters that may not apply to the others. Rather, my issue lies with how to define them in the code base which ultimately leads to solving how to expose them to users.

Update:

After @Mihai and @Ewan gave suggestions, it is clear that having a writer class for each file type is the best way to go and inheritence is not. Now I need to examine if composition is a better strategy and I would like to clarify some details.

  1. Each file type contains data that is formatted to represent a grid of shapes
  2. A visualizer class is not used to display any data a user inputs
  3. A visualizer class is used solely to determine how the write class writes

For example:

class binaryWriter:
    # Writes only binary files
class xmlWriter:
    # Writes only XML files
class Hexagons:
    # Contains methods for determining geometry
class Triangles:
    # Same as Hexagons

Suppose I wanted to write a binary file containing an array of hexagon tiles. Once the visualizer (i.e. the hexagonal grid) is selected, operations within the writer and visualizer class work together to determine how the write class writes to a file on disk. Lets say I want to insert a point in my grid and figure out which hexagon it belongs to, I would do:

w = binaryWriter(shape='hexagon')
w.insert(1, 2) # Some point of x, y
# Repeat insertions
w.save() # Write to file on disk

Best Answer

As you correctly noticed, using inheritance to solve this problem, in the way you presented, is not a good idea. You should look into using composition instead and I would suggest looking at the broader concept of composition over inheritance. To name a more specific design pattern, I think what you need is Strategy.

In this particular case, what I would do is:

  • have a group of similar classes for reading the data; they all look and behave the same, only their internal implementation is different (one reads binary, one reads XML).
  • have an internal contract of how the data looks like once it has been read. This is important in order to guarantee compatibility for all the possible reader-visualisation combinations. All readers output the data in the same format, all visualisations know how to read data from that single format.
  • have a group of similar classes for the visualisations; they all look and behave the same; they receive the data in the internal "universal" format and do something with it.

Then you only need to dynamically combine one instance of the data reading class with one instance of the visualisation and pass the data between them, at run time. The decisions on which data read class and which visualisation class to use will be yours to take (and code). A very simple flow would look like this:

reader = binaryDataReader()
data = reader.readData()
visualisation = prettyColorsVisualisation()
visualisation.display(data)
Related Topic