问题描述
我的任务是做一个简单的订单系统。我希望 JLabel
(Amount: $0.00) 显示用户为他的汉堡和调味品选择的相应金额。例如,如果用户点击牛肉,标签将变为“金额:4.00 美元”,当他选择一种调味品时,它会根据他选择的调味品数量在总数上增加 0.50 美元,反之亦然。此外,当用户取消选中调味品 (JCheckBox
) 时,将从总额中扣除 0.50 美元。
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);
}
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));
}
}
}
解决方法
好的,系好安全带,这会有点麻烦。
您可以使用的最强大的概念之一是“模型”的概念。模型只是对某些东西进行“建模”的东西,是一种分离程序不同区域的方法。因此,模型对数据进行建模(将其视为容器),然后视图将使用这些模型将数据格式化给用户(关注点分离)。模型还可能包含业务逻辑或根据其要求执行计算。
这使您可以集中概念,这样您就不会最终重复自己或忘记做事。这也是改变程序部分工作方式的一种方式,也称为“委托”
起点,一些接口
好吧,那是很多“废话”,让我们开始吧。我更喜欢用 interface
来描述事物,它提供了很大的自由度,因为您可以将不同的 interface
放在一起以满足不同的需求。
菜单
好的,简单的概念,这将是一个可供出售的物品列表
public interface Menu {
public List<MainMenuItem> getMenuItems();
}
菜单项
菜单项的描述,非常基本
public interface MenuItem {
public String getDescription();
public double getPrice();
}
“主”菜单项
这些都是“顶级”、“独立”菜单项,在我们的例子中,可以有调味品:D
public interface MainMenuItem extends MenuItem {
public List<Condiment> getCondiments();
}
调味品
调味品是“特殊的”MenuItem
,因为它们与 MainMenuItem
相关联
public interface Condiment extends MenuItem {
}
汉堡
这只是您可以做的一些事情的演示,Burger
没有什么特别的,但是正如您将看到的,我们可以使用这个概念来做不同的事情
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();
}
这很好地展示了模型的威力。 Order
有一个 getTally
方法,用于计算欠款。模型的不同实现可能会应用不同的计算,例如税收或折扣
实施
好的,因为您可能知道,我们无法创建 interface
的实例,我们需要一些“默认”实现来使用...
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);
}
}
好的,尽量不要太纠结于此,但请在此处查看 abstract
的用法。 AbstractMenuItem
封装了所有 MenuItem
实现都需要的许多“通用”功能,所以我们不需要重复自己,亲爱的。
其中一些实现已经在制定决策或应用规则。例如,当 DefaultOrder
更改时,condiments
将清除 MainMenuItem
。还可以确保所涂抹的调味品确实可以用于该商品。
另请注意,tally
方法不是存储属性,而是每次调用时都会重新计算。这是设计决定,将其改为存储属性并不难,因此每次更改 MenuMenuItem
、添加和/或删除调味品时,属性都会更新,但我感觉很懒惰。但是你可以看到这些东西是如何改变的,它会影响这些模型的所有用户,亲爱的 :D
好的,但这实际上如何回答问题?嗯,实际上相当多。
所以,这个想法是,你从一个空白的 Order
开始。用户选择一个“主要项目”(即汉堡),您将其设置为 Order
,然后您更新 UI 作为响应。 UI 要求 Order
计算 tally
并将其呈现给用户。
此外,同样的概念也适用于调味品。每次用户添加或删除调味品时,Order
都会更新,您也会更新 UI。
好的,但也许,用一个例子更容易理解...
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",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));
condimentPanel = new JPanel();
condimentPanel.setBorder(new EmptyBorder(8,8));
totalPanel = makeTotalPanel();
totalPanel.setBorder(new EmptyBorder(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,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,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,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,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);
}
}
}
好的,有很多内容需要了解。让我们仔细看看 buildBurgerMenu
方法。每当主菜单发生更改时都会调用此方法。
密切注意这个方法中使用的actionListener
,只有一个,而且所有按钮都共享
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,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,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);
}
}
每当 actionListener
被触发时(例如通过用户交互),它会做出一系列决定,如果一切顺利,以更新 Order
结束,{{ 1}} 和 buildCondiments
方法被调用,这会更新可用的调味品并更新 UI 的计数。
这一切都是以“抽象”的方式完成的。 orderDidChange
不关心用户选择了哪种类型的 actionListener
,这不会改变其工作流程,它只需要将项目应用于 MainMenuItem
。同样徒劳的是,Order
并不关心,因为它只需要 Order
信息来计算计数。
所以你可以添加新的菜单项和/或更改价格,一切都会继续工作(?)。
让我们看看 price
方法...
orderDidChange
不是很复杂吧!所有的工作都是由 MODEL 完成的!
我遗漏了什么
为简洁起见,我省略了另一个经常与模型一起使用的概念,即“观察者模式”的概念。
观察者模式允许在其他对象发生变化时通知感兴趣的一方。这是一个非常常见的概念,您已经使用过,protected void orderDidChange() {
if (order == null) {
totalLabel.setText("Amount: " + getCurrentFormatter().format(0));
return;
}
// And now,how easy is it to get the expected
// tally amount!!
totalLabel.setText("Amount: " + getCurrentFormatter().format(order.getTally()));
}
是观察者模式的一个示例。它允许您在给定对象触发时“观察”“动作事件”。
当然可以,但这有什么用?
好吧,现在想象一下,如果不是每次想要更新 UI 时都必须手动调用 ActionListener
(甚至忘记并花几个小时调试原因),orderDidChange
可以,相反,直接将自己注册为 MenuPane
的观察者,并在订单更改时收到通知!!超甜!
这进一步帮助您解耦您的代码,并使以一种独立于模型或其他代码要求的方式更新 UI 变得非常容易。
模型?