實(shí)習(xí)第五天CTO給我們分享了一個(gè)TDD的面試題目的答案,讓我們?cè)u(píng)價(jià)一下螟炫。在大家挑毛病的時(shí)候波附,我發(fā)現(xiàn)自己有很多以前從沒(méi)注意過(guò)的地方,比方PSR規(guī)范昼钻,命名規(guī)則等等掸屡。
題目:https://github.com/joelpittet/phpinterview
答案:https://github.com/tsuijie/phpinterview
今天自己在嘗試解答這道題目時(shí)發(fā)現(xiàn),有兩個(gè)測(cè)試怎么也通過(guò)不了然评。搞了很長(zhǎng)時(shí)間仅财,還是不行,只好借鑒一下答案+_+沾瓦,不過(guò)我發(fā)現(xiàn)答案和我完全就是兩種看待問(wèn)題的思路满着,一個(gè)是面向過(guò)程谦炒,另一個(gè)則是面向?qū)ο笕ニ伎肌6椅艺J(rèn)為兩種做法都能解釋的通风喇。
題目
題目是一個(gè)電影院售票宁改,如何讓票價(jià)最便宜的問(wèn)題,講道理放在高中就是一道稍微麻煩點(diǎn)的應(yīng)用題魂莫,只不過(guò)現(xiàn)在扯上了編程还蹲。
- 售票規(guī)則:
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
- 測(cè)試文件:
<?php
namespace Kata\Tests;
use Kata\DaysOfWeek;
use Kata\Movie;
class MovieTest extends \PHPUnit_Framework_TestCase {
/**
* Data provider for movies.
*
* @return array
*/
public function movieProvider() {
return [
[[72, DaysOfWeek::TUE, TRUE, FALSE], 44.0, "2D, parquet, Tuesday"],
[[72, DaysOfWeek::TUE, TRUE, TRUE], 56.0, "3D, parquet, Tuesday"],
[[72, DaysOfWeek::TUE, FALSE, TRUE], 64.0, "3D, loge, Tuesday"],
[[120, DaysOfWeek::THU, FALSE, TRUE], 56.0, "3D, loge, movie day"],
[[72, DaysOfWeek::SAT, FALSE, TRUE], 70.0, "3D, loge, Saturday"],
[[72, DaysOfWeek::SUN, FALSE, TRUE], 70.0, "3D, loge, Sunday"],
[[145, DaysOfWeek::WED, TRUE, FALSE], 50.0, "overlength, 2D, parquet, Wednesday"],
];
}
/**
* Tests middle age non student buyers.
*
* @dataProvider movieProvider
*/
public function testMiddleAgeNonStudents($movie, $expectedResult, $message) {
$tickets = [[35, FALSE], [35, FALSE], [35, FALSE], [35, FALSE]];
$movie[] = $tickets;
$result = call_user_func_array([$this, 'calc'], $movie);
$this->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");
}
}
我的思路
售票單價(jià)中有5種基于年齡,是否是學(xué)生和是否團(tuán)購(gòu)來(lái)判斷的價(jià)位耙考,其中兒童票最便宜谜喊,普通票最貴。
那么就是購(gòu)票人是一個(gè)對(duì)象倦始,其中年齡斗遏,票價(jià),是否是學(xué)生鞋邑,是否團(tuán)購(gòu)是他的屬性诵次。
由于單次售票過(guò)程中附加條件都是一樣的,所以屬于對(duì)象的公共變量枚碗。只有當(dāng)團(tuán)購(gòu)而且是電影日時(shí)不同逾一。
- 那么在Movie類(lèi)中就的startPurchase()方法就可以用來(lái)初始化和附加條件判斷。addTicket()方法用來(lái)統(tǒng)計(jì)人數(shù)與當(dāng)前的總票價(jià)肮雨。finishPurchase()方法用來(lái)最后的票價(jià)檢查遵堵,檢測(cè)是否有更便宜的購(gòu)票方式。
- 由于兒童票最便宜怨规,團(tuán)購(gòu)票次之陌宿,所以肯定是兒童票越多越好,而且由于年齡是固有屬性所以?xún)和华?dú)立出來(lái)椅亚。當(dāng)人數(shù)大于20時(shí)限番,就有了團(tuán)購(gòu)的可能性,但當(dāng)有兒童存在時(shí)團(tuán)購(gòu)票價(jià)不一定就比較低呀舔,所以要在最后進(jìn)行判斷到底組不組團(tuán)弥虐。
- 我的代碼:
<?php
namespace Kata;
/**
* Class Movie.
*/
class Movie {
const CHILDREN = 5.5;
const SENIOR_CITIZENS = 6.0;
const STUDENTS = 8.0;
const GENERAL_ADMISSION = 11.0;
const GROUP = 6.0;
const IS_3D_MOVIE = 3.0;
const OverLength = 1.5;
const MOVIE_DAY = -2.0;
const WEEKENDS = 1.5;
const LOGE = 2.0;
protected $TotalPrice = 0;
protected $GroupNum = 0;
protected $TotalNum = 0;
protected $Exceptions = 0;
protected $IsMovieDay = false;
/**
* Begins a transaction.
*
* @param int $runtime
* The minutes of the user.
* @param int $day
* The integer value from DaysOfWeek constants.
* @param bool $isParquet
* Is either parquet or lode.
* @param bool $is3D
* Is the film in 3D or 2D.
*/
public function startPurchase($runtime, $day, $isParquet, $is3D) {
$this->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;
}
}
雖然思路上是面向?qū)ο螅珜?shí)際上在寫(xiě)的時(shí)候我并沒(méi)有完全按照規(guī)范去寫(xiě)媚赖,因?yàn)檫@畢竟只是一個(gè)小題目霜瘪。
而且我在最后的判斷中依舊是有問(wèn)題的,因?yàn)闀?huì)有一個(gè)更復(fù)雜的比較方式惧磺,由于存在組團(tuán)時(shí)就不能享受電影日的優(yōu)惠這個(gè)條件颖对,所以如果有一定量的兒童或老人時(shí)就會(huì)出現(xiàn)到底多少人組團(tuán)表較合適,那這基本就是一個(gè)多元方程了磨隘,有種做奧數(shù)題的感覺(jué)=_=缤底。(當(dāng)然也可能是我想復(fù)雜了顾患。。个唧。)
- 在最開(kāi)始進(jìn)行測(cè)試時(shí)我忽略了附加條件中電影日的條件江解,所以在最后的finishPurchase()中又修改了一些代碼,才通過(guò)了測(cè)試徙歼。
- 在測(cè)試文件的最后一個(gè)單元測(cè)試中我發(fā)現(xiàn)測(cè)試時(shí)要滿(mǎn)足多次售票但不能重新新建對(duì)象犁河,所以就在startPurchase() 中強(qiáng)制初始化了部分參數(shù)。
- 不幸的是最后我依舊沒(méi)能通過(guò)其中兩個(gè)測(cè)試魄梯,分別是:
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");
}
- 從第一個(gè)測(cè)試函數(shù)來(lái)看是24個(gè)兒童與兩個(gè)成年人桨螺,沒(méi)有任何附加的票價(jià)。那么這就是一個(gè)需要比較兩種算法的測(cè)試了酿秸。
=>第一種:不組團(tuán)灭翔。245.5+211=154
=>第二種:組團(tuán)。206+65.5=153
那么第二種方法便宜了1元允扇。但測(cè)試中卻顯示最低票價(jià)可以為144元缠局。這就難辦了,到底怎么才能達(dá)到144的票價(jià)呢考润?難不成是兒童與成人都買(mǎi)團(tuán)購(gòu)票?很明顯這不是最佳選項(xiàng)读处。 - 而第二測(cè)試函數(shù)是5個(gè)兒童糊治,7個(gè)普通人,4個(gè)老人罚舱,8個(gè)學(xué)生井辜,附加條件基本都有了而且是電影日存在優(yōu)惠。那這個(gè)就屬于我沒(méi)完善的部分了管闷,因?yàn)椴唤M團(tuán)肯定是最貴的要300.5粥脚。而如果組團(tuán)就存在三類(lèi)人了,普通團(tuán)員單價(jià)12.5包个,單獨(dú)老人單價(jià)11.5刷允,單獨(dú)兒童單價(jià)10。那團(tuán)員至少有15人碧囊,而剩下的5人要在老人與兒童中挑選树灶,所以最佳比例還需要再做計(jì)算,但由于老人比較貴所以老人都要入團(tuán)糯而,而兒童也要有一個(gè)入團(tuán)才能團(tuán)購(gòu)天通。所以最后票價(jià)為290。這比測(cè)試文件中的答案297.5少熄驼。
答案的思路
下面是通過(guò)所有測(cè)試的答案:
<?php
namespace Kata;
/**
* Class Movie.
*/
class Movie {
private $runtime;
private $day;
private $isParquet;
private $is3D;
private $isGroup;
private $queue = array();
private $fee3D = 3.00;
private $feeOverLength = 1.50;
private $feeMovieDay = -2.00;
private $feeWeekends = 1.50;
private $feeLoge = 2.00;
private $rateChildren = 5.50;
private $rateSenior = 6.00;
private $rateGroup = 6.00;
private $rateStudents = 8.00;
private $rateNormal = 11.00;
/**
* Begins a transaction.
*
* @param int $runtime
* The minutes of the user.
* @param int $day
* The integer value from DaysOfWeek constants.
* @param bool $isParquet
* Is either parquet or lode.
* @param bool $is3D
* Is the film in 3D or 2D.
*/
public function startPurchase($runtime, $day, $isParquet, $is3D) {
$this->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()用來(lái)給附加項(xiàng)初始化賦值。addTicket()用來(lái)給所有的票這個(gè)大數(shù)組賦值豌骏。finishPurchase()則是具體的處理邏輯與清空大數(shù)組妒潭。
- 從finishPurchase()中可以看出,他是計(jì)數(shù)然后判斷是不是達(dá)到組團(tuán)要求吴叶,再進(jìn)行年齡的判斷并統(tǒng)計(jì)目前的票價(jià),最后再進(jìn)行各種附加項(xiàng)的判斷與賦值序臂。這是明顯的面向過(guò)程的編程思路蚌卤,一步步的進(jìn)行邏輯判斷與賦值。
- 但是如果仔細(xì)看就會(huì)發(fā)現(xiàn)他在年齡與是否滿(mǎn)足組團(tuán)條件上與我有些不同奥秆。他是直接判斷年齡然后再直接判斷是否滿(mǎn)足組團(tuán)條件逊彭,而我則是判斷完所有的年齡后,單獨(dú)進(jìn)行組團(tuán)條件的判斷构订。
- 而且他的附加條件中電影日的判斷也是一刀切的做法侮叮。
這樣就造成了票價(jià)的不同。按照他的做法我第一個(gè)沒(méi)通過(guò)的單元測(cè)試的邏輯過(guò)程是這樣的悼瘾。先來(lái)的24個(gè)兒童全部都是5.5囊榜,后來(lái)的兩個(gè)成人則是6,結(jié)果票價(jià)為5.524+26=144和測(cè)試文件中一樣亥宿。同樣第二個(gè)單元測(cè)試則是55.5+196+24*(3+2+1.5)=297.5卸勺。
最后
所以從兩種思路上來(lái)看都沒(méi)有什么問(wèn)題,我感覺(jué)OOP注重思考問(wèn)題的本質(zhì)烫扼,而面向過(guò)程則注重解決問(wèn)題的邏輯曙求。
雖然答案通過(guò)了測(cè)試,但個(gè)人感覺(jué)這樣或許并不合理映企。而且如果以后是基于單元測(cè)試進(jìn)行編程悟狱,那么單元測(cè)試部分必須完全正確,不然產(chǎn)生了歧義的話(huà)就會(huì)造成編碼的反復(fù)修改堰氓,喪失了敏捷開(kāi)發(fā)的本意挤渐。不過(guò),講道理一般TDD都是自己負(fù)責(zé)寫(xiě)單元測(cè)試的代碼双絮,應(yīng)該不太會(huì)產(chǎn)生歧義浴麻。。掷邦。白胀。。抚岗。