一、观察者模式简介:
首先看百度百科上对观察者模式的简介:观察者模式(Observer)完美的将观察者和被观察的对象分离开。举个例子,用户界面可以作为一个观察者,业务数据是被观察者,用户界面观察业务数据的变化,发现数据变化后,就显示在界面上。面向对象设计的一个原则是:系统中的每个类将重点放在某一个功能上,而不是其他方面。一个对象只做一件事情,并且将他做好。观察者模式在模块之间划定了清晰的界限,提高了应用程序的可维护性和重用性。
观察者模式体现了系统内模块之间存在的1:n的依赖关系,其中 1在整个系统中被称作主题(Subject),n在系统中被称作观察者(Observer)。在整个系统中,这些观察者均需要利用主题的状态来决定自身的状态,当主题的状态发生改变时,这些观察者需要能被通知到并改变自身的状态。同时,当向系统中增加新的观察者时不能对系统中已存在的观察者和主题的代码造成影响,即需满足软件设计模式的开-闭原则,因此需要运用到抽象接口等技术。在观察者模式中存在以下几个角色:
- Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
- ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
- Observer:抽象观察者,是观察者者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
- ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。
那么问题来了,为什么需要这么设计呢?其实在系统中,主题可能不止一个,观察者可以同时观察多个主题,两边形成了多对多的关系。众多主题均有一个不变的特性—通知:当自己的状态发生变化时通知观察自己的观察者状态发生变化;众多观察者均有一个不变的特性—更新:当收到主题的状态改变信号时对应的更新自己的状态。因此需要把这两个不变的部分从中抽取出来即分离变化,因而需要设计Subject抽象主题以及Observer抽象观察者。举个简单的例子,当学校周一进行一周汇报时老师不可能将一周工作和每个同学一一通知到位,不同的老师会有不同的信息,但他们均采用广播通报的方式进行工作汇报,因此广播通报的方式是老师这个主题不变的特性;同时学生接受信息的时候均具有听这一特性,但每个学生的听的方式却各有不同。这就很好的说明了观察者模式在生活中有很多的应用。在数据发生变化的时候只需通过访问接口类的更新方法即可,在添加新的观察者或者主题时只需实现相应的接口即可。
以下为观察者模式的类图:
二、观察者模式在股票分析中的应用
股票的价格经常处于一种瞬息万变状态之中,股民想要从中获利必须要对股票近段时间的数据走向进行分析,很多炒股软件均提供了数据分析的功能,其中根据股票价格的变化提供分时图,K线图均是很常见的功能。从以上的描述中可以得出股票交易是具体主题,分时图、K线图则是具体观察者。当股票的价格发生变化时,分时图和K线图需要能够根据股票价格的变换而相应的调整自身的状态图,因此对于股票价格的数据分析可以通过观察者模式来解决。为了简化问题来模拟股市的变化,我们先做出以下假设:
- 假设每天的开盘时间段为上午9:00至11:00
- 假设每天的开盘价为每天前10分钟的交易总额的平均值,每天的收盘价为每天最后10分钟的交易总额的平均值
- 假设每支股票的模拟的第一天的前一天的收盘价为100元/股,涨跌幅为10%
- 股票价格的模拟采用随机数来模拟,且每一分钟的价格建立在前一分钟价格的基础上(我采用的模拟公式为P(t) = P(t-1) + random(0,1)*1 - 0.5, 这公式不准确但有效果,最好的方式是采用蒙特卡罗模拟算法)
在以上的假设的基础上,我们针对本问题进行设计:
1、设计4个基本角色:
①抽象主题Subject(包含三种抽象方法):
package Subject; import Observer.Observer; public interface Subject { public void addObserver(Observer o); public void deleteObserver(Observer o); public void notifyObservers(); }
②抽象观察者Observer(包含抽象方法update):
package Observer; public interface Observer { public void drawFig(); }
③具体主题StockDealSubject(实现抽象接口Subject模拟股市交易):在观察者模式中观察者具有两种获取数据的方式:推数据和拉数据。其中推数据方式是指具体主题将变化后的数据全部传给具体观察者,即将变化后的数据作为参数进行传递;而拉数据是指具体主题不将变化后的数据交给具体观察者,而是提供获取这些数据的额方法,具体观察者在得到通知后可以调用具体主题的方法得到数据,就相当于观察者自己将数据拉了过来,故称作拉数据。因为我设计的图形化界面是通过用户点击对应的按钮产生对应的分析图,故在这里我采用的是拉数据的方式。
package Subject; import java.util.*; import Observer.Observer; public class StockDealSubject implements Subject{ double[][] prices; String type; ArrayList<Observer> observerList; public StockDealSubject(){ observerList = new ArrayList<Observer>(); prices = null; } @Override public void addObserver(Observer o) { // TODO Auto-generated method stub if(!(observerList.contains(o))){ observerList.add(o); } } @Override public void deleteObserver(Observer o) { // TODO Auto-generated method stub if(observerList.contains(o)){ observerList.remove(o); } } @Override public void notifyObservers() { // TODO Auto-generated method stub for(int i=0;i<observerList.size();i++){ Observer observer = observerList.get(i); observer.drawFig(); } } public void setPriceandType(double[][] prices,String type){ this.prices = prices; this.type = type; } public String getType(){ return this.type; } public double[][] getPrices(){ return this.prices; } }
④具体观察者TimeFigure(分时图)和KlineFigure(K线图):这两个具体观察者均实现了抽象观察者中的drawFig()的更新方法。在具体的drawFig方法中,由于我设计的图形界面通过用户点击对应的按钮产生对应的分析图,因此需要当前的更新操作是否是对自己这个观察者的更新操作。在数据结构的设计方面,我采用的是二维数组prices[m][n],其含义为第m支股票在第n/120+1天的第n%120分钟的收益为prices[m][n]。对于K线图的绘制,我自定义了数据类Klineobject,其中分别包含了K线图的四个基本属性:开盘价、收盘价、当天交易额最大值、当天交易额最小值,并用ArrayList来存储每天的属性。在这些数据及结构的基础上,需要对涨停和跌停这两种情况进行限定,故需要对随机产生的股票价格数据进行处理,使得其在满足条件的范围内,得出新的数据后则需要根据这些数据来进行绘图。
//TimeFigure分时图 package Observer; import java.awt.GridLayout; import javax.swing.JFrame; import org.jfree.chart.ChartPanel; import DrawMethod.DrawLineChart; import Subject.StockDealSubject; import Subject.Subject; public class TimeFigure implements Observer{ Subject subject; String type; public TimeFigure(Subject subject){ this.subject = subject; this.type = "TimeFigure"; subject.addObserver(this); } @Override public void drawFig() { /* * 假设第一天的前一天的收盘价为每股100元,涨跌幅为10%; * 假设共有四支股票,模拟10天内的股票分时图走向; * 假设每天股票交易时间长度为2小时,时间段为9:00~11:00,用1200个数据来模拟每只股票的价格走向; * 假设最后10笔交易的平均值作为当天的收盘价,最初10笔加一的平均值为当天的开盘价; */ if(subject instanceof StockDealSubject){ String type = ((StockDealSubject) subject).getType(); double[][] prices = ((StockDealSubject) subject).getPrices(); int n = prices.length; int size = prices[0].length; if(type.equals(this.type)){ //根据股票的价格变动和涨跌停情况调整每支股票的价格数组; for(int i=0;i<n;i++){ double yesterdayClosePrice=100; double last10TotalPrice=0; double limitUpPrice = yesterdayClosePrice*1.1; double limitDownPrice = yesterdayClosePrice*0.9; for(int j=0;j<size;j++){ if(j%120==0&&j!=0){ //第二天最后10笔交易清空并改动涨跌价和昨天收盘价; yesterdayClosePrice=last10TotalPrice/10.0; limitUpPrice = yesterdayClosePrice*1.1; limitDownPrice = yesterdayClosePrice*0.9; last10TotalPrice=0; } //每分钟进行的交易额需要判断是否涨停或者跌停; if(prices[i][j]<=limitUpPrice&&prices[i][j]>=limitDownPrice){ prices[i][j]=prices[i][j]; } else if(prices[i][j]>limitUpPrice){ prices[i][j]=limitUpPrice; } else{ prices[i][j]=limitDownPrice; } if(j%120>=110&&j%120<=119){ //统计最后10笔交易的总额; last10TotalPrice+=prices[i][j]; } } } //得到新的价格数组后进行绘图; DrawLineChart drawline = new DrawLineChart(prices); ChartPanel frame1 = drawline.getChartPanel(); JFrame frame=new JFrame("Java数据统计图"); frame.add(frame1); frame.setBounds(50, 50, 800, 600); frame.setVisible(true); } } } }
//KlineFigure K线图 package Observer; import Subject.StockDealSubject; import Subject.Subject; import java.awt.font.*; import java.util.ArrayList; import javax.swing.JFrame; import Main.Klineobject; import org.jfree.chart.ChartPanel; import DrawMethod.DrawKlineChart; public class KlineFigure implements Observer{ Subject subject; String type; public KlineFigure(Subject subject){ this.subject = subject; type = "KlineFigure"; subject.addObserver(this); } @Override public void drawFig() { // TODO Auto-generated method stub if(subject instanceof StockDealSubject){ double[][] prices = ((StockDealSubject) subject).getPrices(); int n = prices.length; int size = prices[0].length; String type = ((StockDealSubject) subject).getType(); if(type.equals(this.type)){ //根据股票的价格变动和涨跌停情况调整每支股票的价格数组,并将每天的统计数据加入到List中; ArrayList<Klineobject> objects = new ArrayList<>(); for(int i=0;i<n;i++){ double yesterdayClosePrice=100;//前一天的收盘价; double last10TotalPrice=0;//当天最后成交的10笔交易总额; double first10TotalPrice=0;//当天前10笔成交总额; double limitUpPrice = yesterdayClosePrice*1.1;//涨停价; double limitDownPrice = yesterdayClosePrice*0.9;//跌停价; double startPrice = 0;//开盘价; double maxDealDay = Double.MIN_VALUE;//当天交易额最大 double minDealDay = Double.MAX_VALUE;//当天交易额最小 for(int j=0;j<size;j++){ if(j%120==0&&j!=0){ //到第二天最后10笔交易清空并改动涨跌价和昨天收盘价; yesterdayClosePrice=last10TotalPrice/10.0; limitUpPrice = yesterdayClosePrice*1.1; limitDownPrice = yesterdayClosePrice*0.9; last10TotalPrice=0; first10TotalPrice=0; } //每分钟进行的交易额需要判断是否涨停或者跌停; if(prices[i][j]<=limitUpPrice&&prices[i][j]>=limitDownPrice){ prices[i][j]=prices[i][j]; } else if(prices[i][j]>limitUpPrice){ prices[i][j]=limitUpPrice; } else{ prices[i][j]=limitDownPrice; } //判断最高价和最低价 maxDealDay = Math.max(maxDealDay, prices[i][j]); minDealDay = Math.min(minDealDay, prices[i][j]); //得出开盘价:每天前10笔交易 if(j%120>=0&&j%120<=9){ first10TotalPrice+=prices[i][j]; } //统计最后10笔交易的总额; if(j%120>=110&&j%120<=119){ last10TotalPrice+=prices[i][j]; } //统计当天的数据; if(j%120==119){ Klineobject object = new Klineobject(first10TotalPrice/10, last10TotalPrice/10, maxDealDay, minDealDay); objects.add(object); //最大最小额重置 maxDealDay = Double.MIN_VALUE; minDealDay = Double.MAX_VALUE; } } } DrawKlineChart drawkline = new DrawKlineChart(objects,size/120); ChartPanel frame1 = drawkline.getChartPanel(); JFrame frame=new JFrame("Java数据统计图"); frame.add(frame1); frame.setBounds(50, 50, 800, 600); frame.setVisible(true); } } } }
2、根据得出的股票价格数据绘图:Java绘图我是采用了JFreeChart,它是JAVA平台上的一个开放的图表绘制类库。需要导入三个包:jfreechart.jar、jcommon.jar、gnujaxp.jar,这些包可以去http://mvnrepository.com/上下载,版本可以挑选下载量最多的。然后根据得到的股票数据创建两个绘图类DrawKlineChart和DrawLineChart来分别绘制K线图和分时图,以下为绘图代码:
//绘制K线图 package DrawMethod; import java.awt.Color; import java.awt.Font; import java.text.SimpleDateFormat; import java.util.ArrayList; import Main.Klineobject; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.CandlestickRenderer; import org.jfree.data.time.*; import org.jfree.data.time.ohlc.OHLCSeries; import org.jfree.data.time.ohlc.OHLCSeriesCollection; import org.jfree.data.xy.*; public class DrawKlineChart { ChartPanel frame1; /*public void showObject(ArrayList<Klineobject> object,int dayCount){ for(int i=0;i<object.size();i++){ System.out.println("第"+i/dayCount+"支股票第 "+i%dayCount+" 天: "+" 开盘价: "+object.get(i).getStartPrice()+" 收盘价:"+object.get(i).getClosePrice()+" 最高价:"+object.get(i).getMaxPrice()+" 最低价:"+object.get(i).getMinPrice()); } }*/ public DrawKlineChart(ArrayList<Klineobject> object,int dayCount){ //showObject(object,dayCount); //将数据集进行输入 OHLCDataset xydataset = createDataset(object, dayCount); //用得到的数据集开始画图 CandlestickRenderer candlestickRender=new CandlestickRenderer();// 设置K线图的画图器, candlestickRender.setUseOutlinePaint(true); // 设置是否使用自定义的边框线,程序自带的边框线的颜色不符合中国股票市场的习惯 candlestickRender.setAutoWidthMethod(CandlestickRenderer.WIDTHMETHOD_AVERAGE);// 设置如何对K线图的宽度进行设定 candlestickRender.setAutoWidthGap(0.001);// 设置各个K线图之间的间隔 candlestickRender.setUpPaint(Color.RED);// 设置股票上涨的K线图颜色 candlestickRender.setDownPaint(Color.GREEN);// 设置股票下跌的K线图颜色 DateAxis x1Axis=new DateAxis("时间"); NumberAxis y1Axis=new NumberAxis("交易价格"); XYPlot plot1=new XYPlot(xydataset,x1Axis,y1Axis,candlestickRender); JFreeChart jfreechart = new JFreeChart("股票交易模拟K线图", plot1); XYPlot xyplot = (XYPlot) jfreechart.getPlot(); frame1=new ChartPanel(jfreechart,true); //设置图表的字体格式 DateAxis dateaxis = (DateAxis) xyplot.getDomainAxis(); dateaxis.setLabelFont(new Font("黑体",Font.BOLD,14)); //水平底部标题 dateaxis.setTickLabelFont(new Font("宋体",Font.BOLD,12)); //垂直标题 ValueAxis rangeAxis=xyplot.getRangeAxis();//获取柱状 rangeAxis.setLabelFont(new Font("黑体",Font.BOLD,15)); jfreechart.getLegend().setItemFont(new Font("黑体", Font.BOLD, 15)); jfreechart.getTitle().setFont(new Font("宋体",Font.BOLD,20));//设置标题字体 } private OHLCDataset createDataset(ArrayList<Klineobject> object, int dayCount) { OHLCSeriesCollection seriescollection = new OHLCSeriesCollection (); OHLCSeries series = new OHLCSeries(""); for(int i=0;i<object.size();i++){ if(i%dayCount==0){ int num = i/dayCount+1; String name="第"+num+"支股票"; series = new OHLCSeries(name); } Day day = new Day(i%dayCount+16,11,2017); series.add(day, object.get(i).getStartPrice(), object.get(i).getMaxPrice(), object.get(i).getMinPrice(), object.get(i).getClosePrice());//对应于开、高、低、收 //对同一支股票计算到最后一天然后加入collection中 if(i%dayCount==dayCount-1){ seriescollection.addSeries(series); } } return seriescollection; } public ChartPanel getChartPanel(){ return frame1; } }
//绘制分时图 package DrawMethod; import java.awt.Font; import java.text.SimpleDateFormat; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.plot.XYPlot; import org.jfree.data.time.*; import org.jfree.data.time.TimeSeries; import org.jfree.data.time.TimeSeriesCollection; import org.jfree.data.xy.*; public class DrawLineChart { ChartPanel frame1; public DrawLineChart(double[][] prices){ XYDataset xydataset = createDataset(prices); JFreeChart jfreechart = ChartFactory.createTimeSeriesChart("股票交易模拟分时图", "时间", "交易价格", xydataset,true,true,true); XYPlot xyplot = (XYPlot) jfreechart.getPlot(); frame1=new ChartPanel(jfreechart,true); DateAxis dateaxis = (DateAxis) xyplot.getDomainAxis(); dateaxis.setLabelFont(new Font("黑体",Font.BOLD,14)); //水平底部标题 dateaxis.setTickLabelFont(new Font("宋体",Font.BOLD,12)); //垂直标题 ValueAxis rangeAxis=xyplot.getRangeAxis();//获取柱状 rangeAxis.setLabelFont(new Font("黑体",Font.BOLD,15)); jfreechart.getLegend().setItemFont(new Font("黑体", Font.BOLD, 15)); jfreechart.getTitle().setFont(new Font("宋体",Font.BOLD,20));//设置标题字体 } private XYDataset createDataset(double[][] prices) { TimeSeriesCollection timeseriescollection = new TimeSeriesCollection(); for(int i=0;i<prices.length;i++){ int num = i+1; String name="第"+num+"支股票"; TimeSeries timeseries = new TimeSeries(name); Day day;Hour hour; for(int j=0;j<prices[0].length;j++){ day = new Day(j/120+16,11,2017); if(j%120>=0&&j%120<60){ hour = new Hour(9,day); } else{ hour = new Hour(10,day); } timeseries.add(new Minute(j%60,hour), prices[i][j]);; } timeseriescollection.addSeries(timeseries); } return timeseriescollection; } public ChartPanel getChartPanel(){ return frame1; } }
3、以上已经将整个观察者模式的框架搭建完成,包括数据分析图的绘制,因此可以在这个基础上编写图形用户界面来使用该观察者模式。因此编写Application类来作为程序的入口,用户可以通过点击对应的分时图或K线图按钮来查看相应的分析图:
package Main; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Random; import javax.swing.*; import Observer.KlineFigure; import Observer.TimeFigure; import Subject.StockDealSubject; import Subject.Subject; public class Application extends JFrame implements ActionListener{ double[][] prices; JButton TimeFigureBtn = new JButton("查看分时图"); JButton KlineFigureBtn = new JButton("查看K线图"); public Application(String title){ super(title); //随机产生4支股票并随机产生1200个随机数来模拟10天股票价格变化,每天交易2小时; prices = getPrice(120,2); StockDealSubject stockSubject = new StockDealSubject(); TimeFigure timeFigure = new TimeFigure(stockSubject); KlineFigure klineFigure = new KlineFigure(stockSubject); Container c = getContentPane(); c.setLayout(new FlowLayout(FlowLayout.CENTER)); TimeFigureBtn.setPreferredSize(new Dimension(100,40)); KlineFigureBtn.setPreferredSize(new Dimension(100,40)); c.add(TimeFigureBtn); c.add(KlineFigureBtn); TimeFigureBtn.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent arg0) { stockSubject.setPriceandType(prices, "TimeFigure"); stockSubject.notifyObservers(); } }); KlineFigureBtn.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { stockSubject.setPriceandType(prices, "KlineFigure"); stockSubject.notifyObservers(); } }); setSize(500,150); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public double[][] getPrice(int size,int n){ double[][] price = new double[n][size]; Random ran = new Random(); for(int i=0;i<n;i++){ for(int j=0;j<size;j++){ price[i][j]=j==0?ran.nextDouble()*20-10+100:ran.nextDouble()*1-0.5+price[i][j-1]; } } return price; } public static void main(String[] args) { // TODO Auto-generated method stub Application application = new Application("分时图&K线图"); application.setVisible(true); } @Override public void actionPerformed(ActionEvent e) { // TODO Auto-generated method stub } }
package Main; public class Klineobject { double startPrice; double closePrice; double maxPrice; double minPrice; public double getStartPrice(){ return startPrice; } public double getClosePrice(){ return closePrice; } public double getMaxPrice(){ return maxPrice; } public double getMinPrice(){ return minPrice; } public Klineobject(double startPrice,double closePrice,double maxPrice,double minPrice){ this.startPrice = startPrice; this.closePrice = closePrice; this.maxPrice = maxPrice; this.minPrice = minPrice; } }
三、实验效果截图:
①运行程序,查看4支股票在一天内的股价变化图,则通过getPrice(120,4)来获取最初的股价数据,以下为截图(由于我定义的随机数生成是在Application中的,故需要通过重新运行程序来获得多组结果):
②运行程序,查看2只股票10天内的K线图,则通过getPrice(1200,2)来获取最初的股价数据,以下为截图(由于我定义的随机数生成是在Application中的,故需要通过重新运行程序来获得多组结果):
以上即为本次对观察者模式的学习与应用,感觉蛮有趣的哈哈
转载请注明出处,谢谢~