javaswingpath-findingmazecustom-painting

How to support and paint multiple algorithms


I have been working on a maze generator/solver program. It works nicely, but I came across a problem while refactoring my code.

Basically, I used to call methods that draw the process of solving a maze directly from another class, as opposed to calling the paintComponent method (or rather the repaint method). As obviously that is not a good practice, nor is it satisfactory performance-wise I am trying to get around that.

The problem is, to draw the sub-steps of a solution, different solver algorithms need different types and numbers of arguments. I could store these in a class, then call the paintComponent method, in which I invoke the method that paints the sub-steps using the aforementioned arguments.

Unfortunately, that would mean that I have to create a bunch of other classes that extend JPanel, just so I can store the necessary collections and variables, just so I can draw that one specific solution.

Is there a nicer way to get around this, or should I just give up and do it the way I mentioned?

So what I am trying to do:

@Override
public void paintComponent(Graphics g){
    super(g);
    drawMaze(g);
    switch(solverType) //Based on what solver is assigned to the maze it calls the proper method
    {
       case solver1:
          solver1Drawer(g, additional arguments);
       break;
       case solver2:
          solver2Drawer(g, different kind, and number of arguments);
       break;
       //Other cases, with other method calls
    }
}

Solution

  • There are many ways to build such applications that can accept different solvers and views. I'll try to demonstrate a very basic, stripped-down one in an effort to make it as simple as possible.
    For this purpose, we'll use a solver that calculates the perimeter of a certain shape and draw the solution.
    For a start, we'll define some interfaces, the use of which should become clearer later:

    interface Model{
        boolean solve();
        int solution();
        boolean isSolved();
    }
    
    interface View{
        void draw(Graphics g);
    }
    
    interface Solver {
        Model getModel();
        View getView();
    }
    

    Using these interfaces we'll define the Model, the View and the Controller to support the calculation of a perimeter of a square.
    The model encapsulates the information (attributes and states) and logic :

    class SquarePrimeterModel implements Model{
    
        private final int edgeLength;
        private int perimeter;
        private boolean isSolved = false;
    
        public SquarePrimeterModel(int edgeLength) {
            this.edgeLength = edgeLength;
        }
    
        @Override
        public boolean solve() {
            perimeter = 4 * edgeLength;
            isSolved = true;
            return true;
        }
    
        @Override
        public int solution() {
            return perimeter;
        }
    
        @Override
        public boolean isSolved() {
            return isSolved;
        }
    
        //edgeLength is a unique property for this model
        public int getEdgeLength() {
            return edgeLength;
        }
    }
    

    The responsibility of the view, as its name suggests to generate the view:

    class SquarePrimeterView implements View{
    
        private final static int xOffset = 50, yOffset = 50, GAP = 20;
        private final SquarePrimeterModel model;
    
        public SquarePrimeterView(SquarePrimeterModel model) {
            this.model = model;
        }
    
        @Override
        public void draw(Graphics g) {
            if(model.isSolved()){
                g.drawRect(xOffset, yOffset, model.getEdgeLength(), model.getEdgeLength());
                String text = "Edge =" + model.getEdgeLength() + " Perimiter ="+ model.solution();
                int yPosition = yOffset + model.getEdgeLength()+ GAP;
                g.drawString(text, xOffset, yPosition);
            }
        }
    }
    

    The controller constructs, configures and manages the model and the view:

    class SquarePrimeterController implements Solver{
    
        private final SquarePrimeterModel model;
        private final View view;
    
        public SquarePrimeterController(int edgeLength) {
            model = new SquarePrimeterModel(edgeLength);
            view = new SquarePrimeterView(model);
        }
    
        @Override
        public Model getModel() {
            return model;
        }
    
        @Override
        public View getView() {
            return view;
        }
    }
    

    If we need another solver, for example, a solver to calculate the perimeter of a triangle, we simply have to write TriangelPrimeterController, TriangelPrimeterModel and TriangelPrimeterView, very similar to the SquarePrimeter classes.

    The last pice of code that we need is the application that uses SquarePrimeterController:

    public class SwingMVCSolveController {
    
        public SwingMVCSolveController() {
            Solver solver = new SquarePrimeterController(150);//todo: select solver by gui 
            solver.getModel().solve(); //todo start solve by gui 
            new MainView(solver.getView());
        }
    
        public static void main(String[] args) {
            new SwingMVCSolveController();
        }
    }
    
    class MainView extends JPanel {
    
        private static final Dimension size = new Dimension(400, 400);
        private final View solverView;
        public MainView(View view) {
            solverView = view;
            createAndShowGui();
        }
    
        private void createAndShowGui() {
            JFrame frame = new JFrame ();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setLocationRelativeTo(null);
            frame.add (this);
            frame.pack();
            frame.setVisible (true);
        }
    
        @Override
        public Dimension getPreferredSize() {
            return size;
        }
    
        @Override
        protected void paintComponent(Graphics g){
            super.paintComponent(g);
            solverView.draw(g);
        }
    }
    

    Clearly Solver solver = new SquarePrimeterController(150); can be changed to Solver solver = new TrianglePrimeterController(150); or any other implementation of Solver.

    A full runnable code is available here