【OO第三次課下討論】農場主的飼料分配問題
- 2020 年 3 月 10 日
- 筆記
本思考題的設計需求是力圖找到一個簡單且可行的飼料分配方案,由於不涉及到飼料價格或者是營養均衡之類的優化問題,因此在假設總的飼料量必能滿足所有動物的熱量需求的前提下,我們只需要採用貪心策略就可以找到一個算法上的可行解。
當然,這裡我們關注的不只是在算法思路上的儘可能簡單,由於是OO,所以我們更需要將整個過程進行抽象和封裝,從而使得該方案可以在頂層模塊的架構上也要儘可能的簡單清晰。而為了做到這一點,本節課上我們所學習到的知識(也就是繼承與實現,多態與歸一化等)可以說是必不可少的。
首先,我們可以看到,這個問題的核心就是要處理動物與飼料之間的一個映射關係,也就是某個動物分別需要哪幾種飼料各多少,某種飼料分別投喂多少給哪幾種動物這樣。那麼很顯然,我們需要建立動物(Animal)和飼料(Fodder)這兩個“家族”,至於它們都有哪些具體的成員以及成員之間有着怎樣的關係(繼承or實現)則取決於進一步對其屬性和方法的分析。
對於動物而言,我們需要知道這個動物可以吃哪些飼料,以及它每天需要的總熱量,這些都是屬性,所以我們需要將Animal定義為一個父類而非接口;同理,對於飼料,我們也需要知道飼料的總重量以及每單位飼料所產生的相應熱量,因此Fodder也必須是一個父類。1
明確屬性只是第一步,接下來我們得知道對這些屬性我們要執行什麼操作,也就是方法。而對於方法的實現,首先應該明確的是我們站在誰的立場上去思考問題,如果我們站在飼料的立場上,那麼我們關心的就是這個飼料應該餵給哪些動物,分別餵了多少等等;而如果我們站在動物的立場上,那麼我們關心的就是這個動物吃了哪些飼料,都吃了多少。相信同學們看到這已經意識到了,沒錯,就本題而言,出題人已經提前幫我們明確了我們的立場——我們知道的是“每一種動物能吃哪些飼料有明確規定”,而非“每一種飼料能給哪些動物吃有明確規定”。
既然如此,我們就可以給動物一個基本方法(Key Method):餵食(feed)。這個方法的大致內容就是:如果這個動物所需的總熱量還沒有得到滿足,就從它能吃的飼料里任選一種,盡最大可能地滿足這個動物的熱量需求。
其實這個方法同時也是我們算法的核心,比如如果一次餵食沒有喂完怎麼辦?是優先把當前這個動物喂到飽,還是盡量給大家都均衡一些?不過由於這個跟我們本節課探討的主題沒什麼關係,所以我們就不用管它了。這裡筆者就採用了一種最簡單的方法,也就是每次只選一種飼料,能給這隻喂多少就喂多少,不夠後面再說。
而圍繞着這個核心方法,其他的方法也就自然而然地明確了,包括更新動物的可食用飼料列表,獲取特定飼料的單位卡路里,獲取特定飼料的當前剩餘量,更新特定飼料的當前剩餘量等等。最後我們得到的Animal與Fodder類的屬性與方法如下圖所示:
![]() |
![]() |
這樣一來,我們在main方法中就只需要利用一個簡單的循環就可以獲得我們想要的飼料分配方案。為了便於演示,我設計了Cow和Pig兩種動物,以及Water, Grass, Corn三種飼料。它們的基本信息如下所示:
Animal | Fodders | Calorie |
---|---|---|
Cow | Water, Grass | 500 |
Pig | Water, Corn | 300 |
Fodder | CaloriePerKG | Amount(可在初始化時設定) |
---|---|---|
Water | 20 | 20 |
Grass | 50 | 5 |
Corn | 80 | 3 |
main方法的代碼如下:
public static void main(String[] args) { //初始化基本信息 ArrayList<Animal> animals = new ArrayList<>(); Water water = new Water(20); Grass grass = new Grass(5); Corn corn = new Corn(3); Cow cow = new Cow(); cow.addFodder(grass); cow.addFodder(water); animals.add(cow); Pig pig = new Pig(); pig.addFodder(corn); pig.addFodder(water); animals.add(pig); ArrayList<FeedInfo> feedInfos = new ArrayList<>(); //制定飼料分配方案 while (true) { boolean allFull = true; for (Animal animal : animals) { FeedInfo feedInfo = animal.feed(); if (feedInfo != null) { feedInfos.add(feedInfo); allFull = false; } } if (allFull) { break; } } //輸出最終信息 for (FeedInfo feedInfo : feedInfos) { System.out.println(feedInfo.toString()); } }
整個項目的UML圖如下:
最終程序運行輸出的結果如下:
至此,整個飼料分配問題基本得到了較為完善的解決。
總結與歸納
那麼,問題來了:在整個方案的設計與實現中,究竟哪裡運用到了歸一化和多態呢?
答案就在之前提到的FeedInfo類中:
1 public class FeedInfo { 2 private Animal animal;//投喂的動物 3 private Fodder fodder;//投喂的飼料 4 private double amount;//一次投喂的飼料量 5 6 public FeedInfo(Animal animal, Fodder fodder, double amount) { 7 this.animal = animal; 8 this.fodder = fodder; 9 this.amount = amount; 10 } 11 12 @Override 13 public String toString() { 14 return animal.toString() + " " + fodder.toString() + " " + amount; 15 } 16 }
可能有的同學看到這會覺得有些失望:為啥到頭來多態就用在了這麼一個“不起眼”的地方呢?其實,這恰恰是因為作為父類的Animal和Fodder把子類的大部分方法和所有屬性都已經實現了,子類也就沒有必要再去修改啦(包括我們的feed()方法)。當然如果你把最後的輸出結果放在feed()裏面,那麼子類就必須對它進行重寫了,不過這樣反而有些冗長而有損簡潔美,所以我就沒有這麼做。2
額外的細節——
1.淺拷貝與深拷貝
如果有同學仔細地看了我的main函數的話,可能還會發現另一個問題:明明所有的飼料都是在main方法里定義的,動物是怎麼在自己的feed()方法里對飼料的量進行修改的呢?其實這就涉及到面向對象裏面另一個很重要的問題:關於深淺拷貝的使用與注意事項。簡單來說,這裡雖然所有的Fodder都是在main()函數里定義的,但是它們的引用卻被傳給了相應的動物。不僅如此,以water為例,cow和pig飼料列表裡的water其實指向的是同一個water(類似c語言的指針),這樣我們就可以很方便地在cow和pig各自的feed()方法里對water的量進行增減了。關於深淺拷貝具體的介紹還請感興趣的同學自己到網上查一下,這裡就不展開了。但是必須要強調的是,這個東西是把雙刃劍,用的好可以讓代碼變得簡潔,但要是用不好,很可能會出現一些不可描述的錯誤,還請各位務必小心哦~
2.生產者-消費者模式
另外有一點不得不說,就是這個農場模型很像並發里經典的生產者–消費者這樣的關係——飼料是生產者,動物是消費者。不過區別在於,這裡我們不是多線程並發實現的,而是所有的動物排好隊一個一個來餵食的。
所以,現在我們不妨做一個更大膽的假設:如果有一天,農場變成了奧威爾筆下的《動物莊園》,所有的飼料都在一個倉庫里,動物們自己過來拿自己想吃的吃,那麼他們會打起來嗎?如果採用我們現在的方法,又會發生什麼有趣的事情呢?