【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: 学习 第三单元终于结束了,这是我目前为止最惨的一单元,第十次作 ...
随机推荐
- rest framework-序列化-长期维护
############### 表结构 ############### from django.db import models class Book(models.Model): titl ...
- bind() 方法
一. 定义和用法 bind() 方法为被选元素添加一个或多个事件处理程序,并规定事件发生时运行的函数. 语法: $(selector).bind(event,data,function) 举例:
- 关联规则之FpGrowth算法
Aprori算法利用频繁集的两个特性,过滤了很多无关的集合,效率提高不少,但是我们发现Apriori算法是一个候选消除算法,每一次消除都需要扫描一次所有数据记录,造成整个算法在面临大数据集时显得无能为 ...
- ios 设置UITextField的placeholder大小颜色
需求:产品嫌弃placeholder的字体太大,颜色太明显,要求跟正常输入时的字体及颜色不同 方法:设置placeholder的大小和颜色,实际上是设置placeholder的label的大小和颜色, ...
- Hadoop_在linux中安装hadopp出现的问题
问题 sudo: no valid sudoers sources found, quitting 网络解决方法 链接:sudo: no valid sudoers sources found, qu ...
- blast -m1
当database是10个物种(A.B.C.E.F.G.H.J.I.K)时,进行all vs all 比对结果是: 此时reference是物种A的第一个基因:即用10个物种的genome中的所有基因 ...
- python3多线程应用详解(第二卷:多线程到底是怎么工作的)
现在很多人都说用多线程工作快是因为多个不同任务可以同时执行,注意我说的是不同任务,要是重复做一件事达到相同效果就是画蛇添足了,其实这是个错误的说法,线程真正的本质是无法同时执行的.现在我们来看下多线程 ...
- getline的使用
函数定义: getline(istream &in, string &s) 作用: 在C++中用 string 类型进行终端输入字符串时,解决无法输入带有空格的字符串的问题. 功能: ...
- AD复制问题汇总
1:文件复制服务NtFrs 13568报错的解决方法 解决方法: 建议不要按照日志的提示进行操作,正确的操作应该是 出现这个问题的原因,是由于在硬件的损坏,导致服务器未正确处理NTFS USN 日志. ...
- win10安装revit失败,怎么强力卸载删除注册表并重新安装
一些搞设计的朋友在win10系统下安装revit失败或提示已安装,也有时候想重新安装revit的时候会出现本电脑windows系统已安装revit,你要是不留意直接安装revit,只会安装revit的 ...