Статьи

Чистый код с помощью Swing и Scala

Я думаю, все, кто знает Java и Swing, также знают Swing Tutorial . Это отличный источник информации, если вы хотите изучить Swing. Это также большая катастрофа, когда дело доходит до структурирования кода. Проблема в том, что многие люди пропускают важную информацию, содержащуюся в учебном пособии, например «Все, что связано с Swing, должно происходить в EDT», но, похоже, они поглощают грязный способ структурировать код, как губка.

Это может привести к коду, подобному приведенному ниже. На самом деле это код Scala, но это не должно иметь большого значения. Единственное, что немного особенное, — это вызовы Binder, который представляет собой небольшой Swing Binding Framework, который я представил пару недель назад.

private def createPersonPanel(p : PersonEditor) = {

            val panel = new JPanel()
            val layout = new GridBagLayout()
            val c = new GridBagConstraints()
            c.gridx = 0
            c.gridy = 0
     
            panel.setLayout(layout)
     
            c.fill = 0
            panel.add(new JLabel("firstname"), c)
     
            c.fill = 1
            c.gridx += 1
            c.weightx = 1
            val firstnameTF = new JTextField()
            panel.add(firstnameTF, c)
            Binder.bind(p.firstname, firstnameTF)
     
            c.fill = 0
            c.weightx = 0
            c.gridx = 0
            c.gridy += 1
     
            panel.add(new JLabel("lastname"), c)
            c.fill = 1
     
            c.gridx += 1
            val lastnameTF = new JTextField()
            Binder.bind(p.lastname, lastnameTF)
            panel.add(lastnameTF, c)
            c.fill = 0
            c.weightx = 1
            c.gridy += 1
            c.anchor = GridBagConstraints.SOUTHEAST
            val button = new JButton("save")
            Binder.bind(p.save, button)
            panel.add(button, c)
            panel
        }

Что такого плохого в этом клочке дерьма?

Метод долгий. 38 строк примерно в 10 раз длиннее, чем здоровые для метода.

Есть тонны дублирования кода.

Метод делает много разных вещей: создание компонентов, добавление их на панель, настройка макета.

There is a strong dependency on the order of commands. We can’t just move stuff up or down in the method and still hope the result will be something reasonable, even if we stick to rearrangements allowed by the compiler.

All this together makes the method extremely hard to understand. How long does it take to understand what kind of GUI results? Don’t bother to much, I help. It looks like this:

and if you resize it, it looks like this

Arguably the result looks just as ugly as the code, but once the code is clean we might be able to improve on the visual design as well.

If you don’t see it in the code, you might see it in the images: There are three different ways JComponents are handled by the method: JLabels are in the left column and don’t resize. The JTextFields are in the right column and do resize and the JButton doesn’t resize and is in the buttom right. In the code this is completely hidden in the manipulation of the GridBagConstraint. So lets make it explicit in the code:

private def addToLabelColumn(

            panel : JPanel,
            component : JComponent,
            row : Int) {
            val c = new GridBagConstraints()
            c.gridx = 0
            c.gridy = row
            c.weightx = 0
            c.fill = 0
            panel.add(component, c)
        }
     
        private def addToComponentColumn(
            panel : JPanel,
            component : JComponent,
            row : Int) {
            val c = new GridBagConstraints()
            c.gridx = 1
            c.gridy = row
            c.weightx = 1
            c.fill = 1
            panel.add(component, c)
        }
     
        private def addButton(
            panel : JPanel,
            component : JButton,
            row : Int) {
            val c = new GridBagConstraints()
     
            c.weightx = 1
            c.gridx = 1
            c.gridy = row
            c.fill = 0
            c.anchor = GridBagConstraints.SOUTHEAST
            panel.add(component, c)
        }
     
        private def createPersonPanel(p : PersonEditor) = {
            val panel = new JPanel()
            val layout = new GridBagLayout()
            panel.setLayout(layout)
     
            addToLabelColumn(panel, new JLabel("firstname"), 0)
     
            val firstnameTF = new JTextField()
            Binder.bind(p.firstname, firstnameTF)
            addToComponentColumn(panel, firstnameTF, 0)
     
            addToLabelColumn(panel, new JLabel("lastname"), 1)
     
            val lastnameTF = new JTextField()
            Binder.bind(p.lastname, lastnameTF)
            addToComponentColumn(panel, lastnameTF, 1)
     
            val button = new JButton("save")
            Binder.bind(p.save, button)
            addButton(panel, button, 2)
            panel
        }

