Папярэдні занятак

Яшчэ мадэляваньне

Мы ўжо маем клас для гуляца. Зараз давайце створым яшчэ адзін відавочны клас – Костка. Спачатку я ствару костку адразу пасьля гульцоў:

puts "Welcome to Pig!\n\n"
players = [Player.new("Player 1"), Player.new("Player 2")]
die = Die.new

Стварыце патрэбную спеку, файлы з логікай і напішыце тэсты.

Зараз давайце адрэфактарым кіданьне костцы выкарыстоўваючы клас Die:

until players.select {|player| player.points >= 100}.any?
  players.each do |player|
    puts "#{player.name}, your turn.\n\n"
    turn_points = 0
    player_choice = nil

    until die.roll == 1 || player_choice == 'h'
      die.roll!
      puts "You rolled a #{die.roll}."
      turn_points += roll
      if die.roll == 1
        turn_points = 0
      else
        # ...
      end
    end
    # ...
  end
end

Вось мой юніт-тэст:

die = Die.new
die.roll should return nil.

Мая рэалізацыя:

class Die
  attr_reader :roll
end

Яшчэ адзін юніт тэст:

die = Die.new
die.roll!
die.roll should equal a random number between one and six.

І рэалізацыя:

class Die
  attr_reader :roll

  def roll!
    @roll = rand(1..6)
  end
end

Зараз, калі Мы хочам каб наша костка мела 8 бакоў або выкідвала 5 больш за ўсё, мы можам проста зьмяніць клас Die і больш нічога не кранаць.

Мадэляваньне, мадэляваньне…

У нас ужо ёсьць класы для костак і гульцоў. А што наконт класа для спробы? Не зусім інтуітыўна што спроба зьяўляецца аб’ектам, але гэта й ёсьць сэрца ААП – ўсё ёсьць аб’ект. Вось якія будзе інтэрфэйс у класа Turn:

puts "#{player.name}, your turn.\n\n"
turn = Turn.new(player)

until turn.over?
  turn.roll(die)
  puts "You rolled a #{die.roll}."
  unless turn.over?
    puts "So far this turn, you have #{turn.points} points."
    puts "Press 'h' to hold or any other key to roll again.\n\n"
    player_choice = gets.chomp
    turn.hold if player_choice == 'h'
  end
end

puts "You got #{turn.points} points this turn."
puts "You have #{player.points} points total.\n\n\n"

Напішыце тэст для Turn.new. Памятайце пра аргумэнт.

Наступны мэтад гэта Turn#over?. Нам будзе патрэбна некалькі юніт тэстаў для яго. Першы будзе такі:

turn = Turn.new
turn.over? should equal false.

Вось мая рэалізацыя яго:

class Turn
  def over?
    false
  end
end

Гэта мэтад проста вяртае false! Ён ня робіць тое, што мусіць рабіць. Так, але мы яшчэ не дайшлі да гэтага. Няхай наш тэст проста выконваецца спачатку. Мы не падрыхтавалі яшчэ астатнюю частку праграмы для сапраўднай рэалізацыіі Turn#over?.

Рабіце, не пытайце

Давайце зрушым крыху юзер-інтэрфэйс кода ў мэтад класа Turn: turn.roll(die).

Навошта я зрабіў каб спроба выкідавла косткі? Я мог бы зрабіць штосьці такое:

die.roll!
if die.roll == 1
  turn.over!
else
  turn.add_points(die.roll)
end

Ёсьць яшчэ адзін прынцып ААП рабі, а не пытай. У прыкладзе вышэй мы пытаем ў костцы яе значэньне й потым робім нейкае дзеяньне ў залежнасьці ад гэтага значэньня. Справа ў тым што зараз, тое што знаходзіцца звонку Turn мусіць ведаць калі спроба скончыцца. Turn мусіць увасабляць усе веды таго як ім карыстацца. Гэта дазваляем нам рабіць наш код чысьцей: калі мы пытаем і адказваем, тады, калі б не быў выкарыстаны наш аб’ект, код які выклікае яго мусіць рабіць гэта ўсё зноў і зноў. Калі аб’ект ведае ўсё пра сябе, ён можа паклапоціцца пра сябе дзе бы ён не знаходзіўся.

