楼主: AlexQin

《 笨方法學 Ruby 》(Learn Ruby The Hard Way)

[复制链接]
论坛徽章:
1056
紫蜘蛛
日期:2015-09-22 15:53:22紫蜘蛛
日期:2015-10-15 13:48:52紫蜘蛛
日期:2015-10-15 14:45:48紫蜘蛛
日期:2015-10-15 14:47:47紫蜘蛛
日期:2015-10-15 14:48:45九尾狐狸
日期:2015-09-22 15:53:22九尾狐狸
日期:2015-10-15 13:50:37九尾狐狸
日期:2015-10-15 14:45:48九尾狐狸
日期:2015-10-15 14:47:47九尾狐狸
日期:2015-10-15 14:48:45
51#
 楼主| 发表于 2013-11-22 09:14 | 只看该作者
習題 42: 物以類聚

雖說將函式放到 Hash 裡是很有趣的一件事情,你應該也會想到「如果 Ruby 內建這件事情該多好」。事實上也的確有,那就是 class 這個關鍵字。你可以使用 class 創建更棒的 「函式 Hash」,比你在上節練習中做的強大多了。Class(類)有著各種各樣強​​大的功能和用法,但本書不會深入講這些內容,在這裡,你只要你學會把它們當作高級的「函式字典」使用就可以了。

