黑馬程序員_Java_交通燈管理

交通燈管理項目模擬了對十字路口交通燈的控制,一般在我們生活中的十字路口是有人行道的,而此項目沒有考慮人行道,到下面需求的第3條,右轉車輛不受信號燈控制可以看出。
具體的需求如下:
1、異步隨機生成按照各個路線行駛的車輛。
    例如:
    由南向而來去往北向的車輛 ---- 直行車輛
    由西向而來去往南向的車輛 ---- 右轉車輛
    由東向而來去往南向的車輛 ---- 左轉車輛
    
2、信號燈忽略黃燈,只考慮紅燈和綠燈 
3、應考慮左轉車輛控制信號燈,右轉車輛不受信號燈控制。
4、具體信號燈控制邏輯與現實生活中普通交通燈控制邏輯相同,不考慮特殊情況下的控制邏輯。
    注:南北向車輛與東西向車輛交替放行,同方向等待車輛應先放行直行車輛而後放行左轉車輛。
    每輛車通過路口時間爲1秒(提示:可通過線程Sleep的方式模擬)。隨機生成車輛時間間隔以及紅綠燈交換時間間隔自定,可以設置。
5、每輛車通過路口的時間爲1秒
6、隨機生成車輛的時間間隔,以及紅綠燈交換時間間隔自定,可以設置。
 
項目編碼之前首先要理清項目的需求,在腦海中大致有一個項目的雛形,下面這個圖描述了項目中十字路口的情況,通過這個圖可以有助於,對項目業務的理解,觀察上圖可以看出,總共有12條路線,爲了統一編程模型,可以假設每條路線都有一個紅綠燈對其進行控制,右轉彎的4條路線的控制燈可以假設稱爲常綠狀態,另外,其他的8條線路是兩兩成對的,可以歸爲4組,所以,程序只需考慮圖中標註了數字號的4條路線的控制燈的切換順序,這4條路線相反方向的路線的控制燈跟隨這4條路線切換,不必額外考慮。
分析需求以及上面這個圖,可以進行這樣的設計:

一、每條路線上都會出現多輛車,路線上要隨機增加新的車,在燈綠期間還要每秒鐘減少一輛車,項目中車沒有其他的邏輯,可以不把它提煉爲一個單獨的類,用字符串表示就可以了
1、 設計一個Road類來表示路線,每個Road對象代表一條路線,總共有12條路線,即系統中總共要產生12個Road實例對象。
2、 每條路線上隨機增加新的車輛,增加到一個集合中保存
3、 每條路線每隔一秒都會檢查控制本路線的燈是否爲綠,是則將本路線保存車的集合中的第一輛車移除,即表示車穿過了路口

二、每條路線每隔一秒都會檢查控制本路線的燈是否爲綠,一個燈由綠變紅時,應該將下一個方向的燈變綠
1、 每個交通燈要有變亮和變黑的方法,並且能返回自己的亮黑狀態。設計一個Lamp類來表示一個交通燈,每個交通燈都維護一個狀態:亮(綠)或不亮(紅)
2、 總共有12條路線,所以,系統中總共要產生12個交通燈。右拐彎的路線本來不受燈的控制,但是爲了讓程序採用統一的處理方式,故假設出有四個右拐彎的燈,只是這些燈爲常亮狀態,即永遠不變黑
3、 除了右拐彎方向的其他8條路線的燈,它們是兩兩成對的,可以歸爲4組,所以,在編程處理時,只要從這4組中各取出一個燈,對這4個燈依次輪詢變亮,與這4個燈方向對應的燈則隨之一同變化,因此Lamp類中要有一個變量來記住自己相反方向的燈,在一個Lamp對象的變亮和變黑方法中,將對應方向的燈也變亮和變黑。每個燈變黑時,都伴隨者下一個燈的變亮,Lamp類中還用一個變量來記住自己的下一個燈
4、 無論在程序的什麼地方去獲得某個方向的燈時,每次獲得的都是同一個實例對象,所以Lamp類改用枚舉來做顯然具有很大的方便性,永遠都只有代表12個方向的燈的實例對象
5、 設計一個燈的控制器LampController類,它定時讓當前的綠燈變紅
在面向對象設計中有一步很關鍵,就是提煉出項目中有哪些類,這個需要根據需求與具體的設計,提煉時,可以看需求文檔中出現的名詞。我們初步設想一下有哪些對象:紅綠燈,紅綠燈的控制系統,汽車,路線。汽車看到自己所在路線對應的燈綠了就穿過路口嗎?不是,還需要看其前面是否有車,看前面是否有車,該問哪個對象呢?該問路,路中存儲着車輛的集合,顯然路上就應該有增加車輛和減少車輛的方法了。再看題目,我們這裏並不要體現車輛移動的過程,只是捕捉出車輛穿過路口的過程,也就是捕捉路上減少一輛車的過程,所以,這個車並不需要單獨設計成爲一個對象,用一個字符串表示就可以了。面向對象設計把握一個重要的經驗:誰擁有數據,誰就對外提供操作這些數據的方法,這個被稱爲是專家模式,有一本關於重構的著名書籍<<重構,改善既有代碼設計>>一書中,就有關於這點的介紹。

