Што робіць добры код добрым?

Мы набліжаемся да паглыбленьня ў аб’ектна-арыентаваны дызайн (таксама завецца аб’екта-арыентаванае праграмаваньне, ці ААП, або проста АА). Паходу нашага далейшага навучаньня я буду распавядаць пра дрэнныя й добрыя падыходы да праграмаваньня. Але што канкрэтна значыць “добрыя падыходы” ў дачыньні праграмаваньня?

Некаторыя праграмісты маюць прыкладна такую філязофію: “Ня мае сэнсу як выглядае мой код – галоўнае што ён працуе. Няма сэнсу выкарыстоўваць АА калі яно запавольвае мяне; для мяне важна даставіць праграму як мага хутчэй канчатковаму карыстальніку. Іншыя праграмісты могуць лічыць, што мой код выглядае пачварна, але мае вынікі хуткія.” Вось чаму ня варта пагаджацца з такой філязофіяй.

Фігачыць код хутка й не прыгожа – гэта для тых хто на PHP стварае інтэрнэт-крамы. Праграмаваньне гэта па-першае мастацтва, гэта самае чыстае ствараньне.

Што заўсёды стала ў праграмаваньні гэта тое, што яно заўсёды зьмяняецца. Заўсёды будуць новыя асаблівасьці, якія трэба дадаць, і новыя багі, якія трэба фіксіць.

Таму, выдатнае вызначэньне добрага кода – гэта: добры код добры тады, калі яго можна лёгка зьмяніць.

АА гэта моцная магчымасьць пісаць код, які лёгка зьмяніць. Трымайце гэтыя думкі ў галаве.

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

Пачнем!

Яшчэ адна гульня ў косьці

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

Вось праграма якую я напісаў каб гуляць у Pig на кампутары. Стварыце тэчку pig; у ёй стварыце файл user_interface.rb. Надрукуйце код які знаходзіцца ніжэй у user_interface.rb каб у Вас была копія таго, што мы будзем рабіць.

puts "Welcome to Pig!\n\n"
player1_points =0
player2_points =0until player1_points >=100|| player2_points >=100
  puts "Player 1, your turn.\n\n"
  turn_points =0
  roll =nil
  player_choice =niluntil roll ==1|| player_choice =='h'
    roll = rand(1..6)
    puts "You rolled a #{roll}."
    turn_points += roll
    if roll ==1
      turn_points =0else
      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
    endend

  player1_points += turn_points
  puts "You got #{turn_points} points this turn.\n\n"
  puts "You have #{player1_points} points total."


  puts "Player 2, your turn.\n\n"
  turn_points =0
  roll =nil
  player_choice =niluntil roll ==1|| player_choice =='h'
    roll = rand(1..6)
    puts "You rolled a #{roll}."
    turn_points += roll
    if roll ==1
      turn_points =0else
      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
    endend

  player2_points += turn_points
  puts "You got #{turn_points} points this turn."
  puts "You have #{player2_points} points total.\n\n\n"endif player1_points > player2_points
  puts "Congratulations, Player 1. You win!"else
  puts "Congratulations, Player 2. You win!"end

Існуе вельмі шмат розных вэрсіяў гульні Pig. Вы можаце выкарыстоўваць дзьве косткі: калі толькі адна костка роўная аднаму, тады Вы атрымліваеце 0, а калі дзьве, тады 25; калі Вы атрымліваеце дзьве іншыя аднолькавыя лічбы, сума іх падвойваецца. Як Вы здзейсьціне гэта ў кодзе? А што калі гуляць у Pig утраёх? А калі 100 гульцоў? Альбо пытаць колькасьці карыстальнікаў у пачатку гульні? Альбо зрабіць 8 бакоў у костцы: два бакі па 1 й два па 6?

Наколькі складана будзе зьмяніць Вам гэтый код?

Давайце адрэфактарым гэтый код, выкарыстоўваючы прынцыпы аб’ектна-арыентаванага праграмаваньня.

Маделяваньне аб’ектаў

ААП – гэта проста мадэляваньне сістэмы карыстаючыся класамі й аб’ектам, як галоўным рухомымі часткамі сістэмы. Мы пачнем рэфактарынг з таго, што створым клас Гульца:

