I am task to do a simple order system. I want the JLabel
(Amount: $0.00) show the corresponding amount on what the user picks for his burger and condiments. For example, if the user click on beef, the label will change into "Amount: $4.00", and when he choose a condiment, it will add $0.50 to the total based on how many condiments he picks and vice versa. Also, when the user unchecks a condiment (JCheckBox
), it will deduct $0.50 dollars to the total.
My code for beef JRadioButton
:
private void beef_radioBtnActionPerformed(java.awt.event.ActionEvent evt) {
total_amount.setText("Amount: $4.00");
ketchup_Checkbox.setEnabled(true);
mustard_Checkbox.setEnabled(true);
pickles_Checkbox.setEnabled(true);
}
Code for ketchup JCheckBox
:
private void ketchup_CheckboxActionPerformed(java.awt.event.ActionEvent evt) {
float condiments_amount = (float) 0.50;
float beef_amount = (float) 4.00;
float total;
if (beef_radioBtn.isSelected()){
total = beef_amount + condiments_amount;
total_amount.setText("Amount: $" + decimal.format(total));
if (!ketchup_Checkbox.isSelected()){
total_amount.setText("Amount: $" + decimal.format(4.50 - condiments_amount));
}
else if (mustard_Checkbox.isSelected()){
total_amount.setText("Amount: $" + decimal.format(4.50 + condiments_amount));
}
else if (pickles_Checkbox.isSelected()){
total_amount.setText("Amount: $" + decimal.format(4.50 + condiments_amount));
}
}
}
Okay, buckle up, this is going to be a bit of ride.
One of the most powerful concepts you have available to you is the concept of "model". A model is just something that "models" something and is a way to seperate different areas of your program. So a model, models the data (think of it like a container) and the view will then use those models to format the data to the user (separation of concerns). A model may also contain business logic or perform calculations depending on its requirements.
The allows you to centralise concepts so you don't end up repeating yourself or forgetting to do things. It's also a way to change how parts of the program work, also known as "delegation"
Well, that's a lot of "blah", so let's get started. I prefer to use interface
s to describe things, it provides a lot of freedom, as you can put different interface
s together to suit different requirements.
Okay, simple concept, this will be a list of items which are available to sell
public interface Menu {
public List<MainMenuItem> getMenuItems();
}
A description of a menu item, pretty basic
public interface MenuItem {
public String getDescription();
public double getPrice();
}
These are all the "top level", "stand alone" menu items and in our case, can have condiments :D
public interface MainMenuItem extends MenuItem {
public List<Condiment> getCondiments();
}
Condiments are a "special" MenuItem
, as they are associated with a MainMenuItem
public interface Condiment extends MenuItem {
}
This is just a demonstration of some of the things you could do, Burger
isn't anything special, but as you will see, we can use this concept to do different things
public interface Burger extends MainMenuItem {
}
And finally, the "order", what have we ordered and what condiments do we want with it
public interface Order {
public MainMenuItem getItem();
public void setItem(MainMenuItem item);
public List<Condiment> getCondiments();
public void addCondiment(Condiment condiment);
public void removeCondiment(Condiment condiment);
public double getTally();
}
This is a good demonstration of the power of the model. The Order
has a getTally
method, which is used to calculate what is owed. Different implementations of the model might apply different calculations, like tax or discounts
Okay, since you're probably aware, we can't create an instance of a interface
, we need some "default" implementations to work with...
public class DefaultOrder implements Order {
private MainMenuItem item;
private List<Condiment> condiments = new ArrayList<>();
@Override
public MainMenuItem getItem() {
return item;
}
@Override
public List<Condiment> getCondiments() {
return Collections.unmodifiableList(condiments);
}
@Override
public double getTally() {
double tally = 0;
if (item != null) {
tally += item.getPrice();
}
for (Condiment condiment : condiments) {
tally += condiment.getPrice();
}
return tally;
}
@Override
public void setItem(MainMenuItem item) {
this.item = item;
// Oh look, we've established a "rule" that this model
// applies, by itself, sweet
condiments.clear();
}
@Override
public void addCondiment(Condiment condiment) {
// Bit pointless if the menu item is not set
if (item == null) {
return;
}
// Probably should check for duplicates
condiments.add(condiment);
}
@Override
public void removeCondiment(Condiment condiment) {
// Bit pointless if the menu item is not set
if (item == null) {
return;
}
condiments.remove(condiment);
}
}
public class DefaultMenu implements Menu {
private List<MainMenuItem> menuItems = new ArrayList<>();
public void add(MainMenuItem menuItem) {
menuItems.add(menuItem);
}
@Override
public List<MainMenuItem> getMenuItems() {
return Collections.unmodifiableList(menuItems);
}
}
public abstract class AbstractMenuItem implements MenuItem {
private String description;
private double price;
public AbstractMenuItem(String description, double price) {
this.description = description;
this.price = price;
}
@Override
public String getDescription() {
return description;
}
@Override
public double getPrice() {
return price;
}
}
public class DefaultCondiment extends AbstractMenuItem implements Condiment {
public DefaultCondiment(String description, double price) {
super(description, price);
}
}
public class DefaultBurger extends AbstractMenuItem implements Burger {
private List<Condiment> condiments;
public DefaultBurger(String description, double price, List<Condiment> condiments) {
super(description, price);
// Protect ourselves from external modifications
this.condiments = new ArrayList<>(condiments);
}
@Override
public List<Condiment> getCondiments() {
return Collections.unmodifiableList(condiments);
}
}
Okay, try not to get too caught up in this, but have a look at the use of abstract
here. AbstractMenuItem
encapsulates a lot of the "common" functionality that all MenuItem
implementations are going to need, so we don't need to repeat ourselves, sweet.
Some of these implementations are already making decisions or applying rules. For example, the DefaultOrder
will clear the condiments
when ever the MainMenuItem
is changed. It could also make sure that the condiment which is been applied is actually available fo this item.
Also note, the tally
method is not a stored property, but is re-calculated every time you call it. This is design decision, it wouldn't be hard to make it a stored property instead, so each time you change the MenuMenuItem
, add and/or remove condiments, the property was updated, but I'm feeling lazy. But you can see how these things can be changed, and it will effect ALL users of these models, sweet :D
Okay, but how does this actually answer the question? Well, quite a bit actually.
So, the idea is, you start with a blank Order
. The user selects a "main item" (ie a burger), you set this to the Order
and then you update the UI in response. The UI asks the Order
to calculate the tally
and presents that to the user.
More over, the same concept works for condiments as well. Each time a condiment is added or removed by the user, the Order
is updated and you update the UI.
Okay, but maybe, it's a little easier to understand with an example...
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.border.EmptyBorder;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame();
List<Condiment> condiments = new ArrayList<>(3);
condiments.add(new DefaultCondiment("Ketchup", 0.5));
condiments.add(new DefaultCondiment("Mustard", 0.5));
condiments.add(new DefaultCondiment("Pickles", 0.5));
DefaultMenu menu = new DefaultMenu();
menu.add(new DefaultBurger("Beef", 4.0, condiments));
menu.add(new DefaultBurger("Chicken", 3.5, condiments));
menu.add(new DefaultBurger("Veggie", 4.0, condiments));
MenuPane menuPane = new MenuPane();
menuPane.setMenu(menu);
frame.add(menuPane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class MenuPane extends JPanel {
private Menu menu;
private Order order;
private List<Condiment> selectedCondiments = new ArrayList<>();
private JPanel burgerPanel;
private JPanel condimentPanel;
private JPanel totalPanel;
private JLabel totalLabel;
private JButton clearButton;
private JButton payButton;
private NumberFormat currencyFormatter;
public MenuPane() {
setLayout(new GridBagLayout());
order = new DefaultOrder();
burgerPanel = new JPanel();
burgerPanel.setBorder(new EmptyBorder(8, 8, 8, 8));
condimentPanel = new JPanel();
condimentPanel.setBorder(new EmptyBorder(8, 8, 8, 8));
totalPanel = makeTotalPanel();
totalPanel.setBorder(new EmptyBorder(8, 8, 8, 8));
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1;
gbc.weighty = 0.5;
add(burgerPanel, gbc);
gbc.gridy++;
add(condimentPanel, gbc);
gbc.gridy++;
gbc.weighty = 0;
gbc.fill = GridBagConstraints.HORIZONTAL;
add(totalPanel, gbc);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(400, 200);
}
protected NumberFormat getCurrentFormatter() {
if (currencyFormatter != null) {
return currencyFormatter;
}
currencyFormatter = NumberFormat.getCurrencyInstance();
currencyFormatter.setMinimumFractionDigits(2);
return currencyFormatter;
}
protected JPanel makeTotalPanel() {
JPanel totalPanel = new JPanel(new GridBagLayout());
totalLabel = new JLabel();
clearButton = new JButton("CLR");
payButton = new JButton("PAY");
clearButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
order = new DefaultOrder();
buildCondiments();
orderDidChange();
}
});
GridBagConstraints gbc = new GridBagConstraints();
gbc.weightx = 1;
gbc.gridx = 0;
gbc.gridy = 0;
totalPanel.add(totalLabel, gbc);
gbc.weightx = 0;
gbc.gridx++;
totalPanel.add(clearButton, gbc);
gbc.gridx++;
totalPanel.add(payButton, gbc);
return totalPanel;
}
protected void buildBurgerMenu() {
burgerPanel.removeAll();
burgerPanel.setLayout(new GridBagLayout());
if (menu == null) {
return;
}
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.anchor = GridBagConstraints.NORTHWEST;
gbc.weightx = 1;
ButtonGroup bg = new ButtonGroup();
// Stick with me, this is a little more advanced, but provides
// a really nice concept and ease of use
// We could also make use of the Action API, but that might
// pushing you just a little to far ;)
ActionListener actionListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (!(e.getSource() instanceof JComponent)) {
return;
}
JComponent comp = (JComponent) e.getSource();
Object obj = comp.getClientProperty("MenuItem");
// I'm putting this here to demonstrate part of the concept
// of polymorphism - techncially, we don't have to care
// of it's a burger or some other type of menu item,
// only that this is going to represent the "main" item
if (!(obj instanceof MainMenuItem)) {
return;
}
MainMenuItem item = (MainMenuItem) obj;
order.setItem(item);
buildCondiments();
orderDidChange();
}
};
System.out.println(menu.getMenuItems().size());
for (MenuItem item : menu.getMenuItems()) {
// Only interested in burgers
// Could have the Menu do this, but that's a design
// decision
if (!(item instanceof Burger)) {
continue;
}
Burger burger = (Burger) item;
JRadioButton btn = new JRadioButton(burger.getDescription() + " (" + getCurrentFormatter().format(burger.getPrice()) + ")");
// Ok, this is just a little cheeky, but we're associating the
// butger with the button for simplicity
btn.putClientProperty("MenuItem", burger);
bg.add(btn);
// Add all the buttons share the same listener, because of polymorphism :D
btn.addActionListener(actionListener);
burgerPanel.add(btn, gbc);
}
}
protected void buildCondiments() {
condimentPanel.removeAll();
condimentPanel.setLayout(new GridBagLayout());
if (menu == null || order.getItem() == null) {
return;
}
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.anchor = GridBagConstraints.NORTHWEST;
gbc.weightx = 1;
// Stick with me, this is a little more advanced, but provides
// a really nice concept and ease of use
// We could also make use of the Action API, but that might
// pushing you just a little to far ;)
ActionListener actionListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (!(e.getSource() instanceof JCheckBox)) {
return;
}
JCheckBox checkBox = (JCheckBox) e.getSource();
Object obj = checkBox.getClientProperty("Condiment");
if (!(obj instanceof Condiment)) {
return;
}
Condiment condiment = (Condiment) obj;
if (checkBox.isSelected()) {
order.addCondiment(condiment);
} else {
order.removeCondiment(condiment);
}
orderDidChange();
}
};
for (Condiment condiment : order.getItem().getCondiments()) {
JCheckBox btn = new JCheckBox(condiment.getDescription() + " (" + getCurrentFormatter().format(condiment.getPrice()) + ")");
// Ok, this is just a little cheeky, but we're associating the
// butger with the button for simplicity
btn.putClientProperty("Condiment", condiment);
// Add all the buttons share the same listener, because of polymorphism :D
btn.addActionListener(actionListener);
condimentPanel.add(btn, gbc);
}
}
public Menu getMenu() {
return menu;
}
public void setMenu(Menu menu) {
this.menu = menu;
order = new DefaultOrder();
buildBurgerMenu();
orderDidChange();
}
protected void orderDidChange() {
if (order == null) {
totalLabel.setText("Amount: " + getCurrentFormatter().format(0));
return;
}
// And now, some magic, how easy is it to get the expected
// tally amount!!
totalLabel.setText("Amount: " + getCurrentFormatter().format(order.getTally()));
}
}
public interface Menu {
public List<MainMenuItem> getMenuItems();
}
public interface MenuItem {
public String getDescription();
public double getPrice();
}
public interface Condiment extends MenuItem {
}
public interface MainMenuItem extends MenuItem {
public List<Condiment> getCondiments();
}
public interface Burger extends MainMenuItem {
}
public interface Order {
public MainMenuItem getItem();
public void setItem(MainMenuItem item);
public List<Condiment> getCondiments();
public void addCondiment(Condiment condiment);
public void removeCondiment(Condiment condiment);
public double getTally();
}
public class DefaultOrder implements Order {
private MainMenuItem item;
private List<Condiment> condiments = new ArrayList<>();
@Override
public MainMenuItem getItem() {
return item;
}
@Override
public List<Condiment> getCondiments() {
return Collections.unmodifiableList(condiments);
}
@Override
public double getTally() {
double tally = 0;
if (item != null) {
tally += item.getPrice();
}
for (Condiment condiment : condiments) {
tally += condiment.getPrice();
}
return tally;
}
@Override
public void setItem(MainMenuItem item) {
this.item = item;
// Oh look, we've established a "rule" that this model
// applies, by itself, sweet
condiments.clear();
}
@Override
public void addCondiment(Condiment condiment) {
// Bit pointless if the menu item is not set
if (item == null) {
return;
}
// Probably should check for duplicates
condiments.add(condiment);
}
@Override
public void removeCondiment(Condiment condiment) {
// Bit pointless if the menu item is not set
if (item == null) {
return;
}
condiments.remove(condiment);
}
}
public class DefaultMenu implements Menu {
private List<MainMenuItem> menuItems = new ArrayList<>();
public void add(MainMenuItem menuItem) {
menuItems.add(menuItem);
}
@Override
public List<MainMenuItem> getMenuItems() {
return Collections.unmodifiableList(menuItems);
}
}
public abstract class AbstractMenuItem implements MenuItem {
private String description;
private double price;
public AbstractMenuItem(String description, double price) {
this.description = description;
this.price = price;
}
@Override
public String getDescription() {
return description;
}
@Override
public double getPrice() {
return price;
}
}
public class DefaultCondiment extends AbstractMenuItem implements Condiment {
public DefaultCondiment(String description, double price) {
super(description, price);
}
}
public class DefaultBurger extends AbstractMenuItem implements Burger {
private List<Condiment> condiments;
public DefaultBurger(String description, double price, List<Condiment> condiments) {
super(description, price);
// Protect ourselves from external modifications
this.condiments = new ArrayList<>(condiments);
}
@Override
public List<Condiment> getCondiments() {
return Collections.unmodifiableList(condiments);
}
}
}
Okay, that's a lot to take in. Lets take a closer look at the buildBurgerMenu
method. This gets called when ever the main menu is changed.
Pay close attention to the actionListener
used in this method, there's only one and it's shared by all the buttons
protected void buildBurgerMenu() {
burgerPanel.removeAll();
burgerPanel.setLayout(new GridBagLayout());
if (menu == null) {
return;
}
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.anchor = GridBagConstraints.NORTHWEST;
gbc.weightx = 1;
ButtonGroup bg = new ButtonGroup();
// Stick with me, this is a little more advanced, but provides
// a really nice concept and ease of use
// We could also make use of the Action API, but that might
// pushing you just a little to far ;)
ActionListener actionListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (!(e.getSource() instanceof JComponent)) {
return;
}
JComponent comp = (JComponent) e.getSource();
Object obj = comp.getClientProperty("MenuItem");
// I'm putting this here to demonstrate part of the concept
// of polymorphism - techncially, we don't have to care
// of it's a burger or some other type of menu item,
// only that this is going to represent the "main" item
if (!(obj instanceof MainMenuItem)) {
return;
}
MainMenuItem item = (MainMenuItem) obj;
order.setItem(item);
buildCondiments();
orderDidChange();
}
};
System.out.println(menu.getMenuItems().size());
for (MenuItem item : menu.getMenuItems()) {
// Only interested in burgers
// Could have the Menu do this, but that's a design
// decision
if (!(item instanceof Burger)) {
continue;
}
Burger burger = (Burger) item;
JRadioButton btn = new JRadioButton(burger.getDescription() + " (" + getCurrentFormatter().format(burger.getPrice()) + ")");
// Ok, this is just a little cheeky, but we're associating the
// butger with the button for simplicity
btn.putClientProperty("MenuItem", burger);
bg.add(btn);
// Add all the buttons share the same listener, because of polymorphism :D
btn.addActionListener(actionListener);
burgerPanel.add(btn, gbc);
}
}
When ever the actionListener
is triggered (via a user interaction for example), it makes a bunch of decisions, which, if all goes well, ends in the Order
been updated, the buildCondiments
and orderDidChange
methods been called, which updates the available condiments and updates the UI's tally.
This is all done in a "abstract" way. The actionListener
doesn't care about what type of MainMenuItem
the user selected, that doesn't change its workflow, it only needs to apply the item to the Order
. In the same vain, the Order
doesn't care, as it just needs the price
information in order to calculate the tally.
So you can add new menu items and/or change the prices and everything just keeps on working (🤞).
Let's look at the orderDidChange
method...
protected void orderDidChange() {
if (order == null) {
totalLabel.setText("Amount: " + getCurrentFormatter().format(0));
return;
}
// And now, some magic, how easy is it to get the expected
// tally amount!!
totalLabel.setText("Amount: " + getCurrentFormatter().format(order.getTally()));
}
Not super complicated is it! All the work is been done by the, MODEL!
For brevity, I left out one other concept, often used with models, the concept of the "observer pattern".
The observer pattern allows an interested party to be notified when some other object changes. It's pretty common concept and you've already used, ActionListener
is an example of an observer pattern. It allows you to "observer" "actions events" when they are triggered by a given object.
Sure, but how is that helpful?
Well, imagine now if, instead of having to manually call orderDidChange
every time you wanted to update the UI (or even forgetting to and spending a few hours debugging why), the MenuPane
could, instead, registered itself as an observer directly to the Order
and be notified when the order changed!! Super sweet!
This further helps you de-couple your code and makes it super easy to update the UI in a verity of ways independently of the model or other code requirements.
Models 💪