此項目設計了4各類,分別是Road、Lamp、LampController、MainClass,下面就來看看這些類是如何設計的:
Road類
1、 每個Road對象都有一個name成員變量來代表方向,有一個vehicles成員變量來代表方向上的車輛集合。
2、 在Road對象的構造方法中啓動一個線程每隔一個隨機的時間向vehicles集合中增加一輛車(用一個“路線名_id”形式的字符串進行表示)。
3、 在Road對象的構造方法中啓動一個定時器,每隔一秒檢查該方向上的燈是否爲綠,是則打印車輛集合和將集合中的第一輛車移除掉。
這個類中需要啓動一個線程以及定時器,啓動線程推薦使用Java5提供的線程庫,性能較好些,提供了線程池,線程池是一上來就搞出來許多線程,放在那,當有任務交給他時,從池中找出一個空閒的去執行這個任務,Executors是一個線程工具類,提供了大量的對線程進行支持的靜態方法。
Road類代碼如下
package traffic3;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;


/**
 *每個Road對象代表一條路線,總共有12條路線,即系統中總共要產生12個Road實例對象。
 * 每條路線上隨機增加新的車輛,增加到一個集合中保存。
 * 每條路線每隔一秒都會檢查控制本路線的燈是否爲綠,是則將本路線保存車的集合中的第一輛車移除,即表示車穿過了路口。
 * @author AdanacLFZ
 * 
 */
public class Road {

// 車並不需要定義一個實體類,將車以字符串的形式定義到數組中即可
private String name;

// 通過分析車的特點,車有增加和減少的方法,所以鏈表結構是符合車的存儲特點的
// 爲什麼不寫ArrayList集合呢 因爲面向接口編程
/*接口 List<E>
所有超級接口: 
Collection<E>, Iterable<E> 
所有已知實現類(部分): 
AbstractList, ArrayList, AttributeList, LinkedList, Vector*/ 


private List<String> vehicles = new ArrayList<String>();

//在構造方法中不斷的創建車。
public Road(String name){
this.name = name;

//線程庫,使用線程庫來創建一個線程,相當於一個線程池
ExecutorService pool = Executors.newSingleThreadExecutor();
//相當於new一個Runnable的實現類對象
pool.execute(new Runnable() {

@Override
public void run() {
for (int i = 1; i < 1000; i++) {
try {
Thread.sleep((new Random().nextInt(10) + 1) * 1000);//sleep是隨機數,隨機值1~10
} catch (InterruptedException e) {
e.printStackTrace();
}
//內部類訪問外部類,可以在構造函數的參數String前加final,也可以使用外部類名.this.變量名
vehicles.add(Road.this.name + "_" + i);
}
}
});

//定時器每隔1s檢查綠燈是否變亮
//調度池
ScheduledExecutorService timer = Executors.newScheduledThreadPool(1);
timer.scheduleAtFixedRate(new Runnable() {

@Override
public void run() {
//首先檢查路上有沒有車
if (vehicles.size() > 0) {
//定義變量lighted判斷當前線路的燈是否是綠燈
boolean lighted ;
lighted = Lamp.valueOf(Road.this.name).isLighted();
//如果是綠燈,就放行第一輛車
if (lighted) {
System.out.println(vehicles.remove(0) +" is traveling!");
}
}

}
}, 1, 1, TimeUnit.SECONDS);
}
}
/***此案例中使用了Java多線程:類Executors主要的工廠方法(也是創建線程池的方法,創建一個單線程化的Executor。):newSingleThreadExecutor()
創建一個使用單個 worker 線程的 Executor,以無界隊列方式來運行該線程。(注意,如果因爲在關閉前的執行期間出現失敗而終止了此單個線程,那麼如果需要,一個新線程將代替它執行後續的任務)。可保證順序地執行各個任務,並且在任意給定的時間不會有多個線程是活動的。與其他等效的 newFixedThreadPool(1) 不同,可保證無需重新配置此方法所返回的執行程序即可使用其他的線程。

     * Creates an Executor that uses a single worker thread operating
     * off an unbounded queue. (Note however that if this single
     * thread terminates due to a failure during execution prior to
     * shutdown, a new one will take its place if needed to execute
     * subsequent tasks.)  Tasks are guaranteed to execute
     * sequentially, and no more than one task will be active at any
     * given time. Unlike the otherwise equivalent
     * <tt>newFixedThreadPool(1)</tt> the returned executor is
     * guaranteed not to be reconfigurable to use additional threads.
     *
     * @return the newly created single-threaded Executor
     *
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestThread {
    public static void main(String args[]){
        //創建一個使用單個 worker 線程的 Executor,以無界隊列方式來運行該線程。
        ExecutorService pool = Executors.newSingleThreadExecutor();
        //創建實現了runnable接口的對象
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        Thread t3 = new MyThread();
        Thread t4 = new MyThread();
        Thread t5 = new MyThread();
        //將線程放入池中進行執行
        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
        pool.execute(t4);
        pool.execute(t5);
        //關閉線程池
        pool.shutdown();
    }
}
 class MyThread extends Thread{
     
     @Override
     public void run(){
         System.out.println(Thread.currentThread().getName()+" is running...");
     }
 }

單任務延遲連接池:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class TestThread {
    public static void main(String args[]){
        //創建一個線程池,它可安排在給定延遲後運行命令或者定期地執行
        ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();
        //創建實現了runnable接口的對象
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        Thread t3 = new MyThread();
        Thread t4 = new MyThread();
        Thread t5 = new MyThread();
        //將線程放入池中進行執行
        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
         //使用延遲執行風格的方法 
        pool.schedule(t4, 10, TimeUnit.MILLISECONDS); 
        pool.schedule(t5, 10, TimeUnit.MILLISECONDS); 

        //關閉線程池
        pool.shutdown();
    }
}
 class MyThread extends Thread{
     
     @Override
     public void run(){
         System.out.println(Thread.currentThread().getName()+" is running...");
     }
 }
***/