puts "Welcome to Pig!\n\n"
player1_points =0
player2_points =0

Зьменім на:

puts "Welcome to Pig!\n\n"
player1 =Player.new
player2 =Player.new

Вы скажаце “пачакайце!”. “Ня існуе яшчэ класа Player!” Вы маеце рацыю. Гэтак жа як і ў мінулай частцы, дзе мы пісалі юніт-тэсты каб зразумець якое ў нас будзе API, тут мы таксама спачатку пішам проста інтэрфэйс, каб зразумець якія ў нас будуць юніт-тэсты. Вось мой першы юніт тэст:

Player.new should be an instance of Player.

Гэтак жа як і раней стварыце файл player_spec.rb у тэчцы spec і дадайце вашую спеку туды.

Зараз давайце зробім каб гэтый тэст выканаўся. Стварыце тэчку lib і файл player.rb у ёй, і надрукуйце:

classPlayerend

Зараз нам толькі засталося падгрузіць player.rb у наш інтэрфэйс:

require'./lib/player'

puts "Welcome to Pig!\n\n"
player1 =Player.new
player2 =Player.new

Працягнем з стварэньнем класаў. Паглядзіце на гэты радок:

until player1_points >=100|| player2_points >=100

і зьмяніце яго на:

until player1.points >=100|| player2.points >=100

Окей, падаецца што зараз нам патрэбен мэтад Player#points. Юніт тэст гэтага мэтада:

player = Player.new
player.points should equal 0.

Рэалізуем наш клас:

classPlayer
  attr_reader :points

  def initialize
    @points=0endend

Пераканайцеся што Вы зьмянілі ўсе зьвяртаньні да player1_points і player2_points у кодзе.

Энкапсуляцыя

Паглядзіце на гэты радок:

player1.points += turn_points

Давайце зьменім яго на:

player1.add_points(turn_points)

Зьменіце Player 2 таксама.

У Вас можа быць пытаньне чаму я проста не выкарыстоўваю attr_accessor :points для класа Player. Як мы ўжо ведаем, выкарыстоўваньне attr_accessor дазваляе усім каму ўзгодна зьмяняць значэньне @points звонку: адымаць і нават прысвойвась нялікавыя значэньні ў зьменную.

У ААП кажуць што выкарыстоўваньне “пісачэй” ламае энкапсуляцыю. Энкапсуляцыя – гэта хаваньне сапраўднага становішча аб’екта ад знешняга сьвета й стварэньне толькі кантралюемых сродкаў для яго зьмяненьня.

Вяртаемся да коду. Наш юніт-тэст:

player = Player.new
player.add_points(4)
player.points should equal 4.

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

classPlayerdef add_points(new_points)@points+= new_points
  endend

Не Паўтарайце Сябе (Don’t Repeat Yourself)

Я працягваю казаць “Не забудзьцеся зьменіць і другога гульца таксама.” Ёсьць прынцы ў распрацоўцы які завецца Don’t Repeat Yourself, або DRY. Ідэя ў тым, што калі Вы паўтараеце Ваш код, робіцца складаней зьмяніць Вашую праграму. Нашмат складаней даведацца што трэба зьмяніць і дзе; складаней зразумець Ваш код; і нашмат прасьцей нарабіць памылак, забыўшыся зьмяніць ва ўсіх месцах.

Давайце падчысьцім Ваш код:

puts "Welcome to Pig!\n\n"
players =[Player.new("Player 1"),Player.new("Player 2")]until players.select{|player| player.points >=100}.any?
  players.each do|player|
    puts "#{player.name}, your turn.\n\n"
    turn_points =0
    roll =nil
    player_choice =niluntil roll ==1|| player_choice =='h'
      roll = rand(1..6)
      puts "You rolled a #{roll}."
      turn_points += roll
      if roll ==1
        turn_points =0else
        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
      endend

    player.add_points(turn_points)
    puts "You got #{turn_points} points this turn."
    puts "You have #{player.points} points total.\n\n\n"endend

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

Гэты код мае шмат рэчаў, якія яшчэ не рэалізаваны. Ваша чарга напісаць юніт-тэсты і потым стварыць іх.

Вы ўжо напэўна заўважылі што зараз праграмай можна карыстацца й ня толькі для двух гульцоў.