用到「class」的程式語言被稱作「Object Oriented Programming(面向對象編程式語言」。這是一種傳統的寫程式的方式,你需要做出「東西」來,然後你「告訴」這些東西去完成它們的工作。類似的事情你其實已經做過不少了,只不過還沒有意識到而已。記得你做過的這個吧:

  1. stuff = ['Test', 'This', 'Out']
  2. puts stuff.join(' ')
复制代码

其實你這裡已經使用了 class。 stuff 這個變數其實是一個 Array Class。而 stuff.join() 呼叫了 Array 函式中的 join,然後傳遞了字串 ' '(就是一個空格),這也是一個 class —— 它是一個 String class (字符串類)。到處都是 class!

其實你這裡已經使用了 class。stuff這個變量其實是一個list class(列表類)。而’ ‘.join(stuff)裡調用函式join的字符串’ ‘(就是一個空格)也是一個class ——它是一個string class (字符串類)。到處都是class!

還有一個概念是 object(物件),不過我們暫且不提。當你建立過幾個class 後就會學到了。怎樣建立class呢?和你建立 ROOMS Hash 的方法差不多,但其實更簡單:

  1. class TheThing
  2.   attr_reader :number

  3.   def initialize()
  4.     @number = 0
  5.   end

  6.   def some_function()
  7.     puts "I got called."
  8.   end

  9.   def add_me_up(more)
  10.     @number += more
  11.     return @number
  12.   end
  13. end

  14. # two different things
  15. a = TheThing.new
  16. b = TheThing.new

  17. a.some_function()
  18. b.some_function()

  19. puts a.add_me_up(20)
  20. puts a.add_me_up(20)
  21. puts b.add_me_up(30)
  22. puts b.add_me_up(30)

  23. puts a.number
  24. puts b.number
复制代码

看到了在 @number 前面的 @ 吧?這是一個實例變數 (instance variable)。每個在 TheThing 中你建立的實例都會擁有 @number 中自己的值。我們不能透過直接打 a.number 直接拿到 number。除非我們特別使用 attr_reader :number,宣告讓外界能存取資料。

若要讓 @number write-only,我們可以使用 attr_writer :number。為了讓它可以既可讀又可寫,我們可以使用 attr_accessor :number。Ruby 使用了這些優良的物件導向原則來封裝資料。

下來,看到 initialize 函式了嗎?這就是你為建立 class 設置內部變數的方式。你可以用以 @ 符號開頭的方式去設定它們。另外看到我們使用了add_me_up() 為你建立 number加值。後面你可以看到我們怎樣可以使用這種方法為數字加值,然後印出來。

Class 是很強大的東西,你應該好好讀讀相關的東西。盡可能多找些東西讀並且多多實驗。你其實知道它們該怎麼用,只要試試就知道了。其實我馬上就要去練吉他了,所以我不會讓你寫練習了。你將使用 class 寫一個練習。

接下來我們將把習題 41 的內容重寫一遍,不過這回我們將使用 class:

  1. class Game

  2.   def initialize(start)
  3.     @quips = [
  4.       "You died.  You kinda suck at this.",
  5.       "Nice job, you died ...jackass.",
  6.       "Such a luser.",
  7.       "I have a small puppy that's better at this."
  8.     ]
  9.     @start = start
  10.     puts "in init @start = " + @start.inspect
  11.   end

  12.   def prompt()
  13.     print "> "
  14.   end

  15.   def play()
  16.     puts "@start => " + @start.inspect
  17.     next_room = @start

  18.     while true
  19.       puts "\n--------"
  20.       room = method(next_room)
  21.       next_room = room.call()
  22.     end
  23.   end

  24.   def death()
  25.     puts @quips[rand(@quips.length())]
  26.     Process.exit(1)
  27.   end

  28.   def central_corridor()
  29.     puts "The Gothons of Planet Percal #25 have invaded your ship and destroyed"
  30.     puts "your entire crew.  You are the last surviving member and your last"
  31.     puts "mission is to get the neutron destruct bomb from the Weapons Armory,"
  32.     puts "put it in the bridge, and blow the ship up after getting into an "
  33.     puts "escape pod."
  34.     puts "\n"
  35.     puts "You're running down the central corridor to the Weapons Armory when"
  36.     puts "a Gothon jumps out, red scaly skin, dark grimy teeth, and evil clown costume"
  37.     puts "flowing around his hate filled body.  He's blocking the door to the"
  38.     puts "Armory and about to pull a weapon to blast you."

  39.     prompt()
  40.     action = gets.chomp()

  41.     if action == "shoot!"
  42.       puts "Quick on the draw you yank out your blaster and fire it at the Gothon."
  43.       puts "His clown costume is flowing and moving around his body, which throws"
  44.       puts "off your aim.  Your laser hits his costume but misses him entirely.  This"
  45.       puts "completely ruins his brand new costume his mother bought him, which"
  46.       puts "makes him fly into an insane rage and blast you repeatedly in the face until"
  47.       puts "you are dead.  Then he eats you."
  48.       return :death

  49.     elsif action == "dodge!"
  50.       puts "Like a world class boxer you dodge, weave, slip and slide right"
  51.       puts "as the Gothon's blaster cranks a laser past your head."
  52.       puts "In the middle of your artful dodge your foot slips and you"
  53.       puts "bang your head on the metal wall and pass out."
  54.       puts "You wake up shortly after only to die as the Gothon stomps on"
  55.       puts "your head and eats you."
  56.       return :death

  57.     elsif action == "tell a joke"
  58.       puts "Lucky for you they made you learn Gothon insults in the academy."
  59.       puts "You tell the one Gothon joke you know:"
  60.       puts "Lbhe zbgure vf fb sng, jura fur fvgf nebhaq gur ubhfr, fur fvgf nebhaq gur ubhfr."
  61.       puts "The Gothon stops, tries not to laugh, then busts out laughing and can't move."
  62.       puts "While he's laughing you run up and shoot him square in the head"
  63.       puts "putting him down, then jump through the Weapon Armory door."
  64.       return :laser_weapon_armory

  65.     else
  66.       puts "DOES NOT COMPUTE!"
  67.       return :central_corridor
  68.     end
  69.   end

  70.   def laser_weapon_armory()
  71.     puts "You do a dive roll into the Weapon Armory, crouch and scan the room"
  72.     puts "for more Gothons that might be hiding.  It's dead quiet, too quiet."
  73.     puts "You stand up and run to the far side of the room and find the"
  74.     puts "neutron bomb in its container.  There's a keypad lock on the box"
  75.     puts "and you need the code to get the bomb out.  If you get the code"
  76.     puts "wrong 10 times then the lock closes forever and you can't"
  77.     puts "get the bomb.  The code is 3 digits."
  78.     code = "%s%s%s" % [rand(9)+1, rand(9)+1, rand(9)+1]
  79.     print "[keypad]> "
  80.     guess = gets.chomp()
  81.     guesses = 0

  82.     while guess != code and guesses < 10
  83.       puts "BZZZZEDDD!"
  84.       guesses += 1
  85.       print "[keypad]> "
  86.       guess = gets.chomp()
  87.     end

  88.     if guess == code
  89.       puts "The container clicks open and the seal breaks, letting gas out."
  90.       puts "You grab the neutron bomb and run as fast as you can to the"
  91.       puts "bridge where you must place it in the right spot."
  92.       return :the_bridge
  93.     else
  94.       puts "The lock buzzes one last time and then you hear a sickening"
  95.       puts "melting sound as the mechanism is fused together."
  96.       puts "You decide to sit there, and finally the Gothons blow up the"
  97.       puts "ship from their ship and you die."
  98.       return :death
  99.     end
  100.   end

  101.   def the_bridge()
  102.     puts "You burst onto the Bridge with the netron destruct bomb"
  103.     puts "under your arm and surprise 5 Gothons who are trying to"
  104.     puts "take control of the ship.  Each of them has an even uglier"
  105.     puts "clown costume than the last.  They haven't pulled their"
  106.     puts "weapons out yet, as they see the active bomb under your"
  107.     puts "arm and don't want to set it off."

  108.     prompt()
  109.     action = gets.chomp()

  110.     if action == "throw the bomb"
  111.       puts "In a panic you throw the bomb at the group of Gothons"
  112.       puts "and make a leap for the door.  Right as you drop it a"
  113.       puts "Gothon shoots you right in the back killing you."
  114.       puts "As you die you see another Gothon frantically try to disarm"
  115.       puts "the bomb. You die knowing they will probably blow up when"
  116.       puts "it goes off."
  117.       return :death

  118.     elsif action == "slowly place the bomb"
  119.       puts "You point your blaster at the bomb under your arm"
  120.       puts "and the Gothons put their hands up and start to sweat."
  121.       puts "You inch backward to the door, open it, and then carefully"
  122.       puts "place the bomb on the floor, pointing your blaster at it."
  123.       puts "You then jump back through the door, punch the close button"
  124.       puts "and blast the lock so the Gothons can't get out."
  125.       puts "Now that the bomb is placed you run to the escape pod to"
  126.       puts "get off this tin can."
  127.       return :escape_pod
  128.     else
  129.       puts "DOES NOT COMPUTE!"
  130.       return :the_bridge
  131.     end
  132.   end

  133.   def escape_pod()
  134.     puts "You rush through the ship desperately trying to make it to"
  135.     puts "the escape pod before the whole ship explodes.  It seems like"
  136.     puts "hardly any Gothons are on the ship, so your run is clear of"
  137.     puts "interference.  You get to the chamber with the escape pods, and"
  138.     puts "now need to pick one to take.  Some of them could be damaged"
  139.     puts "but you don't have time to look.  There's 5 pods, which one"
  140.     puts "do you take?"

  141.     good_pod = rand(5)+1
  142.     print "[pod #]>"
  143.     guess = gets.chomp()

  144.     if guess.to_i != good_pod
  145.       puts "You jump into pod %s and hit the eject button." % guess
  146.       puts "The pod escapes out into the void of space, then"
  147.       puts "implodes as the hull ruptures, crushing your body"
  148.       puts "into jam jelly."
  149.       return :death
  150.     else
  151.       puts "You jump into pod %s and hit the eject button." % guess
  152.       puts "The pod easily slides out into space heading to"
  153.       puts "the planet below.  As it flies to the planet, you look"
  154.       puts "back and see your ship implode then explode like a"
  155.       puts "bright star, taking out the Gothon ship at the same"
  156.       puts "time.  You won!"
  157.       Process.exit(0)
  158.     end
  159.   end
  160. end

  161. a_game = Game.new(:central_corridor)
  162. a_game.play()
复制代码

你應該看到的結果

這個版本的遊戲和你的上一版效果應該是一樣的,其實有些代碼都幾乎一樣。比較一下兩版程式碼,弄懂其中不同的地方,重點在需要理解這些東西:

  • 怎樣建立一個 class Game 並且放函式到裡面去。
  • initialize 是一個特殊的初始方法,怎樣預設重要的變數在裡面。
  • 你如何透過將在 class 下這個關鍵字再巢狀排列這些定義的方式為class 添加函式。
  • 你如何透過在名稱底下加進巢狀內容來添加函式的。
  • @ 的概念,還有它在 initialize、play 和 death 是怎樣被使用的。
  • 最後我們怎樣建立了一個 Game,然後透過 play()讓所有的東西運行起來。


加分習題


  • 研究一下dict是什麼東西,應該怎樣使用。
  • 再為遊戲添加一些房間,確認自己已經學會使用class 。
  • 創建一個新版本,裡邊使用兩個class,其中一個是Map,另一個是Engine。提示:把play放到Engine裡面。




使用道具 举报

回复
论坛徽章:
1056
紫蜘蛛
日期:2015-09-22 15:53:22紫蜘蛛
日期:2015-10-15 13:48:52紫蜘蛛
日期:2015-10-15 14:45:48紫蜘蛛
日期:2015-10-15 14:47:47紫蜘蛛
日期:2015-10-15 14:48:45九尾狐狸
日期:2015-09-22 15:53:22九尾狐狸
日期:2015-10-15 13:50:37九尾狐狸
日期:2015-10-15 14:45:48九尾狐狸
日期:2015-10-15 14:47:47九尾狐狸
日期:2015-10-15 14:48:45
52#
 楼主| 发表于 2013-11-22 09:15 | 只看该作者
本帖最后由 AlexQin 于 2013-11-22 09:16 编辑

習題 43: 你來製作一個遊戲

你要開始學會自食其力了。通過閱讀這本書你應該已經學到了一點,那就是你需要的所有的資訊網路上都有,你只要去搜尋就能找到。唯一困擾你的就是如何使用正確的詞彙進行搜尋。學到現在,你在挑選搜尋關鍵字方面應該已經有些感覺了。現在已經是時候了,你需要嘗試寫一個大的專案,並讓它運行起來。

以下是你的需求:

  • 製作一個截然不同的遊戲。
  • 使用多個檔案,並使用 require呼叫這些檔案。確認自己知道 require的用法。
  • 對於每個房間使用一個 class,class 的命名要能體現出它的用處。例如GoldRoom、KoiPondRoom。
  • 你的執行器程式碼應該了解這些房間,所以創建一個 class 來呼叫並且記錄這些房間。有很多種方法可以達到這個目的,不過你可以考慮讓每個房間傳回下一個房間,或者設置一個變數,讓它指定下一個房間是什麼。


其他的事情就全靠你了。花一個星期完成這件任務,做一個你能做出來的最好的遊戲。使用你學過的任何東西(類,函數,Hash,陣列……)來改進你的程式。這節課的目的是教你如何構建 class 出來,而這些 class 又能調用到其它 Ruby 檔案中的 class。

我不會詳細地告訴你告訴你怎樣做,你需要自己完成。試著下手吧,寫程式就是解決問題的過程,這就意味著你要嘗試各種可能性,進行實驗,經歷失敗,然後丟掉你做出來的東西重頭開始。當你被某個問題卡住的時候,你可以向別人尋求幫助,並把你的程式貼出來給他們看。如果有人刻薄你,別理他們,你只要集中精力在幫你的人身上就可以了。持續修改和清理你的程式碼,直到它完整可執行為止,然後再研究一下看它還能不能被改進。

祝你好運,下個星期你做出遊戲後我們再見。

使用道具 举报

回复
论坛徽章:
1056
紫蜘蛛
日期:2015-09-22 15:53:22紫蜘蛛
日期:2015-10-15 13:48:52紫蜘蛛
日期:2015-10-15 14:45:48紫蜘蛛
日期:2015-10-15 14:47:47紫蜘蛛
日期:2015-10-15 14:48:45九尾狐狸
日期:2015-09-22 15:53:22九尾狐狸
日期:2015-10-15 13:50:37九尾狐狸
日期:2015-10-15 14:45:48九尾狐狸
日期:2015-10-15 14:47:47九尾狐狸
日期:2015-10-15 14:48:45
53#
 楼主| 发表于 2013-11-22 09:17 | 只看该作者

習題 44: 評估你的遊戲

這節練習的目的是檢查評估你的遊戲。也許你只完成了一半,卡在那裡沒有進行下去,也許你勉強做出來了。不管怎樣,我們將串一下你應該弄懂的一些東西,並確認你的遊戲裡有使用到它們。我們將學習如何用正確的格式構建class,使用class 的一些通用習慣,另外還有很多的「書本知識」讓你學習。

為什麼我會讓你先行嘗試,然後才告訴你正確的做法呢?因為從現在開始你要學會「自給自足」,以前是我牽著你前行,以後就得靠你自己了。後面的習題我只會告訴你你的任務,你需要自己去完成,在你完成後我再告訴你如何可以改進你的作業。

一開始你會覺得很困難並且很不習慣,但只要堅持下去,你就會培養出自己解決問題的能力。你還會找出創新的方法解決問題,這比從課本中拷貝解決方案強多了。

函式的風格

以前我教過的怎樣寫好函式的方法一樣是適用的,不過這裡要添加幾條:

  • 由於各種各樣的原因,程序員將 class (類)裡邊的函式稱作method(方法)。很大程度上這只是個市場策略(用來推銷OOP),不過如果你把它們稱作「函式」的話,是會有囉嗦的人跳出來糾正你的。如果你覺得他們太煩了,你可以告訴他們從數學方面示範一下「函式」和「方法」究竟有什麼不同,這樣他們會很快閉嘴的。
  • 在你使用class的過程中,很大一部分時間是告訴你的 class如何「做事情」。給這些函式命名的時候,與其命名成一個名詞,不如命名為一個動詞,作為給class的一個命令。就和陣列中 的 pop 函式一樣,它相當於說:「嘿,陣列,把這東西給我 pop出去。」它的名字不是 remove_from_end_of_list,因為即使它的功能的確是這樣,這一個字串也不是一個命令。
  • 讓你的函式保持簡單小巧。由於某些原因,有些人開始學習 class 後就會忘了這一條。


Classh (類) 的風格

  • 你的 class 應該使用「camel case(駝峰式大小寫)」,例如你應該使用SuperGoldFactory 而不是 super_gold_factory
  • 你的 initialize 不應該做太多的事情,這會讓 class 變得難以使用。
  • 你的其它函式應該使用「underscore format(下劃線隔詞)」,所以你可以寫my_awesome_hair,而不是 myawesomehair 或者 MyAwesomeHair。
  • 用一致的方式組織函式的參數。如果你的 class 需要處理 users、dogs、和cats,就保持這個次序(特別情況除外)。如果一個函式的參數是(dog, cat, user),另一個的是(user, cat, dog) ,這會讓函式使用起來很困難。
  • 不要對全局變數或者來自模組的變數進行重定義或者賦值,讓這些東西自顧自就行了。
  • 不要一根筋式地維持風格一致性,這是思維力底下的妖怪嘍囉做的事情。一致性是好事情,不過愚蠢地跟著別人遵從一些白痴口號是錯誤的行為——這本身就是一種壞的風格。好好為自己著想吧。


程式碼風格

  • 為了以方便他人閱讀,在自己的程式碼之間留下一些空白。你將會看到一些很差的程式設計師,他們寫的程式碼還算通順,但程式碼之間沒有任何空間。這種風格在任何程式語言中都是壞習慣,人的眼睛和大腦會通過空白和垂直對齊的位置來掃描和區隔視覺元素,如果你的程式碼裡沒有任何空白,這相當於為你的程式碼上了迷彩裝。
  • 如果一段程式碼你無法朗讀出來,那麼這段程式碼的可讀性可能就有問題。如你找不到讓某個東西易用的方法,試著也朗讀出來。這樣不僅會逼迫你慢速而且真正仔細閱讀過去,還會幫你找到難讀的段落,從而知道那些程式碼的易讀性需要作出改進。
  • 學著模仿別人的風格寫程式,直到哪天你找到你自己的風格為止。
  • 一旦你有了自己的風格,也別把它太當回事。程式設計師工作的一部分就是和別人的程式碼打交道,有的人審美觀就是很差。相信我,你的審美觀某一方面一定也很差,只是你從未意識到而已。
  • 如果你發現有人寫的程式碼風格你很喜歡,那就模仿他們的風格。


好的註釋

  • 有程序員會告訴你,說你的程式碼需要有足夠的可讀性,這樣你就無需寫註釋了。他們會以自己接近官腔的聲音說「所以你永遠都不應該寫程式碼註釋。」這些人要嘛是一些顧問型的人物,如果別人無法使用他們的程式碼,就會付更多錢給他們讓他們解決問題。要嘛他們能力不足,從來沒有跟別人合作過。別理會這些人,好好寫你的註解。
  • 寫註解的時候,描述清楚為什麼你要這樣做。程式碼只會告訴你「這樣實現」,而不會告訴你「為什麼要這樣實現」,而後者比前者更重要。
  • 當你為函式寫文件註解的時候,記得為別的程式碼使用者也寫些東西。你不需要狂寫一大堆,但一兩句話謝謝這個函式的用法還是很有用的。
  • 最後要說的是,雖然註解是好東西,太多的註解就不見得是了。而且註解也是需要維護的,你要盡量讓註解短小精悍一語中的,如果你對程式碼做了更改,記得檢查並更新相關的註解,確認它們還是正確的。


評估你的遊戲

現在我要求你假裝成是我,板起臉來,把你的程式碼打印出來,然後拿一支紅筆,把程式碼中所有的錯誤都標出來。你要充分利用你在本​​章以及前面學到的知識。等你批改完了,我要求你把所有的錯誤改對。這個過程我需要你多重複幾次,爭取找到更多的可以改進的地方。使用我前面教過的方法,把程式碼分解成最細小的單元一一進行分析。

這節練習的目的是訓練你對於細節的關注程度。等你檢查完自己的程式碼,再找一段別人的程式碼用這種方法檢查一遍。把程式碼打印出來,檢查出所有程式碼和風格方面的錯誤,然後試著在不改壞別人程式碼的前提下把它們修改正確。

這週我要求你的事情就是批改和糾錯,包含你自己的程式碼和別人的程式碼,再沒有別的了。這節習題難度還是挺大,不過一旦你完成了任務,你學過的東西就會牢牢記在腦中。

使用道具 举报

回复
论坛徽章:
1056
紫蜘蛛
日期:2015-09-22 15:53:22紫蜘蛛
日期:2015-10-15 13:48:52紫蜘蛛
日期:2015-10-15 14:45:48紫蜘蛛
日期:2015-10-15 14:47:47紫蜘蛛
日期:2015-10-15 14:48:45九尾狐狸
日期:2015-09-22 15:53:22九尾狐狸
日期:2015-10-15 13:50:37九尾狐狸
日期:2015-10-15 14:45:48九尾狐狸
日期:2015-10-15 14:47:47九尾狐狸
日期:2015-10-15 14:48:45
54#
 楼主| 发表于 2013-11-22 09:19 | 只看该作者
習題 45: 物件、類和從屬關係

有一個重要的概念你需要弄明白,那就是 Class「類」和 Object「物件」的區別。問題在於,class 和 object 並沒有真正的不同。它們其實是同樣的東西,只是在不同的時間名字不同罷了。我用禪語來解釋一下吧:

魚(Fish)和鮭魚(Salmon)有什麼區別?

這個問題有沒有讓你有點暈呢?說真的,坐下來想一分鐘。我的意思是說,魚和鮭魚是不一樣,不過它們其實也是一樣的是不是?泥鰍是魚的一種,所以說沒什麼不同,不過泥鰍又有些特別,它和別的種類的魚的確不一樣,比如鮭魚和比目魚就不一樣。所以鮭魚和魚既相同又不同。怪了。

這個問題讓人暈的原因是大部分人不會這樣去思考問題,其實每個人都懂這一點,你無須去思考魚和鮭魚的區別,因為你知道它們之間的關係。你知道鮭魚是魚的一種,而且魚還有別的種類,根本就沒必要去思考這類問題。

讓我們更進一步,假設你有一隻水桶,裡邊有三條鮭魚。假設你的好人卡多到沒地方用,於是你給它們分別取名叫Frank,Joe,Mary。現在想想這個問題:

Mary 和鮭魚有什麼區別?

這個問題一樣的奇怪,但比起魚和鮭魚的問題來還好點。你知道 Mary是一條鮭魚,所以他並沒什麼不同,他只是鮭魚的一個「實例(instance)」。Joe 和Frank 一樣也是鮭魚的實例。我的意思是說,它們是由鮭魚創建出來的,而且代表著和鮭魚一樣的屬性。

所以我們的思維方式是(你可能會有點不習慣):魚是一個「類(class)」,鮭魚是一個「類(class)」,而 Mary 是一個「物件(object)」。仔細想想,然後我再一點一點慢慢解釋給你。

魚是一個「類」,表示它不是一個真正的東西,而是一個用來描述具有同類屬性的實例的概括性詞彙。你有鰭?你有鰾?你住在水裡?好吧那你就是一條魚。

後來一個博士路過,看到你的水桶,於是告訴你:「小伙子,你這些魚是鮭魚。」 專家一出,真相即現。並且專家還定義了一個新的叫做​​「鮭魚」的「類」,而這個「類」又有它特定的屬性。長鼻子?紅肉?體型大?住在海裡或是乾淨新鮮的水裡?吃起來味道不錯?那你就是一條鮭魚。

最後一個廚師過來了,他跟博士說:「非也非也,你看到的是鮭魚,我看到的是Mary,而且我要把 Mary 淋上美味醬料做一道小菜。 」於是你就有了一隻叫做Mary 的鮭魚的「實例(instance)」(鮭魚也是魚的一個「實例」),並且你使用了它(把它塞到你的胃裡了),這樣它就是一個​​「物件(object)」。

這會你應該了解了:Mary 是鮭魚的成員,而鮭魚又是魚的成員。這裡的關係式:物件屬於某個類,而某個類又屬於另一個類。

寫成程式碼是什麼樣子

這個概念有點詭異,不過實話說,你只要在建立和使用class的時候操心一下就可以了。我來給你兩個區分 Class 和 Object的小技巧。

首先針對類和物件,你需要學會兩個說法,「is-a(是啥)」和「has-a(有啥)」。「是啥」要用在談論「兩者以類的關係互相關聯」的時候,而「有啥」要用在「兩者無共同點,僅是互為參照」的時候。

接下來,通讀這段程式碼,將每一個註解為##??的位置標明他是「is-a」還是「has-a」的關係,並講明白這個關係是什麼。在程式碼的開始我還舉了幾個例子,所以你只要寫剩下的就可以了。

記住,「是啥」指的是魚和鮭魚的關係,而「有啥」指的是鮭魚和烤肉架的關係。

  1. ## Animal is-a object (yes, sort of confusing) look at the extra credit
  2. class Animal

  3. end

  4. ## ??
  5. class Dog < Animal

  6.   def initialize(name)
  7.     ## ??
  8.     @name = name
  9.   end

  10. end

  11. ## ??
  12. class Cat < Animal

  13.   def initialize(name)
  14.     ## ??
  15.     @name = name
  16.   end

  17. end

  18. ## ??
  19. class Person

  20.   attr_accessor :pet

  21.   def initialize(name)
  22.     ## ??
  23.     @name = name

  24.     ## Person has-a pet of some kind
  25.     @pet = nil
  26.   end

  27. end
  28. ## ??
  29. class Employee < Person

  30.   def initialize(name, salary)
  31.     ## ?? hmm what is this strange magic?
  32.     super(name)
  33.     ## ??
  34.     @salary = salary
  35.   end

  36. end

  37. ## ??
  38. class Fish

  39. end

  40. ## ??
  41. class Salmon < Fish

  42. end

  43. ## ??
  44. class Halibut < Fish

  45. end

  46. ## rover is-a Dog
  47. rover = Dog.new("Rover")

  48. ## ??
  49. satan = Cat.new("Satan")

  50. ## ??
  51. mary = Person.new("Mary")

  52. ## ??
  53. mary.pet = satan

  54. ## ??
  55. frank = Employee.new("Frank", 120000)

  56. ## ??
  57. frank.pet = rover

  58. ## ??
  59. flipper = Fish.new

  60. ## ??
  61. crouse = Salmon.new

  62. ## ??
  63. harry = Halibut.new
复制代码


加分習題

  • 有沒有辦法把 Class 當作 Object 使用呢?
  • 在習題中為 animals、fish、還有people 添加一些函式,讓它們做一些事情。看看當函數在 Animal 這樣的「基類(base class)」裡和在 Dog 裡有什麼區別。
  • 找些別人的程式碼,理清裡邊的「是啥」和「有啥」的關係。
  • 使用 Array 和 Hash 建立一些新的一對應多的「has-many」的關係。
  • 你認為會有一種「has-many」的關係嗎?閱讀一下關於「多重繼承(multiple inheritance)」的資料,然後儘量避免這種用法。

使用道具 举报

回复
论坛徽章:
1056
紫蜘蛛
日期:2015-09-22 15:53:22紫蜘蛛
日期:2015-10-15 13:48:52紫蜘蛛
日期:2015-10-15 14:45:48紫蜘蛛
日期:2015-10-15 14:47:47紫蜘蛛
日期:2015-10-15 14:48:45九尾狐狸
日期:2015-09-22 15:53:22九尾狐狸
日期:2015-10-15 13:50:37九尾狐狸
日期:2015-10-15 14:45:48九尾狐狸
日期:2015-10-15 14:47:47九尾狐狸
日期:2015-10-15 14:48:45
55#
 楼主| 发表于 2013-11-24 11:44 | 只看该作者
習題 46: 一個專案骨架

這裡你將學會如何建立一個專案「骨架」目錄。這個骨架目錄具備讓專案跑起來的所有基本內容。它裡邊會包含你的專案檔案佈局、自動化測試程式碼,模組,以及安裝腳本。當你建立一個新專案的時候,只要把這個目錄複製過去,改改目錄的名字,再編輯裡面的檔案就行了。

骨架內容: Linux/OSX

首先使用下述命令創建你的骨架目錄:

  1. $ mkdir -p projects
  2. $ cd projects/
  3. $ mkdir skeleton
  4. $ cd skeleton
  5. $ mkdir bin lib lib/NAME test
复制代码

我使用了一個叫 projects 的目錄,用來存放我自己的各個專案。然後我在裡邊建立了一個叫做 skeleton 的檔案夾,這就是我們新專案的基礎目錄。其中叫做 NAME 的檔案夾是你的專案的主檔案夾,你可以將它任意取名。

接下來我們要配置一些初始檔案:

  1. $ touch lib/NAME.rb
  2. $ touch lib/NAME/version.rb
复制代码

然後我們可以建立一個 NAME.gemspec 的檔案在我們的專案的根目錄,這個檔案在安裝專案的時候我們會用到它:

  1. # -*- encoding: utf-8 -*-
  2. $:.push File.expand_path("../lib", __FILE__)
  3. require "NAME/version"

  4. Gem::Specification.new do |s|
  5.   s.name        = "NAME"
  6.   s.version     = NAME::VERSION
  7.   s.authors     = ["Rob Sobers"]
  8.   s.email       = ["rsobers@gmail.com"]
  9.   s.homepage    = ""
  10.   s.summary     = %q{TODO: Write a gem summary}
  11.   s.description = %q{TODO: Write a gem description}

  12.   s.rubyforge_project = "NAME"

  13.   s.files         = `git ls-files`.split("\n")
  14.   s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
  15.   s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
  16.   s.require_paths = ["lib"]
  17. end
复制代码

編輯這個檔案,把自己的聯絡方式寫進去,然後放到那裡就行了。

最後你需要一個簡單的測試專用(我們將會在下一節中提到 Test )的骨架檔案叫 test/test_NAME.rb:

  1. require 'test/unit'

  2. class MyUnitTests < Test::Unit::TestCase

  3.   def setup
  4.     puts "setup!"
  5.   end

  6.   def teardown
  7.     puts "teardown!"
  8.   end

  9.   def test_basic
  10.     puts "I RAN!"
  11.   end

  12. end
复制代码

安裝 Gems

Gems 是 Ruby 的套件系統,所以你需要知道怎麼安裝它和使用它。不過問題就來了。我的本意是讓這本書越清晰越乾淨越好,不過安裝軟體的方法是在是太多了,如果我要一步一步寫下來,那10 頁都寫不完,而且告訴你吧,我本來就是個懶人。

所以我不會提供詳細的安裝步驟了,我只會告訴你需要安裝哪些東西,然後讓你自己搞定。這對你也有好處,因為你將打開一個全新的世界,裡邊充滿了其他人發佈的軟體。

接下來你需要安裝下面的軟體套件:



不要只是手動下載並且安裝這些軟體套件,你應該看一下別人的建議,尤其看看針對你的操作系統別人是怎樣建議你安裝和使用的。同樣的軟體套件在不一樣的操作系統上面的安裝方式是不一樣的,不一樣版本的 Linux 和 OSX 會有不同,而 Windows 更是不同。

我要預先警告你,這個過程會是相當無趣。在業內我們將這種事情叫做「yak shaving(剃犛牛)」。它指的是在你做一件有意義的事情之前的一些準備工作,而這些準備工作又是及其無聊冗繁的。你要做一個很酷的 Ruby 專案,但是創建骨架目錄需要你安裝一些軟體到件,而安裝軟體套件之前你還要安裝package installer (軟件套件安裝工具),而要安裝這個工具你還得先學會如何在你的操作系統下安裝軟體,真是煩不勝煩呀。

無論如何,還是克服困難吧。你就把它當做進入程式俱樂部的一個考驗。每個程式設計師都會經歷這條道路,在每一段「酷」的背後總會有一段「煩」的。

使用這個骨架

剃犛牛的事情已經做的差不多了,以後每次你要新建一個專案時,只要做下面的事情就可以了:

  • 拷貝這份骨架目錄,把名字改成你新專案的名字。
  • 再將 NAME模組和 NAME.rb 更名為你需要的名字,它可以是你專案的名字,當然別的名字也行。
  • 編輯你的 NAME.gemspec 檔案,讓它包含你新專案的相關資訊。
  • 重命名 test/test_NAME.rb,讓它的名字匹配到你模組的名字。
  • 開始寫程式吧。


小測驗

這節練習沒有加分習題,不過需要你做一個小測驗:

  • 找文件閱讀,學會使用你前面安裝了的軟體套件。
  • 閱讀關於NAME.gemspec 的文件,看它裡邊可以做多少配置。
  • 建立一個專案,在 NAME.rb 裡寫一些程式碼。
  • 在 bin 目錄下放一個可以運行的腳本,找材料學習一下怎樣建立可以在系統下運行的 Ruby 腳本。
  • 確定你建立的 bin 教本,有在 NAME.gemspec 中被參照到,這這樣你安裝時就可以連它安裝進去。
  • 使用你的 NAME.gemspec 和 gem build、gem install 來安裝你寫的程式和確定它能用。然後使用 gem uninstall 去移除它。
  • 弄懂如何使用 Bundler 來自動建立一個骨架目錄。

使用道具 举报

回复
论坛徽章:
1056
紫蜘蛛
日期:2015-09-22 15:53:22紫蜘蛛
日期:2015-10-15 13:48:52紫蜘蛛
日期:2015-10-15 14:45:48紫蜘蛛
日期:2015-10-15 14:47:47紫蜘蛛
日期:2015-10-15 14:48:45九尾狐狸
日期:2015-09-22 15:53:22九尾狐狸
日期:2015-10-15 13:50:37九尾狐狸
日期:2015-10-15 14:45:48九尾狐狸
日期:2015-10-15 14:47:47九尾狐狸
日期:2015-10-15 14:48:45
56#
 楼主| 发表于 2013-11-24 11:49 | 只看该作者
習題 47: 自動化測試

為了確認遊戲的功能是否正常,你需要一遍一遍地在你的遊戲中輸入命令。這個過程是很枯燥無味的。如果能寫一小段程式碼用來測試你的程式碼豈不是更好?然後只要你對程序做了任何修改,或者添加了什麼新東西,你只要「跑一下你的測試」,而這些測試能確認程序依然能正確運行。這些自動測試不會抓到所有的bug,但可以讓你無需重複輸入命令運行你的程式碼,從而為你節約很多時間。

從這一章開始,以後的練習將不會有「你應該看到的結果」這一節,取而代之的是一個「你應該測試的東西」一節。從現在開始,你需要為自己寫的所有程式碼寫自動化測試,而這將讓你成為一個更好的程序員。

我不會試圖解釋為什麼你需要寫自動化測試。我要告訴你的是,你想要成為一個程式設計師,而程序的作用是讓無聊冗繁的工作自動化,測試軟件毫無疑問是無聊冗繁的,所以你還是寫點程式碼讓它為你測試的更好。

這應該是你需要的所有的解釋了。因為你寫單元測試的原因是讓你的大腦更加強健。你讀了這本書,寫了很多程式碼讓它們實現一些事情。現在你將更進一步,寫出懂得你寫的其他程式碼的程式碼。這個寫程式碼測試你寫的其他程式碼的過程將強迫你清楚的理解你之前寫的程式碼。這會讓你更清晰地了解你寫的程式碼實現的功能及其原理,而且讓你對細節的注意更上一個台階。

撰寫 Test Case

我們將拿一段非常簡單的程式碼為例,寫一個簡單的測試,這個測試將建立在上節我們創建的項目骨架上面。

首先從你的專案骨架創建一個叫做 ex47 的專案。確認該改名稱的地方都有改過,尤其是 tests/ex47_tests.rb 這處不要寫錯。

接下來建立一個簡單的 ex47/lib/game.rb 檔案,裡邊放一些用來被測試的程式碼。我們現在放一個傻乎乎的小 class 進去,用來作為我們的測試對象:

  1. class Room

  2.   attr_accessor :name, :description, :paths

  3.   def initialize(name, description)
  4.     @name = name
  5.     @description = description
  6.     @paths = {}
  7.   end

  8.   def go(direction)
  9.     @paths[direction]
  10.   end

  11.   def add_paths(paths)
  12.     @paths.update(paths)
  13.   end

  14. end
复制代码

一旦你有了這個檔案,修改你的 unit test 骨架變成這樣:

  1. require 'test/unit'
  2. require_relative '../lib/ex47'

  3. class MyUnitTests < Test::Unit::TestCase

  4.   def test_room()
  5.     gold = Room.new("GoldRoom",
  6.                     """This room has gold in it you can grab. There's a
  7.                 door to the north.""")
  8.     assert_equal(gold.name, "GoldRoom")
  9.     assert_equal(gold.paths, {})
  10.   end

  11.   def test_room_paths()
  12.     center = Room.new("Center", "Test room in the center.")
  13.     north = Room.new("North", "Test room in the north.")
  14.     south = Room.new("South", "Test room in the south.")

  15.     center.add_paths({:north => north, :south => south})
  16.     assert_equal(center.go(:north), north)
  17.     assert_equal(center.go(:south), south)
  18.   end

  19.   def test_map()
  20.     start = Room.new("Start", "You can go west and down a hole.")
  21.     west = Room.new("Trees", "There are trees here, you can go east.")
  22.     down = Room.new("Dungeon", "It's dark down here, you can go up.")

  23.     start.add_paths({:west => west, :down => down})
  24.     west.add_paths({:east => start})
  25.     down.add_paths({:up => start})

  26.     assert_equal(start.go(:west), west)
  27.     assert_equal(start.go(:west).go(:east), start)
  28.     assert_equal(start.go(:down).go(:up), start)
  29.   end

  30. end
复制代码

這個文件 require 了你在 lib/ex47.rb 裡建立的 Room這個類,接下來我們要做的就是測試它。於是我們看到一系列的以 test_ 開頭的測試函式,它們就是所謂的「Test Case」,每一個Test Case裡面都有一小段程式碼,它們會建立一個或者一些房間,然後去確認房間的功能和你期望的是否一樣。它測試了基本的房間功能,然後測試了路徑,最後測試了整個地圖。

這裡最重要的函數時 assert_equal,它保證了你設置的變數,以及你在Room 裡設置的路徑和你的期望相符。如果你得到錯誤的結果的話,Ruby 的 Test::Unit 模組將會印出一個錯誤信息,這樣你就可以找到出錯的地方並且修正過來。

測試指南

在寫測試程式碼時,你可以照著下面這些不是很嚴格的指南來做:

  • 測試腳本要放到 tests/ 目錄下,並且命名為 test_NAME.rb。這樣做還有一個好處就是防止測試程式碼和別的程式碼互相混掉。
  • 為你的每一個模組寫一個測試。
  • Test Cast 函式保持簡短,但如果看上去不怎麼整潔也沒關係,Test Cast一般都有點亂。
  • 就算Test Cast有些亂,也要試著讓他們保持整潔,把裡邊重複的程式碼刪掉。建立一些輔助函數來避免重複的程式碼。當你下次在改完程式碼需要改測試的時候,你會感謝我這一條建議的。重複的程式碼會讓修改測試變得很難操作。
  • 最後一條是別太把測試當做一回事。有時候,更好的方法是把程式碼和測試全部刪掉,然後重新設計程式碼。


你應該看到的結果

  1. $ ruby test_ex47.rb
  2. Loaded suite test_ex47
  3. Started
  4. ...
  5. Finished in 0.000353 seconds.

  6. 3 tests, 7 assertions, 0 failures, 0 errors, 0 skips

  7. Test run options: --seed 63537
复制代码

如果一切都正确运行的话,那这就是你所看到的结果。尝试引入错误,看看结果会是什么样,然后去解决这些错误内容。

加分習題

  • 仔細閱讀 Test::Unit相關的文件,再去了解一下其他的替代方案。
  • 了解一下 Rspec,看看它是否可以幹得更出色。
  • 改進你遊戲裡的 Room,然後用它重建你的遊戲。這次重寫,你需要一邊寫程式碼,一般把單元測試寫出來。

使用道具 举报

回复
论坛徽章:
1056
紫蜘蛛
日期:2015-09-22 15:53:22紫蜘蛛
日期:2015-10-15 13:48:52紫蜘蛛
日期:2015-10-15 14:45:48紫蜘蛛
日期:2015-10-15 14:47:47紫蜘蛛
日期:2015-10-15 14:48:45九尾狐狸
日期:2015-09-22 15:53:22九尾狐狸
日期:2015-10-15 13:50:37九尾狐狸
日期:2015-10-15 14:45:48九尾狐狸
日期:2015-10-15 14:47:47九尾狐狸
日期:2015-10-15 14:48:45
57#
 楼主| 发表于 2013-11-24 11:52 | 只看该作者
習題 48: 更進階的使用者輸入

你的遊戲可能一路跑得很爽,不過你處理使用者輸入的方式肯定讓你不勝其煩了。每一個房間都需要一套自己的語句,而且只有使用者完全輸入正確後才能執行。你需要一個設備,它可以允許使用者以各種方式輸入語彙。例如下面的幾種表述都應該被支援才對:

  • open door
  • open the door
  • go THROUGH the door
  • punch bear
  • Punch The Bear in the FACE


也就是說,如果使用者的輸入和常用英語很接近也應該是可以的,而你的遊戲要識別出它們的意思。為了達到這個目的,我們將寫一個模組專門做這件事情。這個模組裡邊會有若干個類,它們互相配合,接受使用者輸入,並且將使用者輸入轉換成你的遊戲可以識別的命令。

英語的簡單格式是這個樣子的:

  • 單詞由空格隔開。
  • 句子由單詞組成。
  • 語法控制句子的含義。


所以最好的開始方式是先搞定如何得到使用者輸入的詞彙,並且判斷出它們是什麼。

我們的遊戲語彙

我在遊戲裡建立了下面這些語彙:

  • 表示方向: north, south, east, west, down, up, left, right, back.
  • 動詞: go, stop, kill, eat.
  • 修飾詞: the, in, of, from, at, it
  • 名詞: door, bear, princess, cabinet.
  • 數字詞: 由 0-9 構成的數字。


說到名詞,我們會碰到一個小問題,那就是不一樣的房間會用到不一樣的一組名詞,不過讓我們先挑一小組出來寫程式,以後再做改進吧。

如何斷句

我們已經有了詞彙表,為了分析句子的意思,接下來我們需要找到一個斷句的方法。我們對於句子的定義是「空格隔開的單詞」,所以只要這樣就可以了:

  1. stuff = gets.chomp()
  2. words = stuff.split()
复制代码

目前做到這樣就可以了,不過這招在相當一段時間內都不會有問題。

語彙結構

一旦我們知道瞭如何將句子轉化成詞彙列表,剩下的就是逐一檢查這些詞彙,看它們是什麼類型。為了達到這個目的,我們將用到一個非常便利的 Ruby 資料結構「struct」。「struct」其實就是一個可以把一串的 attrbutes 綁在一起的方式,使用 accessor 函式,但不需要寫一個複雜的 class。它的建立方式就像這樣:

  1. Pair = Struct.new(:token, :word)
  2. first_word = Pair.new("direction", "north")
  3. second_word = Pair.new("verb", "go")
  4. sentence = [first_word, second_word]
复制代码

這建立了一對 (TOKEN, WORD) 可以讓你看到 word 和在裡面做事。

這只是一個例子,不過最後做出來的樣子也差不多。你接受使用者輸入,用split 將其分隔成單詞列表,然後分析這些單詞,識別它們的類型,最後重新組成一個句子。

掃描輸入資料

現在你要寫的是詞彙掃描器。這個掃描器會將使用者的輸入字符串當做參數,然後返回由多個(TOKEN, WORD) struct 組成的列表,這個列表實現類似句子的功能。如果一個單詞不在預定的詞彙表中,那它返回時 WORD 應該還在,但TOKEN 應該設置成一個專門的錯誤標記。這個錯誤標記將告訴使用者哪裡出錯了。

有趣的地方來了。我不會告訴你這些該怎樣做,但我會寫一個「單元測試(unit test)」,而你要把掃描器寫出來,並保證單元測試能夠正常通過。

Exceptions And Numbers

有一件小事情我會先幫幫你,那就是數字轉換。為了做到這一點,我們會作一點弊,使用「異常(exceptions)」來做。「異常」指的是你運行某個函數時得到的錯誤。你的函數在碰到錯誤時,就會「提出(raise)」一個「異常」,然後你就要去處理(handle)這個異常。假如你在 IRB 裡寫了這些東西:

  1. ruby-1.9.2-p180 :001 > Integer("hell")
  2. ArgumentError: invalid value for Integer(): "hell"
  3.     from (irb):1:in `Integer'
  4.     from (irb):1
  5.     from /home/rob/.rvm/rubies/ruby-1.9.2-p180/bin/irb:16:in `<main>'
复制代码

這個 ArgumentError 就是 Integer() 函式拋出的一個異常。因為你給Integer() 的參數不是一個數字。Integer()函數其實也可以傳回一個值來告訴你它碰到了錯誤,不過由於它只能傳回整數值,所以很難做到這一點。它不能返回-1,因為這也是一個數字。 Integer()沒有糾結在它「究竟應該返回什麼」上面,而是提出了一個叫做TypeError的異常,然後你只要處理這個異常就可以了。

處理異常的方法是使用 begin 和 rescue 這兩個關鍵字:

  1. def convert_number(s)
  2.   begin
  3.     Integer(s)
  4.   rescue ArgumentError
  5.     nil
  6.   end
  7. end
复制代码

你把要試著運行的程式碼放到「begin」的區段裡,再將出錯後要運行的程式碼放到「except」區段裡。在這裡,我們要試著呼叫 Integer() 去處理某個可能是數字的東西,如果中間出了錯,我們就「rescue」這個錯誤,然後返回 「nil」。

在你寫的掃描器裡面,你應該使用這個函數來測試某個東西是不是數字。做完這個檢查,你就可以聲明這個單詞是一個錯誤單詞了。

What You Should Test

這裡是你應該使用的測試檔案 test/test_lexicon.rb:

  1. require 'test/unit'
  2. require_relative "../lib/lexicon"

  3. class LexiconTests < Test::Unit::TestCase

  4.   Pair = Lexicon::Pair
  5.   @@lexicon = Lexicon.new()

  6.   def test_directions()
  7.     assert_equal([Pair.new(:direction, 'north')], @@lexicon.scan("north"))
  8.     result = @@lexicon.scan("north south east")
  9.     assert_equal(result, [Pair.new(:direction, 'north'),
  10.                  Pair.new(:direction, 'south'),
  11.                  Pair.new(:direction, 'east')])
  12.   end

  13.   def test_verbs()
  14.     assert_equal(@@lexicon.scan("go"), [Pair.new(:verb, 'go')])
  15.     result = @@lexicon.scan("go kill eat")
  16.     assert_equal(result, [Pair.new(:verb, 'go'),
  17.                  Pair.new(:verb, 'kill'),
  18.                  Pair.new(:verb, 'eat')])
  19.   end

  20.   def test_stops()
  21.     assert_equal(@@lexicon.scan("the"), [Pair.new(:stop, 'the')])
  22.     result = @@lexicon.scan("the in of")
  23.     assert_equal(result, [Pair.new(:stop, 'the'),
  24.                  Pair.new(:stop, 'in'),
  25.                  Pair.new(:stop, 'of')])
  26.   end

  27.   def test_nouns()
  28.     assert_equal(@@lexicon.scan("bear"), [Pair.new(:noun, 'bear')])
  29.     result = @@lexicon.scan("bear princess")
  30.     assert_equal(result, [Pair.new(:noun, 'bear'),
  31.                  Pair.new(:noun, 'princess')])
  32.   end

  33.   def test_numbers()
  34.     assert_equal(@@lexicon.scan("1234"), [Pair.new(:number, 1234)])
  35.     result = @@lexicon.scan("3 91234")
  36.     assert_equal(result, [Pair.new(:number, 3),
  37.                  Pair.new(:number, 91234)])
  38.   end

  39.   def test_errors()
  40.     assert_equal(@@lexicon.scan("ASDFADFASDF"), [Pair.new(:error, 'ASDFADFASDF')])
  41.     result = @@lexicon.scan("bear IAS princess")
  42.     assert_equal(result, [Pair.new(:noun, 'bear'),
  43.                  Pair.new(:error, 'IAS'),
  44.                  Pair.new(:noun, 'princess')])
  45.   end

  46. end
复制代码

記住你要使用你的專案骨架來建立新專案項目,將這個 Test Case 寫下來(不許複製貼上!),然後編寫你的掃描器,直至所有的測試都能通過。注意細節並確認結果一切工作良好。

設計提示

集中一次實現一個測試,盡量保持簡單,只要把你的 lexicon.rb 詞彙表中所有的單詞放那裡就可以了。不要修改輸入的單詞表,不過你需要建立自己的新列表,裡邊包含你的語彙元組。另外,記得使用 include? 函式來檢查這些語彙陣列,以確認某個單詞是否在你的語彙表中。

加分習題

  • 改進單元測試,讓它覆蓋到更多的語彙。
  • 向語彙列表添加更多的語彙,並且更新單元測試程式碼。
  • 讓你的掃描器能夠識別任意大小寫的詞彙。更新你的單元測試以確認其功能。
  • 找出另外一種轉換為數字的方法。
  • 我的解決方案用了37 行程式碼,你的是更長還是更短呢?

使用道具 举报

回复
论坛徽章:
1056
紫蜘蛛
日期:2015-09-22 15:53:22紫蜘蛛
日期:2015-10-15 13:48:52紫蜘蛛
日期:2015-10-15 14:45:48紫蜘蛛
日期:2015-10-15 14:47:47紫蜘蛛
日期:2015-10-15 14:48:45九尾狐狸
日期:2015-09-22 15:53:22九尾狐狸
日期:2015-10-15 13:50:37九尾狐狸
日期:2015-10-15 14:45:48九尾狐狸
日期:2015-10-15 14:47:47九尾狐狸
日期:2015-10-15 14:48:45
58#
 楼主| 发表于 2013-11-24 11:55 | 只看该作者
習題 49: 創造句子

從我們這個小遊戲的詞彙掃描器中,我們應該可以得到類似下面的列表(你的看起來可能格式會不太一樣):

  1. ruby-1.9.2-p180 :003 > print Lexicon.scan("go north")
  2. [#<struct Lexicon::Pair token=:verb, word="go">,
  3.     #<struct Lexicon::Pair token=:direction, word="north">] => nil
  4. ruby-1.9.2-p180 :004 > print Lexicon.scan("kill the princess")
  5. [#<struct Lexicon::Pair token=:verb, word="kill">,
  6.     #<struct Lexicon::Pair token=:stop, word="the">,
  7.     #<struct Lexicon::Pair token=:noun, word="princess">] => nil
  8. ruby-1.9.2-p180 :005 > print Lexicon.scan("eat the bear")
  9. [#<struct Lexicon::Pair token=:verb, word="eat">,
  10.     #<struct Lexicon::Pair token=:stop, word="the">,
  11.     #<struct Lexicon::Pair token=:noun, word="bear">] => nil
  12. ruby-1.9.2-p180 :006 > print Lexicon.scan("open the door and smack the bear in the nose")
  13. [#<struct Lexicon::Pair token=:error, word="open">,
  14.     #<struct Lexicon::Pair token=:stop, word="the">,
  15.     #<struct Lexicon::Pair token=:noun, word="door">,
  16.     #<struct Lexicon::Pair token=:error, word="and">,
  17.     #<struct Lexicon::Pair token=:error, word="smack">,
  18.     #<struct Lexicon::Pair token=:stop, word="the">,
  19.     #<struct Lexicon::Pair token=:noun, word="bear">,
  20.     #<struct Lexicon::Pair token=:stop, word="in">,
  21.     #<struct Lexicon::Pair token=:stop, word="the">,
  22.     #<struct Lexicon::Pair token=:error, word="nose">] => nil
  23. ruby-1.9.2-p180 :007 >
复制代码

現在讓我們把它轉化成遊戲可以使用的東西,也就是一個 Sentence 類。

如果你還記得學校學過的東西的話,一個句子是由這樣的結構組成的:

主語(Subject) + 謂語(動詞Verb) + 賓語(Object)

很顯然實際的句子可能會比這複雜,而你可能已經在英語的語法課上面被折騰得夠嗆了。我們的目的,是將上面的 struct 列表轉換為一個 Sentence 物件,而這個對象又包含主謂賓各個成員。

匹配(Match) And 窺視(Peek)

為了達到這個效果,你需要四樣工具:

一個循環存取 struct 列表的方法,這挺簡單的。
「匹配」我們的主謂賓設置中不同種類 struct 的方法。
一個「窺視」潛在struct的方法,以便做決定時用到。
「跳過(skip)」我們不在乎的內容的方法,例如形容詞、冠詞等沒有用處的詞彙。
我們使用 peek 函式查看 struct 列表中的下一個成員,做匹配以後再對它做下一步動作。讓我們先看看這個 peek 函式:
  1. def peek(word_list)
  2.   begin
  3.     word_list.first.token
  4.   rescue
  5.     nil
  6.   end
  7. end
复制代码

很簡單。再看看 match 函式:

  1. def match(word_list, expecting)
  2.   begin
  3.     word = word_list.shift
  4.     if word.token == expecting
  5.       word
  6.     else
  7.       nil
  8.     end
  9.   rescue
  10.     nil
  11.   end
  12. end
复制代码

還是很簡單,最後我們看看 skip 函式:

  1. def skip(word_list, word_type)
  2.   while peek(word_list) == word_type
  3.     match(word_list, word_type)
  4.   end
  5. end
复制代码

以你現在的水準,你應該可以看出它們的功能來。確認自己真的弄懂了它們。

句子的語法

有了工具,我們現在可以從 struct 列表來構建句子(Sentence)對象了。我們的處理流程如下:

  • 使用 peek 識別下一個單詞。
  • 如果這個單詞和我們的語法匹配,我們就調用一個函式來處理這部分語法。假設函式的名字叫 parse_subject 好了。
  • 如果語法不匹配,我們就 raise 一個錯誤,接下來你會學到這方面的內容。
  • 全部分析完以後,我們應該能得到一個 Sentence 物件,然後可以將其應用在我們的遊戲中。


演示這個過程最簡單的方法是把程式碼展示給你讓你閱讀,不過這節習題有個不一樣的要求,前面是我給你測試程式碼,你照著寫出程式碼來,而這次是我給你的程序,而你要為它寫出測試程式碼來。

以下就是我寫的用來解析簡單句子的程式碼,它使用了 ex48 這個 Lexicon class。

  1. class ParserError < Exception

  2. end

  3. class Sentence

  4.   def initialize(subject, verb, object)
  5.     # remember we take Pair.new(:noun, "princess") structs and convert them
  6.     @subject = subject.word
  7.     @verb = verb.word
  8.     @object = object.word
  9.   end

  10. end

  11. def peek(word_list)
  12.   begin
  13.     word_list.first.token
  14.   rescue
  15.     nil
  16.   end
  17. end

  18. def match(word_list, expecting)
  19.   begin
  20.     word = word_list.shift
  21.     if word.token == expecting
  22.       word
  23.     else
  24.       nil
  25.     end
  26.   rescue
  27.     nil
  28.   end
  29. end

  30. def skip(word_list, token)
  31.   while peek(word_list) == token
  32.     match(word_list, token)
  33.   end
  34. end

  35. def parse_verb(word_list)
  36.   skip(word_list, :stop)

  37.   if peek(word_list) == :verb
  38.     return match(word_list, :verb)
  39.   else
  40.     raise ParserError.new("Expected a verb next.")
  41.   end
  42. end

  43. def parse_object(word_list)
  44.   skip(word_list, :stop)
  45.   next_word = peek(word_list)

  46.   if next_word == :noun
  47.     return match(word_list, :noun)
  48.   end
  49.   if next_word == :direction
  50.     return match(word_list, :direction)
  51.   else
  52.     raise ParserError.new("Expected a noun or direction next.")
  53.   end
  54. end

  55. def parse_subject(word_list, subj)
  56.   verb = parse_verb(word_list)
  57.   obj = parse_object(word_list)

  58.   return Sentence.new(subj, verb, obj)
  59. end

  60. def parse_sentence(word_list)
  61.   skip(word_list, :stop)

  62.   start = peek(word_list)

  63.   if start == :noun
  64.     subj = match(word_list, :noun)
  65.     return parse_subject(word_list, subj)
  66.   elsif start == :verb
  67.     # assume the subject is the player then
  68.     return parse_subject(word_list, Pair.new(:noun, "player"))
  69.   else
  70.     raise ParserError.new("Must start with subject, object, or verb not: #{start}")
  71.   end
  72. end
复制代码

關於異常(Exception)

你已經簡單學過關於異常的一些東西,但還沒學過怎樣拋出(raise)它們。這節的程式碼示範了如何 raise。首先在最前面,你要定義好 ParserException這個類,而它又是 Exception 的一種。另外要注意我們是怎樣使用 raise這個關鍵字來拋出異常的。

你的測試程式碼應該也要測試到這些異常,這個我也會示範給你如何實現。

你應該測試的東西

為《習題49》寫一個完整的測試方案,確認程式碼中所有的東西都能正常工作,其中異常的測試——輸入一個錯誤的句子它會拋出一個異常來。

使用 assert_raises 這個函式來檢查異常,在 Test::Unit 的文件裡查看相關的內容,學著使用它寫針對「執行失敗」的測試,這也是測試很重要的一個方面。從文件中學會使用 assert_raises,以及一些別的函式。

寫完測試以後,你應該就明白了這段程式碼的運作原理,而且也學會了如何為別人的程式碼寫測試程式碼。相信我,這是一個非常有用的技能。

加分習題

  • 修改 parse_ method,將它們放到一個類裡邊,而不僅僅是獨立的方法函式。這兩種設計你喜歡哪一種呢?
  • 提高parser 對於錯誤輸入的抵禦能力,這樣即使使用者輸入了你預定義語彙之外的詞語,你的程式碼也能正常運行下去。
  • 改進語法,讓它可以處理更多的東西,例如數字。
  • 想想在遊戲裡你的 Sentence 類可以對使用者輸入做哪些有趣的事情。

使用道具 举报

回复
论坛徽章:
1056
紫蜘蛛
日期:2015-09-22 15:53:22紫蜘蛛
日期:2015-10-15 13:48:52紫蜘蛛
日期:2015-10-15 14:45:48紫蜘蛛
日期:2015-10-15 14:47:47紫蜘蛛
日期:2015-10-15 14:48:45九尾狐狸
日期:2015-09-22 15:53:22九尾狐狸
日期:2015-10-15 13:50:37九尾狐狸
日期:2015-10-15 14:45:48九尾狐狸
日期:2015-10-15 14:47:47九尾狐狸
日期:2015-10-15 14:48:45
59#
 楼主| 发表于 2013-11-24 11:59 | 只看该作者
習題 50: 你的第一個網站

這節以及後面的習題中,你的任務是把前面建立的遊戲做成網頁版。這是本書的最後三個章節,這些內容對你來說難度會相當大,你要在上面花些時間才能做出來。在你開始這節練習以前,你必須已經成功地完成過了《習題46》的內容,正確安裝了 RubyGems,而且學會瞭如何安裝軟體套件以及如何建立專案骨架。如果你不記得這些內容,就回到《習題46》重新複習一遍。

安裝 Sinatra

在建立你的第一個網頁應用程式之前,你需要安裝一個「Web框架」,它的名字叫 Sinatra。所謂的「框架」通常是指「讓某件事情做起來更容易的軟體套件」。在網頁應用的世界裡,人們建立了各種各樣的「網頁框架」,用來解決他們在建立網站時碰到的問題,然後把這些解決方案用軟體套件的方式發佈出來,這樣你就可以利用它們引導建立你自己的專案了。

可選的框架類型有很多很多,不過在這裡我們將使用 Sinatra 框架。你可以先學會它,等到差不多的時候再去接觸其它的框架,不過 Sinatra 本身挺不錯的,所以就算你一直使用也沒關係。

使用 gem 安裝 Sinatra:

  1. $ gem install sinatra
  2. Fetching: tilt-1.3.2.gem (100%)
  3. Fetching: sinatra-1.2.6.gem (100%)
  4. Successfully installed tilt-1.3.2
  5. Successfully installed sinatra-1.2.6
  6. 2 gems installed
  7. Installing ri documentation for tilt-1.3.2...
  8. Installing ri documentation for sinatra-1.2.6...
  9. Installing RDoc documentation for tilt-1.3.2...
  10. Installing RDoc documentation for sinatra-1.2.6...
复制代码

寫一個簡單的「Hello World」專案

現在你將做一個非常簡單的「Hello World」專案出來,首先你要建立一個專案目錄:

  1. $ cd projects
  2. $ bundle gem gothonweb
复制代码

你最終的目的是把《習題42》中的遊戲做成一個 web 應用,所以你的專案名稱叫做 gothonweb,不過在此之前,你需要建立一個最基本的 Sinatra應用,將下面的代碼放到lib/gothonweb.rb中:

  1. require_relative "gothonweb/version"
  2. require "sinatra"

  3. module Gothonweb
  4.   get '/' do
  5.     greeting = "Hello, World!"
  6.     return greeting
  7.   end
  8. end
复制代码

然後使用下面的方法來運行這個 web 程式:

  1. $ ruby lib/gothonweb.rb
  2. == Sinatra/1.2.6 has taken the stage on 4567 for development with backup from WEBrick
  3. [2011-07-18 11:27:07] INFO  WEBrick 1.3.1
  4. [2011-07-18 11:27:07] INFO  ruby 1.9.2 (2011-02-18) [x86_64-linux]
  5. [2011-07-18 11:27:07] INFO  WEBrick::HTTPServer#start: pid=6599 port=4567
复制代码

最後,使用你的網頁瀏覽器,打開 URL http://localhost:4567/,你應該看到兩樣東西,首先是瀏覽器裡顯示了 Hello, world!,然後是你的命令行終端顯示了如下的輸出:

  1. 127.0.0.1 - - [18/Jul/2011 11:29:10] "GET / HTTP/1.1" 200 12 0.0015
  2. localhost - - [18/Jul/2011:11:29:10 EDT] "GET / HTTP/1.1" 200 12
  3. - -> /
  4. 127.0.0.1 - - [18/Jul/2011 11:29:10] "GET /favicon.ico HTTP/1.1" 404 447 0.0008
  5. localhost - - [18/Jul/2011:11:29:10 EDT] "GET /favicon.ico HTTP/1.1" 404 447
  6. - -> /favicon.ico
复制代码

這些是 Sinatra 印出的 log 資訊,從這些資訊你可以看出服務器有在運行,而且能了解到程式在瀏覽器背後做了些什麼事情。這些資訊還有助於你發現程式的問題。例如在最後一行它告訴你瀏覽器試圖存取 /favicon.ico,但是這個文件並不存在,因此它返回的狀態碼是 404 Not Found。

到這裡,我還沒有講到任何 web 相關的工作原理,因為首先你需要完成準備工作,以便後面的學習能順利進行,接下來的兩節習題中會有詳細的解釋。我會要求你用各種方法把你的 Sinatra 應用程式弄壞,然後再將其重新構建起來:這樣做的目的是讓你明白運行 Sinatra 程式需要準備好哪些東西。

發生了什麼事情?

在瀏覽器訪問到你的網頁應用程式時,發生了下面一些事情:

  • 瀏覽器通過網路連接到你自己的電腦,它的名字叫做 localhost,這是一個標準稱謂,表示的誰就是網路中你自己的這台電腦,不管它實際名字是什麼,你都可以使用 localhost來訪問。它使用到port 4567。
  • 連接成功以後,瀏覽器對 lib/gothonweb.rb這個應用程式發出了HTTP請求(request),要求訪問URL/`,這通常是一個網站的第一個URL。
  • 在lib/gothonweb.rb 裡,我們有一個程式碼區段,裡面包含了 URL 的匹配關係。我們這裡只定義了一組匹配,那就是「/」。它的含義是:如果有人使用瀏覽器訪問 / 這一級目錄,Sinatra 將找到它,從而用它處理這個瀏覽器請求。
  • Sinatra 呼叫匹配到的程式碼區段,這段程式碼只簡單的回傳了一個字串傳回給瀏覽器。
  • 最後 Sinatra 完成了對於瀏覽器請求的處理將響應(response)回傳給瀏覽器,於是你就看到了現在的頁面。


確定你真的弄懂了這些,你需要畫一個示意圖,來理清資訊是如何從瀏覽器傳遞到 Sinata,再到 / 區段,再回到你的瀏覽器的。

修正錯誤

第一步,把第 6 行的 greeting 變數刪掉,然後重新刷瀏覽器。你應該會看到一個錯誤畫面,你可以通過這一頁豐富的資訊看出你的程式崩潰的原因。當然你已經知道出錯的原因是 greeting的賦值遺失了,不過 Sinatra還是會給你一個挺好的錯誤頁面,讓你能找到出錯的具體位置。試試在這個錯誤頁面上做以下操作:

  • 看看 sinatra.error 變數。
  • 看看 REQUEST_ 變數裡的資訊。裡面哪些知識是你已經熟悉了的。這是瀏覽器發給你的 gothonweb 應用程式的資訊。這些知識對於日常網頁瀏覽沒有什麼用處,但現在你要學會這些東西,以便寫出web應用程式來。


建立基本的模板

你已經試過用各種方法把這個Sinatra 程式改錯,不過你有沒有注意到「Hello World」不是一個好 HTML 網頁呢?這是一個 web 應用,所以需要一個合適的HTML 響應頁面才對。為了達到這個目的,下一步你要做的是將「Hello World」以較大的綠色字體顯示出來。

第一步是建立一個 lib/views/index.erb 檔案,內容如下:

  1. <html>
  2.     <head>
  3.         <title>Gothons Of Planet Percal #25</title>
  4.     </head>
  5. <body>

  6.   <% if greeting %>
  7.     <p>I just wanted to say <em style="color: green; font-size: 2em;"><%= greeting %></em>.
  8.   <% else %>
  9.     <em>Hello</em>, world!
  10.   <% end %>

  11. </body>
  12. </html>
复制代码

什麼是一個 .erb 的檔案?ERB 的全名是 Embedded Ruby。.erb 檔案其實是一個內嵌一點 Ruby 程式碼的 HTML。如果你學過HTML的話,這些內容你看上去應該很熟悉。如果你沒學過HTML,那你應該去研究一下,試著用HTML寫幾個網頁,從而知道它的運作原理。既然這是一個 erb 模版,Sinatra 就會在模板中找到對應的位置,將參數的內容填充到模板中。例如每一個出現 `<%= greeting %> 的位置,內容都會被替換成對應這個變數名的參數。

為了讓你的 lib/gothonweb.rb 處理模板,你需要寫一寫程式碼,告訴Sinatra 到哪裡去找到模板進行加載,以及如何渲染(render)這個模板,按下面的方式修改你的檔案:

  1. require_relative "gothonweb/version"
  2. require "sinatra"
  3. require "erb"

  4. module Gothonweb
  5.   get '/' do
  6.     greeting = "Hello, World!"
  7.     erb :index, :locals => {:greeting => greeting}
  8.   end
  9. end
复制代码

特別注意我改了 / 這個程式碼區段最後一行的內容,這樣它就可以呼叫 erb 然後把 greeting 變數傳給它。

改好上面的程式後,刷新一下瀏覽器中的網頁,你應該會看到一條和之前不同的綠色資訊輸出。你還可以在瀏覽器中通過「查看原始碼(View Source)」看到模板被渲染成了標準有效的HTML 原始碼。

這麼講也許有些太快了,我來詳細解釋一下模板的運作原理吧:

  • 在 lib/gothonweb.rb 你添加了一個 erb 函式呼叫。
  • 這個 erb 函式知道怎麼載入 lib/views 目錄夾裡的 .erb 的檔案。它知道去抓哪些檔案(在這個例子裡是 index.erb)。因為你傳了一個參數進去(erb :index …)。
  • 現在,當瀏覽器讀取 / 且 lib/gothonweb.eb 匹配然後執行 get '/' do 區段,它再也沒有只是回傳字串 greeting,而是呼叫 erb 然後傳入 greeting 作為一個變數。
  • 最後,你讓 lib/views/index.erb 去檢查 greeting 這個變數,如果這個變數存在的話,就印出變數裡的內容。如果不存在的話,就會印出一個預設的訊息。


要深入理解這個過程,你可以修改 greeting 變數以及 HTML ,看看會友什麼效果。然後也創作另外一個叫做lib/views/foo.erb的模板。然後把erb :index改成erb :foo。從這個過程中你也可以看到,你傳入給erb的第一個參數只要匹配到lib/views下的.erb` 檔案名稱,就可以被渲染出來了。

加分習題

  • 到 Sinatra 這個框架的官方網站去閱讀更多文件。
  • 實驗一下你在上述網站中看到的所有東西,包括他們的範例程式碼。
  • 閱讀有關於 HTML5 和 CSS3 相關的東西,自己練習寫幾個 .html 和 .css 文件。
  • 如果你有一個懂 Rails 的朋友可以幫你的畫,你可以自己試著使用 Rails 完成一下習題 50,51,52,看看結果會是什麼樣子。

使用道具 举报

回复

您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

TOP技术积分榜 社区积分榜 徽章 团队 统计 知识索引树 积分竞拍 文本模式 帮助
  ITPUB首页 | ITPUB论坛 | 数据库技术 | 企业信息化 | 开发技术 | 微软技术 | 软件工程与项目管理 | IBM技术园地 | 行业纵向讨论 | IT招聘 | IT文档
  ChinaUnix | ChinaUnix博客 | ChinaUnix论坛
CopyRight 1999-2011 itpub.net All Right Reserved. 北京盛拓优讯信息技术有限公司版权所有 联系我们 未成年人举报专区 
京ICP备16024965号-8  北京市公安局海淀分局网监中心备案编号:11010802021510 广播电视节目制作经营许可证:编号(京)字第1149号
  
快速回复 返回顶部 返回列表