Lamp類
1、 系統中有12個方向上的燈,在程序的其他地方要根據燈的名稱就可以獲得對應的燈的實例對象,將Lamp類用java5中的枚舉形式定義更爲簡單。

2、 每個Lamp對象中的亮黑狀態用lighted變量表示,選用S2N、S2W、E2W、E2N這四個方向上的Lamp對象依次輪詢變亮,Lamp對象中還要有一個opposite變量來表示它們相反方向的燈,再用一個next變量來表示此燈變亮後的下一個變亮的燈。這三個變量用構造方法的形式進行賦值,因爲枚舉元素必須在定義之後引用,所以無法在構造方法中彼此相互引用,所以,相反方向和下一個方向的燈用字符串形式表示。

3、 增加讓Lamp變亮和變黑的方法:light和blackOut,對於S2N、S2W、E2W、E2N這四個方向上的Lamp對象,這兩個方法內部要讓相反方向的燈隨之變亮和變黑,blackOut方法還要讓下一個燈變亮。

4、 除了S2N、S2W、E2W、E2N這四個方向上的Lamp對象之外,其他方向上的Lamp對象的next和opposite屬性設置爲null即可,並且S2N、S2W、E2W、E2N這四個方向上的Lamp對象的next和opposite屬性必須設置爲null,以防止light和blackOut進入死循環。

Lamp類代碼如下:
package traffic3;

/**
 * 每個Lamp元素代表一個方向上的燈,總共有12個方向,所有總共有12個Lamp元素。 有如下一些方向上的燈,每兩個形成一組,一組燈同時變綠或變紅.
 * 
 * @author AdanacLFZ
 * 
 */
public enum Lamp {

// 定義12條線路,每個枚舉元素各表示一個方向的控制燈
// 關鍵在於模擬右轉彎的燈始終是亮着的,以統一模型
S2N("N2S", "S2W", false), S2W("N2E", "E2W", false), E2W("W2E", "E2S", false), E2S(
"W2N", "S2N", false), N2S(null, null, false), N2E(null, null, false), W2E(
null, null, false), W2N(null, null, false), S2E(null, null, true), E2N(
null, null, true), N2W(null, null, true), W2S(null, null, true);

/*與當前燈同時爲綠的對應方向(爲了避免傳遞的錯誤,我們將燈Lamp變爲字符串,傳遞對應燈的名字字符串)*/
private String opposite;
/*當前燈變紅時下一個變綠的燈*/
private String next;
/*當前燈是否爲綠*/
private boolean lighted;

private Lamp(String opposite, String next, boolean lighted) {
this.opposite = opposite;
this.next = next;
this.lighted = lighted;
}

//定義一個返回燈是否亮着的方法
public boolean isLighted() {
return lighted;
}

// 讓燈變綠,並且讓對應方向的燈也變綠
public void light() {
this.lighted = true;
if (opposite != null) {
Lamp.valueOf(opposite).light();
}
System.out.println(name() + " 燈亮了,有6個方向的車在移動");
}


// 讓燈變紅,並且讓對應方向的燈也變紅,也要讓下一個方向的燈變綠
public Lamp blackOut() {
this.lighted = false;
if (opposite != null) {
System.out.println("綠燈由" + name() +" -----> " + next);
Lamp.valueOf(opposite).blackOut();
}

Lamp nextLamp = null;
// 定義一個避免死循環的解決辦法
if (next != null) {
nextLamp = Lamp.valueOf(next);
nextLamp.light();
}
return nextLamp;
}
}

