1. 程式人生 > >【OO第三次課下討論】農場主的飼料分配問題

【OO第三次課下討論】農場主的飼料分配問題

需求分析與專案設計

  本思考題的設計需求是力圖找到一個簡單且可行的飼料分配方案,由於不涉及到飼料價格或者是營養均衡之類的優化問題,因此在假設總的飼料量必能滿足所有動物的熱量需求的前提下,我們只需要採用貪心策略就可以找到一個演算法上的可行解。

  當然,這裡我們關注的不只是在演算法思路上的儘可能簡單,由於是OO,所以我們更需要將整個過程進行抽象和封裝,從而使得該方案可以在頂層模組的架構上也要儘可能的簡單清晰。而為了做到這一點,本節課上我們所學習到的知識(也就是繼承與實現,多型與歸一化等)可以說是必不可少的。

  首先,我們可以看到,這個問題的核心就是要處理動物與飼料之間的一個對映關係,也就是某個動物分別需要哪幾種飼料各多少,某種飼料分別投喂多少給哪幾種動物這樣。那麼很顯然,我們需要建立動物(Animal)和飼料(Fodder)這兩個“家族”,至於它們都有哪些具體的成員以及成員之間有著怎樣的關係(繼承or實現)則取決於進一步對其屬性和方法的分析。

  對於動物而言,我們需要知道這個動物可以吃哪些飼料,以及它每天需要的總熱量,這些都是屬性,所以我們需要將Animal定義為一個父類而非介面;同理,對於飼料,我們也需要知道飼料的總重量以及每單位飼料所產生的相應熱量,因此Fodder也必須是一個父類。1

[​1]  Key: 有無屬性是決定用介面還是父類的關鍵之一

  明確屬性只是第一步,接下來我們得知道對這些屬性我們要執行什麼操作,也就是方法。而對於方法的實現,首先應該明確的是我們站在誰的立場上去思考問題,如果我們站在飼料的立場上,那麼我們關心的就是這個飼料應該餵給哪些動物,分別餵了多少等等;而如果我們站在動物的立場上,那麼我們關心的就是這個動物吃了哪些飼料,都吃了多少

。相信同學們看到這已經意識到了,沒錯,就本題而言,出題人已經提前幫我們明確了我們的立場——我們知道的是“每一種動物能吃哪些飼料有明確規定”,而非“每一種飼料能給哪些動物吃有明確規定”

  既然如此,我們就可以給動物一個基本方法(Key Method):餵食(feed)。這個方法的大致內容就是:如果這個動物所需的總熱量還沒有得到滿足,就從它能吃的飼料裡任選一種,盡最大可能地滿足這個動物的熱量需求。

  其實這個方法同時也是我們演算法的核心,比如如果一次餵食沒有喂完怎麼辦?是優先把當前這個動物喂到飽,還是儘量給大家都均衡一些?不過由於這個跟我們本節課探討的主題沒什麼關係,所以我們就不用管它了。這裡筆者就採用了一種最簡單的方法,也就是每次只選一種飼料,能給這隻喂多少就喂多少,不夠後面再說。

  而圍繞著這個核心方法,其他的方法也就自然而然地明確了,包括更新動物的可食用飼料列表,獲取特定飼料的單位卡路里,獲取特定飼料的當前剩餘量,更新特定飼料的當前剩餘量等等。最後我們得到的Animal與Fodder類的屬性與方法如下圖所示:

 

     這裡的FeedInfo是一個投喂資訊類,由(Animal, Fodder, amount)三元組構成,代表一次餵食的基本資訊。其具體程式碼見後文。

  這樣一來,我們在main方法中就只需要利用一個簡單的迴圈就可以獲得我們想要的飼料分配方案。為了便於演示,我設計了Cow和Pig兩種動物,以及Water, Grass, Corn三種飼料。它們的基本資訊如下所示:

AnimalFoddersCalorie
Cow Water, Grass 500
Pig Water, Corn 300

 

FodderCaloriePerKGAmount(可在初始化時設定)
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 }

       可以看到FeedInfo的toString()方法是直接呼叫了animal和fodder的toString()方法,但事實上,不同的具體的動物類和飼料類都對這個方法進行了重寫,從而使得最終輸出的結果裡體現的是各自子類的toString(),而不是Animal的。

  可能有的同學看到這會覺得有些失望:為啥到頭來多型就用在了這麼一個“不起眼”的地方呢?其實,這恰恰是因為作為父類的Animal和Fodder把子類的大部分方法和所有屬性都已經實現了,子類也就沒有必要再去修改啦(包括我們的feed()方法)。當然如果你把最後的輸出結果放在feed()裡面,那麼子類就必須對它進行重寫了,不過這樣反而有些冗長而有損簡潔美,所以我就沒有這麼做。2

[2]  Key: 實現需求才是最重要的,沒必要太過刻意去使用某些語言本身的特性。東西是拿來用的,不是拿來顯擺的。

額外的細節——

1.淺拷貝與深拷貝

  如果有同學仔細地看了我的main函式的話,可能還會發現另一個問題:明明所有的飼料都是在main方法裡定義的,動物是怎麼在自己的feed()方法裡對飼料的量進行修改的呢?其實這就涉及到面向物件裡面另一個很重要的問題:關於深淺拷貝的使用與注意事項。簡單來說,這裡雖然所有的Fodder都是在main()函式裡定義的,但是它們的引用卻被傳給了相應的動物。不僅如此,以water為例,cow和pig飼料列表裡的water其實指向的是同一個water(類似c語言的指標),這樣我們就可以很方便地在cow和pig各自的feed()方法裡對water的量進行增減了。關於深淺拷貝具體的介紹還請感興趣的同學自己到網上查一下,這裡就不展開了。但是必須要強調的是,這個東西是把雙刃劍,用的好可以讓程式碼變得簡潔,但要是用不好,很可能會出現一些不可描述的錯誤,還請各位務必小心哦~

2.生產者-消費者模式

  另外有一點不得不說,就是這個農場模型很像併發裡經典的生產者--消費者這樣的關係——飼料是生產者,動物是消費者。不過區別在於,這裡我們不是多執行緒併發實現的,而是所有的動物排好隊一個一個來餵食的。

  所以,現在我們不妨做一個更大膽的假設:如果有一天,農場變成了奧威爾筆下的《動物莊園》,所有的飼料都在一個倉庫裡,動物們自己過來拿自己想吃的吃,那麼他們會打起來嗎?如果採用我們現在的方法,又會發生什麼有趣的事情