I introduced three methods. One for adding a JLabel, one for adding a JComponent and one for adding JButtons. These handle the arrangement of components on a JPanel. The total length of the code increased because we create the GridBagConstraints insided the methods and have to set all properties and don’t rely anymore on the previous step to leave the constraint in a specific state.

If we now look at the createPersonPanel we’ll get a strong fealing of repetition in various places:

  • each call to add* methods takes the same JPanel as an argument. We can improve on this by creating a Builder wich crates the panel, contains the add* methods and can return the fully configured panel at the end.
  • for each property we create a JLabel, a JTextField, bind the property to the later and add both to the JPanel. We can fix this by encapsulating it in a seperate method.

The result might look like this:

  case class PanelBuilder() {
        val panel = new JPanel()
        val layout = new GridBagLayout()
        panel.setLayout(layout)
 
        def add(components : (JLabel, JComponent), row : Int) {
            addToLabelColumn(components._1, row)
            addToComponentColumn(components._2, row)
        }
 
        def add(
            component : JButton,
            row : Int) {
            val c = new GridBagConstraints()
 
            c.weightx = 1
            c.gridx = 1
            c.gridy = row
            c.fill = 0
            c.anchor = GridBagConstraints.SOUTHEAST
            panel.add(component, c)
        }
 
        private def addToLabelColumn(
            component : JComponent,
            row : Int) {
            val c = new GridBagConstraints()
            c.gridx = 0
            c.gridy = row
            c.weightx = 0
            c.fill = 0
            panel.add(component, c)
        }
 
        private def addToComponentColumn(
            component : JComponent,
            row : Int) {
            val c = new GridBagConstraints()
            c.gridx = 1
            c.gridy = row
            c.weightx = 1
            c.fill = 1
            panel.add(component, c)
        }
 
    }
 
    private def create(name : String, property : Property[String]) : (JLabel, JComponent) = {
        val textField = new JTextField()
        Binder.bind(property, textField)
        (new JLabel(name), textField)
    }
 
    private def create(name : String, action : => Unit) = {
        val button = new JButton("save")
        Binder.bind(action, button)
        button
    }
 
    private def createPersonPanel(p : PersonEditor) = {
        val builder = PanelBuilder()
 
        builder.add(create("firstname", p.firstname), 0)
        builder.add(create("lastname", p.lastname), 1)
        builder.add(create("save", p.save), 2)
 
        builder.panel
    }

The PanelBuilder has now two simple public add methods. In order to imitate the method signitures in Java we would have to create a couple of helper classes and interfaces. It would make the code less compact but this shouldn’t be a serious problem. Creation of the various components is just as the binding extracted in two create Methods. The createPersonPanel has now only 5 lines of code. Adding another property to the form should be trivial. Changing the extremely simplistic layout should be trivial and is at least limited to a single small class. I think this is pretty much OK for a first step toward clean swing code. So I leave it like it is right now. Although I do have further plans for this.

I hope most of you agree that the code is much easier to understand and maintain in the form it is right now. But is it really worth the effort? Some might say no. If the whole application would consist of only this little panel. I would agree. But a Swing application typically does not have a single panel with two textfields and a button. But tens or even hundreds of panels. Many consisting of large collections of components. If you have to maintain such a monster, would you prefer createPersonPanel methods like the last one, or would you prefer the first version? What if the customer if finally fed up with your crappy layout and insists on proper spacing between the labels and the JTextFields or other components?

All this is pretty nice when you start a new Swing application. But what if you have an existing Swing application? One with convoluted code just as the Swing Tutorial taught you? Well … start changing it now. Don’t sit down for two months and rewrite all your code, but find pieces of code duplication and extract them. It will be a long way, but you wont reach the end if you don’t start walking.

But what if you are not using Swing, but writing a web application? Well it really shouldn’t matter much. Your PanelBuilder might be written in JavaScript, or create HTML, but the principle is the same: Seperate creation of components, layout of components and binding of components to properties and actions.

 

From http://blog.schauderhaft.de/2011/06/26/clean-code-with-swing-and-scala/