7

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

2 Answers 2

2

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)
2
  • Looks like your answer isnt complete; did you mean to supply more?
    – pstatix
    Commented Jan 11, 2019 at 9:21
  • @datta yes, I accidentaly posted it before I was done. Commented Jan 11, 2019 at 9:26
2

Separate the Object in code from its persistence format.

Imagine I add a requirement where I want to load binary files and save them as xml files.

Hexagons v = binaryFileReader.Load(filename);
v.AddHex(1,2);
xmlFileWriter.Save(v);

the Hexagon class is only concerned about the visualization. It only has methods related to setting the colours, adding new elements etc etc

The binaryFileReader/Writer class only knows about converting in memory Hexagon objects to the binary format file and back again. Similarly the xmlFileWriter just knows about xml and Hexagons.

Now if I add a new Visualization type i have options. I can improve the file readers, adding in some knowledge about multiple visualisations, Or I can create a brand new class that deals with the new visualisation type.

Similarly, if I add a new file type, I can create a new CsvFileReaderWriter object to deal with that.

17
  • Still chewing on this idea. In the meantime, does this use composition? It clearly doesnt use inheritance. For example, a binaryFileReader object may take a filename as an parameter at construction, and its self.visualizer might be a Hexagon object. Given what I intend to do, I don't plan to return an object like you've shown here after reading, but thats alterable to my case quite easily.
    – pstatix
    Commented Jan 11, 2019 at 9:57
  • Yeah, No inheritance in my example and you really should be returning an object and not tacking the visualiser onto the file reader
    – Ewan
    Commented Jan 11, 2019 at 10:02
  • Returning becomes somewhat problematic, and I will need to think on it. And perhaps not with a reader, but a binaryFileWriter may have a self.visualizer = Hexagons(). Thats valid composition right? I'm afraid I havent used composition intentionally before.
    – pstatix
    Commented Jan 11, 2019 at 10:05
  • It would be helpful if you expanded your question with more information on exactly what these classes are supposed to do. But no, you do not want to have a self.visualiser property.
    – Ewan
    Commented Jan 11, 2019 at 10:08
  • I'll have to add that detail later, but why not? Don't I need it to use composition? For example, writer = binaryFileWriter(shape='hex'). Now the writer.visualizer is a Hexagons() object. That what I call call writer.add(1, 2) and it calls the self.visualizer to update it accordingly. Some confusion may be coming from the fact that Hexagons() isnt just a single hexagon shape, but rather a grid.
    – pstatix
    Commented Jan 11, 2019 at 10:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.