LampController類
1、 整個系統中只能有一套交通燈控制系統,所以,LampController類最好是設計成單例。
2、 LampController構造方法中要設定第一個爲綠的燈。
3、 LampController對象的start方法中將當前燈變綠,然後啓動一個定時器,每隔10秒將當前燈變紅和將下一個燈變綠。
LampController類代碼如下:
package traffic3;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

//定義一個燈的控制器
public class LampController {

//定義一個變量:當前的燈
private Lamp currentLamp;

//定義構造方法, 構造方法上來 應該讓某一個燈變綠
public LampController() {

currentLamp = Lamp.S2N;
currentLamp.light();

//做個定時器,讓每隔10s綠燈變紅燈,並讓下一個方向的燈變綠
ScheduledExecutorService timer = Executors.newScheduledThreadPool(1);
timer.scheduleAtFixedRate(new Runnable() {

@Override
public void run() {
currentlLamp = currentlLamp.blackOut();
}, 10, 10, TimeUnit.SECONDS);
}
}

MainClass類
從名字可以看出這個類,是main方法所在類,main方法中,用for循環創建出代表12條路線的對象,接着再獲得LampController對象並調用其start方法。
代碼如下:
package traffic3;

public class MainClass {

public static void main(String[] args) {

//創建12條路線的對象
String[] directions = {
"S2N","S2W","E2W","E2S",
"N2S","N2E","W2E","W2N",
"S2E","E2N","N2W","W2S"
};
for (int i = 0; i < directions.length; i++) {
new Road(directions[i]);
}

//產生整個交通燈系統 
new LampController();
}
}

執行結果:
N2S變綠了,即將看到有6個方向的有車通過~~~
S2N變綠了,即將看到有6個方向的有車通過~~~
N2S_1 is traveling!
S2N_1 is traveling!
W2S_1 is traveling!
S2E_1 is traveling!
N2W_1 is traveling!
W2S_2 is traveling!
S2E_2 is traveling!
S2N_2 is traveling!
N2S_2 is traveling!
E2N_1 is traveling!
綠燈從S2N ----> S2W
N2E變綠了,即將看到有6個方向的有車通過~~~
S2W變綠了,即將看到有6個方向的有車通過~~~
S2W_1 is traveling!
N2E_1 is traveling!
S2E_3 is traveling!
S2W_2 is traveling!
N2E_2 is traveling!
N2W_2 is traveling!
S2E_4 is traveling!
S2W_3 is traveling!
W2S_3 is traveling!
E2N_2 is traveling!
N2W_3 is traveling!
S2E_5 is traveling!
綠燈從S2W ----> E2W
W2E變綠了,即將看到有6個方向的有車通過~~~
E2W變綠了,即將看到有6個方向的有車通過~~~
E2W_1 is traveling!
W2E_1 is traveling!
E2W_2 is traveling!
W2E_2 is traveling!

成功的模擬的對交通燈的控制,這個項目中最重要的一步就是,分析業務邏輯,畫出相應的分析圖:總共有12條路線,爲了統一編程模型,可以假設每條路線都有一個紅綠燈對其進行控制,右轉彎的4條路線的控制燈可以假設稱爲常綠狀態,另外,其他的8條線路是兩兩成對的,可以歸爲4組,所以,程序只需考慮圖中標註了數字號的4條路線的控制燈的切換順序,這4條路線相反方向的路線的控制燈跟隨這4條路線切換,不必額外考慮。沒有這一步這個項目就無從入手了,這也告訴我們,做一個項目之前,要理解需求,理清思路,讓項目在大腦中有一個大致的雛形,然後再去編碼,隨着編碼的進行對項目的理解會越來越深,這個項目纔會成功,而拿到一個項目不仔細思考上來就編碼的方式不是良好的方式。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章