一、觀察者模式簡介:
首先看百度百科上對觀察者模式的簡介:觀察者模式(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中的,故需要通過重新運行程序來獲得多組結果):
以上即為本次對觀察者模式的學習與應用,感覺蠻有趣的哈哈
轉載請注明出處,謝謝~