实习第五天CTO给我们分享了一个TDD的面试题目的答案,让我们评价一下。在大家挑毛病的时候,我发现自己有很多以前从没注意过的地方,比方PSR规范,命名规则等等。
题目:https://github.com/joelpittet/phpinterview
答案:https://github.com/tsuijie/phpinterview
今天自己在尝试解答这道题目时发现,有两个测试怎么也通过不了。搞了很长时间,还是不行,只好借鉴一下答案+_+,不过我发现答案和我完全就是两种看待问题的思路,一个是面向过程,另一个则是面向对象去思考。而且我认为两种做法都能解释的通。
题目
题目是一个电影院售票,如何让票价最便宜的问题,讲道理放在高中就是一道稍微麻烦点的应用题,只不过现在扯上了编程。
- 售票规则:
Basic admission rates (regular weekday, 2D movie, <=120 min, parquet):
General admission $11.00
Students $8.00
Senior Citizens (65 & older) $6.00
Children (under 13) $5.50
Group (20 people or more) $6.00 each
Exceptions:
3D movie +$3.00
Over-length (more than 120 min.) +$1.50
Movie Day (Thurdsday, except for groups!) -$2.00
Weekends +$1.50
Loge +$2.00
- 测试文件:
assertEquals($expectedResult, $result, $message . ", mid-age, no student");
}
protected function applyTickets(Movie $cashReg, $runtime, $day, $isParquet, $is3D, $tickets) {
$cashReg->startPurchase($runtime, $day, $isParquet, $is3D);
foreach ($tickets as $ticket) {
$cashReg->addTicket($ticket[0], $ticket[1]);
}
return $cashReg;
}
/**
* Calculate helper.
*
* @param int $runtime
* The runtime in minutes.
* @param int $day
* The day of the week in integer value.
* @param bool $isParquet
* If the movie is Parquet or Lode.
* @param bool $is3D
* Whether the movie is in 3D or 2D.
* @param array $tickets
* A list of tickets.
*
* @return float
*/
protected function calc($runtime, $day, $isParquet, $is3D, $tickets) {
$cashReg = new Movie();
$this->applyTickets($cashReg, $runtime, $day, $isParquet, $is3D, $tickets);
return $cashReg->finishPurchase();
}
public function testNoTicketsCostsZero() {
$result = $this->calc(0, DaysOfWeek::MON, FALSE, FALSE, []);
$this->assertNotNull($result, "No tickets costs are not NULL");
$this->assertNotInternalType('string', $result, "No tickets costs are not a string");
$this->assertEquals(0, $result, "No tickets costs zero");
}
public function testOverlength2DParquetWednesdayMidAgeStudents() {
$tickets = [[35, FALSE], [35, FALSE], [64, TRUE], [35, TRUE]];
$result = $this->calc(121, DaysOfWeek::WED, TRUE, FALSE, $tickets);
$this->assertEquals(44.0, $result, "overlength, 2D, parquet, wednesday, mid-age, students");
}
public function testOverlength2DParquetMondaySeniorNoStudents() {
$tickets = [[35, FALSE], [35, FALSE], [64, FALSE], [65, FALSE]];
$result = $this->calc(123, DaysOfWeek::MON, TRUE, FALSE, $tickets);
$this->assertEquals(45.0, $result, "overlength, 2D, parquet, monday, senior, no students");
}
public function testOverlength2DParquetTuesdaySeniorStudents() {
$tickets = [[35, FALSE], [35, FALSE], [64, FALSE], [68, TRUE]];
$result = $this->calc(145, DaysOfWeek::TUE, TRUE, FALSE, $tickets);
$this->assertEquals(45.0, $result, "overlength, 2D, parquet, tuesday, senior students");
}
public function testOverlength2DParquetTuesday1ChildNoStudents() {
$tickets = [[35, FALSE], [35, FALSE], [64, FALSE], [10, FALSE]];
$result = $this->calc(145, DaysOfWeek::TUE, TRUE, FALSE, $tickets);
$this->assertEquals(44.5, $result, "overlength, 2D, parquet, tuesday, 1 child, no students");
}
public function test2DparquetTuesdayGroupNoStudents() {
$tickets = [];
for ($i = 0; $i < 23; $i++) {
$tickets[] = [35, FALSE];
}
$result = $this->calc(72, DaysOfWeek::TUE, TRUE, FALSE, $tickets);
$this->assertEquals(138.0, $result, "2D, parquet, tuesday, group, no students");
}
public function test3DParquetTuesdayGroupNoStudents() {
$tickets = [];
for ($i = 0; $i < 23; $i++) {
$tickets[] = [35, FALSE];
}
$result = $this->calc(72, DaysOfWeek::TUE, TRUE, TRUE, $tickets);
$this->assertEquals(207.0, $result, "3D, parquet, tuesday, group, no students");
}
public function test2DGroupOfKidsWithTwoAdults() {
$tickets = [];
for ($i = 0; $i < 24; $i++) {
$tickets[] = [12, FALSE];
}
$tickets[] = [45, FALSE];
$tickets[] = [27, FALSE];
$result = $this->calc(72, DaysOfWeek::FRI, TRUE, FALSE, $tickets);
$this->assertEquals(144.0, $result, "2D, group of kids with two adults");
}
public function test2D17KidsWithTwoAdults() {
$tickets = [];
for ($i = 0; $i < 17; $i++) {
$tickets[] = [12, FALSE];
}
$tickets[] = [45, FALSE];
$tickets[] = [27, FALSE];
$result = $this->calc(72, DaysOfWeek::WED, TRUE, FALSE, $tickets);
$this->assertEquals(115.5, $result, "2D, 17 kids with two adults");
}
public function testOverlengthLoge3DMovieDayGroup() {
$tickets = [];
for ($i = 0; $i < 5; $i++) {
$tickets[] = [12, FALSE];
}
for ($i = 0; $i < 7; $i++) {
$tickets[] = [45, FALSE];
}
for ($i = 0; $i < 4; $i++) {
$tickets[] = [75, FALSE];
}
for ($i = 0; $i < 8; $i++) {
$tickets[] = [27, TRUE];
}
$result = $this->calc(125, DaysOfWeek::THU, FALSE, TRUE, $tickets);
$this->assertEquals(297.5, $result, "overlength, loge, 3D, movie-day group");
}
public function testOverlengthLoge3DmovieDayNonGroup() {
$tickets = [];
for ($i = 0; $i < 2; $i++) {
$tickets[] = [12, FALSE];
}
for ($i = 0; $i < 7; $i++) {
$tickets[] = [45, FALSE];
}
for ($i = 0; $i < 4; $i++) {
$tickets[] = [75, FALSE];
}
for ($i = 0; $i < 4; $i++) {
$tickets[] = [27, TRUE];
}
$result = $this->calc(125, DaysOfWeek::THU, FALSE, TRUE, $tickets);
$this->assertEquals(220.5, $result, "overlength, loge, 3D, movie-day non-group");
}
public function testMultipleTransactionSameRegister() {
$cashReg = new Movie();
$tickets = [
[35, FALSE], [35, FALSE], [35, FALSE], [35, FALSE], [35, FALSE],
];
$this->applyTickets($cashReg, 90, DaysOfWeek::MON, TRUE, TRUE, $tickets);
$this->assertEquals(70.0, $cashReg->finishPurchase(), "multiple transactions, same register");
$this->applyTickets($cashReg, 90, DaysOfWeek::MON, TRUE, TRUE, $tickets);
$this->assertEquals(70.0, $cashReg->finishPurchase(), "multiple transactions, same register");
}
}
我的思路
售票单价中有5种基于年龄,是否是学生和是否团购来判断的价位,其中儿童票最便宜,普通票最贵。
那么就是购票人是一个对象,其中年龄,票价,是否是学生,是否团购是他的属性。
由于单次售票过程中附加条件都是一样的,所以属于对象的公共变量。只有当团购而且是电影日时不同。
- 那么在Movie类中就的startPurchase()方法就可以用来初始化和附加条件判断。addTicket()方法用来统计人数与当前的总票价。finishPurchase()方法用来最后的票价检查,检测是否有更便宜的购票方式。
- 由于儿童票最便宜,团购票次之,所以肯定是儿童票越多越好,而且由于年龄是固有属性所以儿童要被独立出来。当人数大于20时,就有了团购的可能性,但当有儿童存在时团购票价不一定就比较低,所以要在最后进行判断到底组不组团。
- 我的代码:
TotalPrice = 0;
$this->GroupNum = 0;
$this->TotalNum = 0;
$this->Exceptions = 0;
$this->IsMovieDay = false;
if ($runtime > 120){
$this->Exceptions += self::OverLength;
}
if ($day == DaysOfWeek::THU){
$this->Exceptions += self::MOVIE_DAY;
$this->IsMovieDay = true;
} elseif ($day == DaysOfWeek::SUN || $day == DaysOfWeek::SAT){
$this->Exceptions += self::WEEKENDS;
}
if (! $isParquet){
$this->Exceptions += self::LOGE;
}
if ($is3D){
$this->Exceptions += self::IS_3D_MOVIE;
}
}
/**
* Adds a ticket to the transaction.
*
* @param int $age
* The age of the ticket holder.
* @param bool $isStudent
* Whether the ticket is for a student.
*/
public function addTicket($age, $isStudent) {
$this->TotalNum ++;
if ($age < 13){
$this->TotalPrice += self::CHILDREN;
} elseif ($age >= 65){
$this->TotalPrice += self::SENIOR_CITIZENS;
$this->GroupNum ++;
} elseif ($isStudent){
$this->TotalPrice += self::STUDENTS;
$this->GroupNum++;
} else {
$this->TotalPrice += self::GENERAL_ADMISSION;
$this->GroupNum ++;
}
}
/**
* Get the final price.
*
* @return float
* The total price of the transaction.
*/
public function finishPurchase() {
if ($this->TotalNum > 20){
if ($this->GroupNum >= 20){
if ($this->IsMovieDay){
$this->TotalPrice = $this->GroupNum * (self::GROUP - self::MOVIE_DAY) + ($this->TotalNum - $this->GroupNum) * self::CHILDREN;
} else {
$this->TotalPrice = $this->GroupNum * self::GROUP + ($this->TotalNum - $this->GroupNum) * self::CHILDREN;
}
} else {
if ($this->IsMovieDay){
$OtherPrice = $this->GroupNum * (self::GROUP - self::MOVIE_DAY) + ($this->TotalNum - $this->GroupNum - 20 + $this->GroupNum) * self::CHILDREN;
} else {
$OtherPrice = 20 * self::GROUP + ($this->TotalNum - $this->GroupNum - 20 + $this->GroupNum) * self::CHILDREN;
}
$this->TotalPrice = ($OtherPrice < $this->TotalPrice) ? $OtherPrice : $this->TotalPrice;
}
}
// echo 'xxxxxxxx'.($this->TotalNum - $this->GroupNum - 20 + $this->GroupNum).'xxxxx';
// echo 'ppppppGroupNum'.$this->GroupNum.'???????TotalNum'.$this->TotalNum.'----------TotalPrice'.$this->TotalPrice.'-----------Exceptions'.$this->Exceptions.'*********\n';
$this->TotalPrice += ($this->TotalNum * $this->Exceptions);
return $this->TotalPrice;
}
}
虽然思路上是面向对象,但实际上在写的时候我并没有完全按照规范去写,因为这毕竟只是一个小题目。
而且我在最后的判断中依旧是有问题的,因为会有一个更复杂的比较方式,由于存在组团时就不能享受电影日的优惠这个条件,所以如果有一定量的儿童或老人时就会出现到底多少人组团表较合适,那这基本就是一个多元方程了,有种做奥数题的感觉=_=。(当然也可能是我想复杂了。。。)
- 在最开始进行测试时我忽略了附加条件中电影日的条件,所以在最后的finishPurchase()中又修改了一些代码,才通过了测试。
- 在测试文件的最后一个单元测试中我发现测试时要满足多次售票但不能重新新建对象,所以就在startPurchase() 中强制初始化了部分参数。
- 不幸的是最后我依旧没能通过其中两个测试,分别是:
public function test2DGroupOfKidsWithTwoAdults() {
$tickets = [];
for ($i = 0; $i < 24; $i++) {
$tickets[] = [12, FALSE];
}
$tickets[] = [45, FALSE];
$tickets[] = [27, FALSE];
$result = $this->calc(72, DaysOfWeek::FRI, TRUE, FALSE, $tickets);
$this->assertEquals(144.0, $result, "2D, group of kids with two adults");
}
public function testOverlengthLoge3DMovieDayGroup() {
$tickets = [];
for ($i = 0; $i < 5; $i++) {
$tickets[] = [12, FALSE];
}
for ($i = 0; $i < 7; $i++) {
$tickets[] = [45, FALSE];
}
for ($i = 0; $i < 4; $i++) {
$tickets[] = [75, FALSE];
}
for ($i = 0; $i < 8; $i++) {
$tickets[] = [27, TRUE];
}
$result = $this->calc(125, DaysOfWeek::THU, FALSE, TRUE, $tickets);
$this->assertEquals(297.5, $result, "overlength, loge, 3D, movie-day group");
}
- 从第一个测试函数来看是24个儿童与两个成年人,没有任何附加的票价。那么这就是一个需要比较两种算法的测试了。
=>第一种:不组团。245.5+211=154
=>第二种:组团。206+65.5=153
那么第二种方法便宜了1元。但测试中却显示最低票价可以为144元。这就难办了,到底怎么才能达到144的票价呢?难不成是儿童与成人都买团购票?很明显这不是最佳选项。 - 而第二测试函数是5个儿童,7个普通人,4个老人,8个学生,附加条件基本都有了而且是电影日存在优惠。那这个就属于我没完善的部分了,因为不组团肯定是最贵的要300.5。而如果组团就存在三类人了,普通团员单价12.5,单独老人单价11.5,单独儿童单价10。那团员至少有15人,而剩下的5人要在老人与儿童中挑选,所以最佳比例还需要再做计算,但由于老人比较贵所以老人都要入团,而儿童也要有一个入团才能团购。所以最后票价为290。这比测试文件中的答案297.5少。
答案的思路
下面是通过所有测试的答案:
runtime = $runtime;
$this->day = $day;
$this->isParquet = $isParquet;
$this->is3D = $is3D;
}
/**
* Adds a ticket to the transaction.
*
* @param int $age
* The age of the ticket holder.
* @param bool $isStudent
* Whether the ticket is for a student.
*/
public function addTicket($age, $isStudent) {
$this->queue[] = ['age' => $age, 'isStudent' => $isStudent];
}
/**
* Get the final price.
*
* @return float
* The total price of the transaction.
*/
public function finishPurchase() {
$price = 0;
$count = count($this->queue);
$this->isGroup = $count >= 20 ? true : false;
foreach ($this->queue as $ticket) {
if ($ticket['age'] < 13) {
$price += $this->rateChildren;
} elseif ($ticket['age'] >= 65) {
$price += $this->rateSenior;
} elseif ($this->isGroup) {
$price += $this->rateGroup;
} elseif ($ticket['isStudent']) {
$price += $this->rateStudents;
} else {
$price += $this->rateNormal;
}
}
if ($this->is3D) {
$price += $count * $this->fee3D;
}
if (!$this->isParquet) {
$price += $count * $this->feeLoge;
}
if ($this->runtime > 120) {
$price += $count * $this->feeOverLength;
}
if ($this->day === 4 && !$this->isGroup) {
$price += $count * $this->feeMovieDay;
}
if (in_array($this->day, array(0, 6)))
$price += $count * $this->feeWeekends;
// clean register queue after purchase
$this->queue = [];
return $price;
}
}
答案中startPurchase()用来给附加项初始化赋值。addTicket()用来给所有的票这个大数组赋值。finishPurchase()则是具体的处理逻辑与清空大数组。
- 从finishPurchase()中可以看出,他是计数然后判断是不是达到组团要求,再进行年龄的判断并统计目前的票价,最后再进行各种附加项的判断与赋值。这是明显的面向过程的编程思路,一步步的进行逻辑判断与赋值。
- 但是如果仔细看就会发现他在年龄与是否满足组团条件上与我有些不同。他是直接判断年龄然后再直接判断是否满足组团条件,而我则是判断完所有的年龄后,单独进行组团条件的判断。
- 而且他的附加条件中电影日的判断也是一刀切的做法。
这样就造成了票价的不同。按照他的做法我第一个没通过的单元测试的逻辑过程是这样的。先来的24个儿童全部都是5.5,后来的两个成人则是6,结果票价为5.524+26=144和测试文件中一样。同样第二个单元测试则是55.5+196+24*(3+2+1.5)=297.5。
最后
所以从两种思路上来看都没有什么问题,我感觉OOP注重思考问题的本质,而面向过程则注重解决问题的逻辑。
虽然答案通过了测试,但个人感觉这样或许并不合理。而且如果以后是基于单元测试进行编程,那么单元测试部分必须完全正确,不然产生了歧义的话就会造成编码的反复修改,丧失了敏捷开发的本意。不过,讲道理一般TDD都是自己负责写单元测试的代码,应该不太会产生歧义。。。。。。