Ці памятаеце Вы як я прапаноўваў думаць пра -5.abs як пра дасыланьне мэтада abs аб’екту -5? Адзін з шляхоў як уяўляць ААП – гэта думаць што кожны аб’ект дасылае іншаму аб’екту паведамленьне ў выглядзе матадаў. Яны не павінны магчы зьмяніць іншы аб’екта напрамую; толькі па сродках дасыланьня паведамленьняў адзін аднаму.

Вяртаемся да кода. Рэалізацыя Turn#roll:

class Turn
  def roll(die)
    die.roll!
  end
end

Давайце напішам яшчэ адзін тэст для Turn#over?:

turn = Turn.new
die = Die.new
turn.roll(die)
turn.over? should equal true if die.roll is 1 and false otherwise.

Для Вас засталося рэалізаваць усе гэта, напісаць тэсты й рэалізацыю мэтада Turn#hold і Turn#points, і дадатковыя ўласіцівасьці мэтаду Turn#roll.

Выманьне ўсёй логікі з інтэрфэйса

Вось прыкладна як мусіць выгладаць наша праграма зараз:

puts "Welcome to Pig!\n\n"
players = [Player.new("Player 1"), Player.new("Player 2")]
die = Die.new

until players.select {|player| player.points >= 100}.any?
  players.each do |player|
    puts "#{player.name}, your turn.\n\n"
    turn = Turn.new(player)

    until turn.over?
      turn.roll(die)
      puts "You rolled a #{die.roll}."
      unless turn.over?
        puts "So far this turn, you have #{turn.points} points."
        puts "Press 'h' to hold or any other key to roll again.\n\n"
        player_choice = gets.chomp
        turn.hold if player_choice == 'h'
      end
    end

    puts "You got #{turn.points} points this turn."
    puts "You have #{player.points} points total.\n\n\n"
  end
end

winner = players.max_by {|player| player.points}
puts "Congratulations, #{winner.name}. You win!"

Ня дрэнна, так?

Ці можаце Вы ўявіць сабе штосьці, што яшчэ можна пераўтварыць у клас?

Падказка: інтэрфэйс карыстальніка не павінен мець ніякага коду акрамя коду ўзаемадзеяньня з карыстальнікам :)

Калі Вы здагадаліся што гэта, адрэфактарыце код. Спачатку інтэрфэйс, потым тэсты й сам код.

Калі не – рухайцеся далей.

Апошнія крокі да сканчэньня

Калі Вам была патрэбна падказка вось яна – сама гульня можа мець мадэль.

Калі Вы скончыце працаваць над класам Game, Вы можаце паглядзець на ўвесь код маёй праграмы. Звярніце ўвагу на тое, якія кароткія мае мэтады й класы. Вы павінны спрабаваць пісаць мэтады, якія ня больш чым некалькі радкоў, і класы, якія змяшчаюцца на экране. Калі Вы будзеце трымаць вашую праграму чыстай і простай, будзе вельмі проста зразумець (і зьмяніць) яе.

lib/player.rb:

class Player
  attr_reader :name, :points

  def initialize(name)
    @name = name
    @points = 0
  end

  def add_points(new_points)
    @points += new_points
  end
end

lib/die.rb:

class Die
  attr_reader :roll

  def roll!
    @roll = rand(1..6)
  end
end

lib/turn.rb:

class Turn
  attr_reader :points

  def initialize(player)
    @player = player
    @over = false
    @points = 0
  end

  def over?
    @over
  end

  def roll(die)
    die.roll!
    roll = die.roll
    if roll == 1
      @over = true
      @points = 0
    else
      @points += roll
    end
  end

  def hold
    @over = true
    @player.add_points(@points)
  end
end

lib/game.rb:

class Game
  def initialize(players)
    @players = players
  end

  def over?
    @players.select {|player| player.points >= 100}.any?
  end

  def winner
    @players.max_by {|player| player.points}
  end
end

bin/pig_ui.rb:

require './lib/die'
require './lib/game'
require './lib/player'
require './lib/turn'


puts "Welcome to Pig!\n\n"
players = [Player.new("Player 1"), Player.new("Player 2")]
game = Game.new(players)
die = Die.new

