【OO第三次课下讨论】农场主的饲料分配问题
需求分析与项目设计
本思考题的设计需求是力图找到一个简单且可行的饲料分配方案,由于不涉及到饲料价格或者是营养均衡之类的优化问题,因此在假设总的饲料量必能满足所有动物的热量需求的前提下,我们只需要采用贪心策略就可以找到一个算法上的可行解。
当然,这里我们关注的不只是在算法思路上的尽可能简单,由于是OO,所以我们更需要将整个过程进行抽象和封装,从而使得该方案可以在顶层模块的架构上也要尽可能的简单清晰。而为了做到这一点,本节课上我们所学习到的知识(也就是继承与实现,多态与归一化等)可以说是必不可少的。
首先,我们可以看到,这个问题的核心就是要处理动物与饲料之间的一个映射关系,也就是某个动物分别需要哪几种饲料各多少,某种饲料分别投喂多少给哪几种动物这样。那么很显然,我们需要建立动物(Animal)和饲料(Fodder)这两个“家族”,至于它们都有哪些具体的成员以及成员之间有着怎样的关系(继承or实现)则取决于进一步对其属性和方法的分析。
对于动物而言,我们需要知道这个动物可以吃哪些饲料,以及它每天需要的总热量,这些都是属性,所以我们需要将Animal定义为一个父类而非接口;同理,对于饲料,我们也需要知道饲料的总重量以及每单位饲料所产生的相应热量,因此Fodder也必须是一个父类。1
明确属性只是第一步,接下来我们得知道对这些属性我们要执行什么操作,也就是方法。而对于方法的实现,首先应该明确的是我们站在谁的立场上去思考问题,如果我们站在饲料的立场上,那么我们关心的就是这个饲料应该喂给哪些动物,分别喂了多少等等;而如果我们站在动物的立场上,那么我们关心的就是这个动物吃了哪些饲料,都吃了多少。相信同学们看到这已经意识到了,没错,就本题而言,出题人已经提前帮我们明确了我们的立场——我们知道的是“每一种动物能吃哪些饲料有明确规定”,而非“每一种饲料能给哪些动物吃有明确规定”。
既然如此,我们就可以给动物一个基本方法(Key Method):喂食(feed)。这个方法的大致内容就是:如果这个动物所需的总热量还没有得到满足,就从它能吃的饲料里任选一种,尽最大可能地满足这个动物的热量需求。
其实这个方法同时也是我们算法的核心,比如如果一次喂食没有喂完怎么办?是优先把当前这个动物喂到饱,还是尽量给大家都均衡一些?不过由于这个跟我们本节课探讨的主题没什么关系,所以我们就不用管它了。这里笔者就采用了一种最简单的方法,也就是每次只选一种饲料,能给这只喂多少就喂多少,不够后面再说。
而围绕着这个核心方法,其他的方法也就自然而然地明确了,包括更新动物的可食用饲料列表,获取特定饲料的单位卡路里,获取特定饲料的当前剩余量,更新特定饲料的当前剩余量等等。最后我们得到的Animal与Fodder类的属性与方法如下图所示:
![]() |
![]() |
这里的FeedInfo是一个投喂信息类,由(Animal, Fodder, amount)三元组构成,代表一次喂食的基本信息。其具体代码见后文。
这样一来,我们在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类中:
public class FeedInfo {
private Animal animal;//投喂的动物
private Fodder fodder;//投喂的饲料
private double amount;//一次投喂的饲料量
public FeedInfo(Animal animal, Fodder fodder, double amount) {
this.animal = animal;
this.fodder = fodder;
this.amount = amount;
}
@Override
public String toString() {
return animal.toString() + " " + fodder.toString() + " " + amount;
}
}
可以看到FeedInfo的toString()方法是直接调用了animal和fodder的toString()方法,但事实上,不同的具体的动物类和饲料类都对这个方法进行了重写,从而使得最终输出的结果里体现的是各自子类的toString(),而不是Animal的。
可能有的同学看到这会觉得有些失望:为啥到头来多态就用在了这么一个“不起眼”的地方呢?其实,这恰恰是因为作为父类的Animal和Fodder把子类的大部分方法和所有属性都已经实现了,子类也就没有必要再去修改啦(包括我们的feed()方法)。当然如果你把最后的输出结果放在feed()里面,那么子类就必须对它进行重写了,不过这样反而有些冗长而有损简洁美,所以我就没有这么做。2
额外的细节——
1.浅拷贝与深拷贝
如果有同学仔细地看了我的main函数的话,可能还会发现另一个问题:明明所有的饲料都是在main方法里定义的,动物是怎么在自己的feed()方法里对饲料的量进行修改的呢?其实这就涉及到面向对象里面另一个很重要的问题:关于深浅拷贝的使用与注意事项。简单来说,这里虽然所有的Fodder都是在main()函数里定义的,但是它们的引用却被传给了相应的动物。不仅如此,以water为例,cow和pig饲料列表里的water其实指向的是同一个water(类似c语言的指针),这样我们就可以很方便地在cow和pig各自的feed()方法里对water的量进行增减了。关于深浅拷贝具体的介绍还请感兴趣的同学自己到网上查一下,这里就不展开了。但是必须要强调的是,这个东西是把双刃剑,用的好可以让代码变得简洁,但要是用不好,很可能会出现一些不可描述的错误,还请各位务必小心哦~
2.生产者-消费者模式
另外有一点不得不说,就是这个农场模型很像并发里经典的生产者--消费者这样的关系——饲料是生产者,动物是消费者。不过区别在于,这里我们不是多线程并发实现的,而是所有的动物排好队一个一个来喂食的。
所以,现在我们不妨做一个更大胆的假设:如果有一天,农场变成了奥威尔笔下的《动物庄园》,所有的饲料都在一个仓库里,动物们自己过来拿自己想吃的吃,那么他们会打起来吗?如果采用我们现在的方法,又会发生什么有趣的事情呢?
【OO第三次课下讨论】农场主的饲料分配问题的更多相关文章
- 【OO课下讨论】bug中的“二八定律”
bug中的"二八定律" 本文主要为讨论2020/3/17下午OO课讨论的第三个思考题设立 有一个经典的经验性原则,叫帕累托原则,也称为二八定律.这个原则在经济.社会和科技等多个领域 ...
- OO前三次作业简单总结
随着几周的进行,OO课堂已经经历过三次课下作业.在这三次作业中,我被扣了一些分数,也发现了自己几次作业中一些存在的共同的问题. 首先以第三次作业为例分析,我程序的类图如下 一共九个类,其中Als_sc ...
- OO第三单元作业(JML)总结
OO第三单元作业(JML)总结 目录 OO第三单元作业(JML)总结 JML语言知识梳理 使用jml的目的 jml注释结构 jml表达式 方法规格 类型规格 SMT Solver 部署JMLUnitN ...
- 渡过OO的死劫,了解规格的意义——OO第三次博客总结
当熬过了一次次黑暗,迎接我们的却是被扣的惨不忍睹的JSF ┭┮﹏┭┮ 一.总结调研 规格的历史 传统科学的特点是发现世界,而软件的特点是构造世界.软件的最底层就是0,1,两个离散的值.程序设计语言的三 ...
- OO第三次博客作业——规格
OO第三次博客作业——规格 一.调研结果: 规格的历史: 引自博文链接:http://blog.sina.com.cn/s/blog_473d5bba010001x9.html 传统科学的特点是发现世 ...
- OO第三单元作业总结
OO第三单元作业总结--JML 第三单元的主题是JML规格的学习,其中的三次作业也是围绕JML规格的实现所展开的(虽然感觉作业中最难的还是如何正确适用数据结构以及如何正确地对于时间复杂度进行优化). ...
- 【OO学习】OO第三单元作业总结
[OO学习]OO第三单元作业总结 第三单元,我们学习了JML语言,用来进行形式化设计.本单元包括三次作业,通过给定的JML来实行了一个对路径的管理系统,最后完成了一个地铁系统,来管理不同的线路,求得关 ...
- OO第三单元个人总结
OO第三单元个人总结 JML理论与基础与应用工具链 JML是什么? Java建模语言(JML)是一种行为接口规范语言,可用于指定Java模块的行为 .它结合了Eiffel的契约设计方法 和Larch ...
- 2020 OO 第三单元总结 JML语言
title: 2020 OO 第三单元总结 date: 2020-05-21 10:10:06 tags: OO categories: 学习 第三单元终于结束了,这是我目前为止最惨的一单元,第十次作 ...
随机推荐
- “全隐藏式3D摄像头”亮相,FindX如何将设计与体验融为一体
北京时间6月20日,OPPO在卢浮宫发布暌违四年之久的Find旗舰系列新手机--Find X.在Find X背后,我认为其设计值得深思.尤其是Find X为突破传统设计束缚,首创双轨潜望结构有着重要启 ...
- HTTP Status 400,400 (Bad Request)
400 (Bad Request) 点击添加按钮转跳没反应,控制台没报错,然后在Chrome上检查发现报错了 百度了一下,发现http Status 400这个错误大多是因为,jsp的form表单提交 ...
- hibernate反向工程
- LeetCode No.82,83,84
No.82 DeleteDuplicates 删除排序链表中的重复元素 II 题目 给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字. 示例 输入: 1->2 ...
- [LC] 674. Longest Continuous Increasing Subsequence
Given an unsorted array of integers, find the length of longest continuous increasing subsequence (s ...
- OpenCV Sobel 导数
#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #i ...
- jQuery中的bind(), live(), on(), delegate()
当我们试图绑定一些事件到DOM元素上的时候,我相信上面这4个方法是最常用的.而它们之间到底有什么不同呢?在什么场合下用什么方法是最有效的呢? 准备知识: 当我们在开始的时候,有些知识是必须具备的: D ...
- <JZOJ5906>传送门
emmm dpdpdp然鹅我考场上并想不到 还是凉凉 #include<cstdio> #include<cmath> #include<iostream> #in ...
- 简单了解char和varchar的区别
一.CHAR和 VARCHAR的区别? 1.CHAR有固定的长度,VARCHAR属于可变长度的字符类型. 2.比如,写入内容到CHAR(10)里,如果内容占用的空间不够10,则会用空格字符补足. 二. ...
- python 常用模块介绍
1.定义 模块:用来从逻辑上组织python代码(变量.函数.类,逻辑),本质就是.py结尾的python文件(文件名:test.py,对应的模块名:test). 包:用来从逻辑上组织模块的,本质就是 ...

