Search code examples
pythoninheritanceumlabstract-classclass-diagram

How to draw UML diagram for python with abstract classes


I have a base Abstract class Function and child classes ExpFunction, LogFunction, and CubicFunction. I have Main class as below:

class Main:
    """
    Main class to run the program
    """

    def __init__(self):
        self.f_1 = ExpFunction("Exponential", 0.0, 1.0, 3.0, -5.0, 0)
        self.f_2 = LogFunction("Logarithmic", 0.0001, 0.05, 3.0, 10.0, 0.001)
        self.f_3 = CubicFunction("f3", 1.0, 2.0, 2.0, -3.0, 4.0, -5.0, 0.0001)

    def _run(self):
        """
        Run the program
        """
        print("Starting the program...\n\n")
        self.f_1.print_function()
        self.f_1.find_roots()

        self.f_2.print_function()
        self.f_2.find_roots()

        self.f_3.print_function()
        self.f_3.find_roots()

        print("Program finished.")

    def __call__(self, *args, **kwds):
        self._run()


if __name__ == "__main__":
    main = Main()
    main()

I have drawn the following diagram: enter image description here

enter image description here

Is this correct diagram?


Solution

  • Abstract classes and operations

    We cannot say if Function and its 3 specialization are correct, since we don't have the corresponding code. But if it is abstract, it should be shown as such in the diagram (italic class name), and so should be the abstract operations (aka member functions in python) that you have explicitly defined in each of the 3 specialization.

    Is this the real design?

    In your code snippet, Main has f_1, f_2 and f_3. Your diagram says that these are always LogFunction, ExpFunction and CubicFunction. Considering the current code, this is not wrong. Here the diagram (simplified version) automatically generated by pyreverse (note that the pyreverse layout is not so nice as yours, and it also doesn't show the abstract items in italic):

    enter image description here

    However, nothing prevents you from changing f_1, f_2 and f_3 to be other kinds of Function, for example by adding the following reassignment in at the end of _run():

        self.f_2 = CubicFunction(...)
        self.f_2.print_function()
    

    You could also chose to initialize them differently, because you use only their common abstract interface. You could therefore decide to show Main associated with 3 Function, and leave it to the runtime to decide which of the subclass to instantiate:

    enter image description here

    This diagram would also be correct. Moreover it corresponds to a more powerful design that better use the abstraction and polymorphism that your abstract class allows.

    Since your code does not give a type hint and since python is dynamically typed, we cannot tell which of the two alternative design is the one you wanted. Only you know what you intended to do.

    To generate the second diagram with pyreverese, I updated the initialization to use type hints; this makes the design intent more explicit in the code:

    def __init__(self):
        self.f_1:Function = ExpFunction()
        self.f_2:Function = LogFunction()
        self.f_3:Function = CubicFunction()
    

    As a side remark, feel free to remove the shared aggregation (hollow diamond): UML does not define any semantic for shared aggregation (since more than 20 years!) : a simple association would express exactly the same thing.