Re: Speaking of thread safety?
I don't know if you had time to read my earlier reply, but after I
posted it I starting thinking about the little design I posted. I'm
thinking that what I said--that it is probably good design to allow a
thread safe method of the observer--is just wrong.
There's two competing principles in design: 1. usefulness or ease of
use, and 2. pithiness. The opposite of pithiness is gold plating, a
known anti-pattern.
If you over emphasize 1, then you risk a baroque, ornamented design that
takes 20 method calls to accomplish what should take just one method call.
If you over emphasize 2, then you risk a stark design that only works
for your particular need, but can't be extended and is difficult to
maintain due to it being too embedded in the particular code you wrote.
Always there's a balance to maintain.
Anyway, what I'm trying to say here is that because you have a JPanel to
begin with, you have events on the EDT anyway, so why not just skip the
thread safety and say all methods have to be called on the EDT?
For example:
Knute Johnson wrote:
Panel1 p1 = new Panel1();
Panel2 p2 = new Panel2();
p1.addObserver(p2);
This is EXACTLY the example I was going to provide, implemented on the
EDT just as you have. And it doesn't need to be specially thread safe,
it's naturally called on the EDT anyway.
Yes, making the classes not thread safe risks making the class hard to
use. But most Swing classes aren't thread safe, and folks are used to
dealing with that. So there's a well developed body of knowledge for
working with Swing classes that aren't thread safe, and I don't think
you're risking much by requiring the user be responsible for thread safety.
By contrast, especially after looking at your code, I think you are
risking a baroque, gold-plated, hard-to-maintain design by trying to
make the class totally thread safe. Events fire on the EDT because it's
fast to do so; trying to spawn a separate thread just to re-issue an
event to update your 2nd panel on the EDT again seems like mis-design.
(What one of my professors used to call "going around your elbow to get
from your forefinger to your thumb.")
Anyway, here's my design. I nixed your actual panel because it was just
easier to use a JTextArea for the example, but the whole thing fires on
the EDT, so it doesn't matter. It's all thread safe, and I don't have
to make any threads, and therefore things should happen quickly and the
UI should stay fast too. The code is shorter, and runs faster. Win-win.
(Note the JSpinner doesn't actually say that a ChangeListener is
guaranteed to be called on the EDT. I assume that it is. Therefore any
observer added can assume that it's update() method is also called on
the EDT, because of my design.)
Here's the main class, you can see it's pretty short and too the point:
package panelstest;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class Main {
public static void main(String[] args) {
SwingUtilities.invokeLater( new Runnable() {
public void run()
{
createAndShowGui();
}
} );
}
private static void createAndShowGui() {
JPanel panel = new JPanel();
BoxLayout layout = new BoxLayout( panel, BoxLayout.PAGE_AXIS );
panel.setLayout( layout );
SpinnerPanel spinPan = new SpinnerPanel();
VariableRectanglePanel vrp = new VariableRectanglePanel();
spinPan.addOberver( vrp );
panel.add( spinPan );
panel.add( vrp );
JFrame frame = new JFrame("Oberver Test" );
frame.add( panel );
frame.setLocationRelativeTo( null );
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
frame.pack();
frame.setVisible( true );
}
}
The rest of the code is kinda baroque because I was lazy and used a GUI
editor to generate the panels, so they're nicely laid out. Phear the
code generator, I guess. If you suck these classes into an IDE and use
code-folding to hide the methods that have the auto-generated comment,
you'll see that they're very simple in concept. The code I had to add
is pretty easy.
First the easy bit, my observer interface:
package panelstest;
public interface MyObserver {
void update( Object message );
}
Now for the hold-your-nose bits:
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
/*
* SpinnerPanel.java
*
* Created on Mar 11, 2010, 1:18:21 PM
*/
package panelstest;
import java.util.ArrayList;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/**
*
* @author Brenden
*/
public class SpinnerPanel extends javax.swing.JPanel {
private ArrayList<MyObserver> observers = new ArrayList<MyObserver>();
/** Creates new form SpinnerPanel */
public SpinnerPanel() {
initComponents();
jSpinner1.addChangeListener( new ChangeListener() {
public void stateChanged( ChangeEvent e )
{
System.err.println( "EDT:
"+SwingUtilities.isEventDispatchThread() );
notifyObservers( jSpinner1.getValue() );
}
} );
}
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {
jLabel1 = new javax.swing.JLabel();
jSpinner1 = new javax.swing.JSpinner();
setBorder(javax.swing.BorderFactory.createTitledBorder("Spinner
-- Observable"));
jLabel1.setText("Spin me! ");
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(jLabel1)
..addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jSpinner1,
javax.swing.GroupLayout.PREFERRED_SIZE,
javax.swing.GroupLayout.DEFAULT_SIZE,
javax.swing.GroupLayout.PREFERRED_SIZE)
.addContainerGap(59, Short.MAX_VALUE))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
..addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(jLabel1)
.addComponent(jSpinner1,
javax.swing.GroupLayout.PREFERRED_SIZE,
javax.swing.GroupLayout.DEFAULT_SIZE,
javax.swing.GroupLayout.PREFERRED_SIZE))
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE,
Short.MAX_VALUE))
);
}// </editor-fold>
// Variables declaration - do not modify
private javax.swing.JLabel jLabel1;
private javax.swing.JSpinner jSpinner1;
// End of variables declaration
/** NOT THREAD SAFE! Call only on the EDT */
public void addOberver( MyObserver observer ) {
observers.add( observer );
}
private void notifyObservers( Object message ) {
for( MyObserver o : observers ) {
o.update( message );
}
}
}
------------And-------------
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
/*
* VariableRectanglePanel.java
*
* Created on Mar 11, 2010, 1:23:27 PM
*/
package panelstest;
/**
*
* @author Brenden
*/
public class VariableRectanglePanel extends javax.swing.JPanel
implements MyObserver {
/** Creates new form VariableRectanglePanel */
public VariableRectanglePanel() {
initComponents();
}
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {
jLabel1 = new javax.swing.JLabel();
jScrollPane1 = new javax.swing.JScrollPane();
jTextArea1 = new javax.swing.JTextArea();
setBorder(javax.swing.BorderFactory.createTitledBorder("Variable
Rectangle -- Observer"));
jLabel1.setText("Variable Rectangle:");
jTextArea1.setColumns(20);
jTextArea1.setRows(5);
jScrollPane1.setViewportView(jTextArea1);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(jLabel1)
..addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
.addComponent(jScrollPane1,
javax.swing.GroupLayout.DEFAULT_SIZE, 245, Short.MAX_VALUE)
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
..addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(jScrollPane1,
javax.swing.GroupLayout.DEFAULT_SIZE, 154, Short.MAX_VALUE)
.addComponent(jLabel1))
.addContainerGap())
);
}// </editor-fold>
// Variables declaration - do not modify
private javax.swing.JLabel jLabel1;
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JTextArea jTextArea1;
// End of variables declaration
public void update( Object message )
{
jTextArea1.append( "Update: "+message.toString()+"\n" );
}
}