until game.over?
  players.each do |player|
    puts "#{player.name}, your turn.\n\n"
    turn = Turn.new(player)

    until turn.over?
      turn.roll(die)
      puts "You rolled a #{die.roll}."
      unless turn.over?
        puts "So far this turn, you have #{turn.points} points."
        puts "Press 'h' to hold or any other key to roll again.\n\n"
        player_choice = gets.chomp
        turn.hold if player_choice == 'h'
      end
    end

    puts "You got #{turn.points} points this turn."
    puts "You have #{player.points} points total.\n\n\n"
  end
end

puts "Congratulations, #{game.winner.name}. You win!"

Заўважце як амаль ніякай логікі не засталося ў інтэрфэйсе – ўсё жыве ў класах. Гэта вельмі добрая практыка падзяляць інтэрфэйс ад логікі Вашае праграмы. Калі мне будзе патрэбна зрабіць анлайн, або графічную вэрсію нашае гульні – будзе вельмі проста, таму што мне няма патрэбы зьмяняць логікі яе працы.

Памятаеце пра крыху іншыя вэрсіі гульні, пра якія мы размаўлялі спачатку: розная колькасьць гульцоў, косьці рознага кшталту й рознай колькасьцю бакоў, іншыя ўмовы перамогі? Падумайце пра тое, як проста зараз гэта ўсё ажжыцявіць.

ААП дапамагае проста зразумець што робіць код іншага праграміста, зьмяніць яго без праблемаў і дае магчымасьць працаваць розным людзям над рознымі часткамі праграмы адначасова, не перашкаджае адзін аднаму.

Вывады ААП

Шмат пра што мы даведаліся, так? Давайце праглядзім:

  • Выкарыстоўвайце назвы, якія лёгка разумеюцца. Не абрывіятуры.
  • Мадэлюйце вашую праграму класамі.
  • Дасылайце паведамленьні Вашым аб’ектам і дазваляйце ім рабіць усё самастойна.
  • Рабіце, не пытайце. Калі Вы робіце нейкае дзеяньне ў залежнасьці ад становішча аб’екта адрэфактарце код так, каб аб’ект рабіў яго сам.
  • Энкапсуляцыя. Каб ніхто акрамя аб’екта ня ведаў пра яго, больш чым ян ведае сам.
  • Стварайце свае ўласные інтэрфэйсы для аб’ектаў. Пазьбягайце “чытачоў” і “пісачоў”.
  • Не Паўтарайце Сябе.
  • Пішыце кароткія мэтады й класы.
  • Падзяляйце Ваш інтэрфэйс і логіку.

Yacht dice

Ці гатовыя Вы напісаць яшчэ адну праграму ?

Yacht гэта яшчэ адна гульня ў косткі. Кожны гульца кідае пяць костка й спадзяваецца на адну з камбінацыяў:

  • чатыры аднолькавыя – дадаецца сумма ўсіх костак;
  • “full house” – тры аднолькавыя й пара – дадаецца 25;
  • пасьлядоўна – пяць лікаў па парадку – дадаецца 40;
  • “yacht” – пяць аднолькавых – дадаецца 50.

За ўсе астатнія камбінацыі нічога не даецца.

Гульня працягваецца 13 хадоў.

Памятайце пра наступныя добрыя правілы:

  • Спачатку стварыце інтэрфэйс карыстальніка (або прынамсі яго першую частку; магчыма будзе прасьцей стварыць спачатку трошку інтэрфэйса, потым трошку маделяў, і зноў вярнуцца да інтэрфэйса).
  • Стварыце новы файл і юніт-тэст для кожнага класа, пакладзіце іх у тэчкі spec і lib суадносна.
  • Спачатку заўсёды пішыце тэсты, бо гэта дазваляе Вам бачыць праграму праз яе інтэрфэйсы, а не праз іхную рэалізацыю.
  • Пішыце столькі коду колькі дастаткова й ня больш.

Падглядайце ў мінулы занятак калі нешта ня будзе зразумела.

Пачытайце пра мову разметцы Markdown і стварыце файл README.md які апісвае Ваш праект. Гэта будзе патрэбна для наступнага занятка

Наступны занятак