Коментари за шеста задача

  1. Сигурно вече сте видели, че задача 6 е проверена и на част от решенията има коментари :)
    Тъй като не всяко решение има подробен коментар, тук ще направя един обзор на често срещани проблеми в решенията.

    (линк към нашето решение)

    Нестилови бележки

    Основният (и май единствен) често срещан не-стилов проблем е при избора на място за методите, генериращи ASCII и HTML код. Главната част от задачата се състои точно в това да организирате кода на логически единици (класове и модули), всеки от които има единствена задача. Например, задачата на Turtle е да създаде една матрица, която да запълни с числа чрез DSL. Turtle не трябва да се интересува от това как се рисува после тази матрица.

    В голям брой решения има вариации на следния код:

    def draw(canvas = nil, &block)
      instance_eval &block
    
      case canvas
        when Canvas::ASCII then canvas.draw_ascii(@canvas)
        when Canvas::HTML then canvas.draw_html(@canvas)
        else @canvas
      end
    end
    

    В някои решения методите draw_ascii и draw_html са в класа Turtle. За това са отнемани точки, защото така класът започва да се грижи за твърде много неща и в даден момент става неуправляем. Има прекалено много различни видове логика на едно място - методи, които без проблем могат да са самостоятелни. Ако това беше реален код в програма и се наложи да се добави още един вид рендериране, този код ще расте и ще се оплита още повече.

    Да се върнем на горния пример. Методите са в съответните им класове, но все още има проблем - класът Turtle трябва да знае за всеки един вид canvas. Хубаво е да се стараем да правим нещата така, че добавяне на (сравнително очаквана) функционалност да става с минимален брой промени по минимален брой файлове. Ако се наложи да добавим нов вид Canvas, трябва да напишем нов клас, но и да отидем в Turtle#draw и да го добавим и там. Последното не винаги е очевидно, че трябва да се случи. В един по-голям проект много лесно може да се пропусне. Особено ако извикването му става на повече места.

    def draw(canvas = nil, &block)
      instance_eval &block
    
      if canvas
        canvas.draw(@canvas)
      else
        @canvas
      end
    end
    

    Това решава проблема. Не се интересуваме какво ни е подадено за canvas, стига то да има метод draw, приемащ матрица.

    Хубаво е да се стремим да свеждаме логическите разклонения до минимум. Логически разклонения са основно if-овете и други конструкции, които правят различни неща в зависимост от някакво условие. Тестването (автоматизирано и ръчно) на код с малък брой логически разклонения е много по-лесно.

    Горният if може да се премахне като просто си дефинираме един canvas Matrix:

    module Canvas
      class Matrix
        def draw(canvas)
          canvas
        end
      end
    end
    
    class Turtle
      ...
    
      def draw(canvas = Canvas::Matrix.new, &block)
        instance_eval &block
    
        canvas.draw(@canvas)
      end
    end
    

    Така draw винаги прави едно и също. В автоматизирани тестове ще имаме два случая - ако е подаден canvas, и ако не е. Във втория случай очакваното поведение ще е същото - да се викне метода draw на подаденото.

    Стилови бележки

    • Не дефинирайте класове по този начин: class TurtleGraphics::Turtle. Така в тялото на класа търсенето на константи (и други класове) се променя. Не се търси в TurtleGraphics. Ако напишете Canvas::ASCII, очаквайки да ви върне TurtleGraphics::Canvas::ASCII - няма да се получи.
    • Не използвайте класови променливи вместо константи. Избягвайте ги и за други неща. Ако нещо трябва да е "статично" и не се променя - трябва да е константа.
    • Паралелното присвояване е прекалено излишно използвано. Лоша практика е да правите @variable_one, @variable_two = variable_one, variable_two само за да си спестите един ред. Кодът става по-неясен, защото на един ред се случват повече неща. А като трябва да се добави нова променлива или стават три неща, или се губи консистентност и третият става отделен.
    • Използвайте private като маркер, не като модификатор към всеки метод. private се слага сам на ред, огражда се с празни редове и под него се пишат методите. Забравете за private def име() .... Не пишем на Java/C#/PHP/... :)
    • Не следвайте сляпо условието. В ASCII сме дефинирали символите като интервали от стойности, но това не означава, че най-добрият начин да се напише в код е да се изгенерират интервалите. Сметката е много проста: (intensity * (@symbols.size - 1)).ceil
    • Не конкатенирайте низове. Използвайте или интерполация, или форматиране, или Array#join. reduce също е неподходящ начин за това, защото вкарва излишна сложност, особено ако тялото му не е просто.
    • Избягвайте създаването на "builder" методи, като def closing_tags "</body></html>" end. HTML-а е идеален случай за използване на форматиране. Целият HTML е един темплейт, в който просто трябва да се вкарат някакви неща (било то чрез интерполация или форматиране). От "builder" методи има смисъл, ако са сравнително независими, могат да се параметризират и има много варианти за структурата на крайния резултат. В случая, като се промени нещо от едната част на HTML-а е много вероятно да се промени и другата част.
    • Не оставяйте празни редове след дефиницията на клас или модул. Пише го в style guide-а.
    • "<td>#{format('%.2f', intensity)}</td>" и '<td>%s</td>' % format('%.2f', intensity) са като if a == true then true else false end. Защо не просто '<td>%.2f</td>' % intensity? Между другото, format(string, data) и string % data правят едно и също.
    • В доста решения има if-ове, които много лесно могат да се заменят с оператора %. @x % @width вместо if @x >= @width then 0 elsif @x < 0 then @width - 1 else @x end
    • Не слагайте коментари на затварящия end на клас или модул - end # HTML. Ако сте си свършили работата добре, класовете ще са достатъчно малки и ще си личи по индентацията кое от кой клас е.
    • Не дефинирайте помощни модули/класове извън вашия. Така рискувате да предефинирате функционалност на вече създаден клас с това име. Така ще се счупи и вашият, и чуждият код едновременно.

    TL;DR;

    Ако не искате да ви вземем точка от следващото домашно - прочетете бележките :)

    Коментари и въпроси

    Ако не сте съгласни с някое от тези неща - напишете си мнението, подкрепено с доводи "защо". Нека стане дискусия - така се ражда истината. Питайте и ако нещо ви интересува защо е направено така в нашето решение.

    Също, ще се радвам ако някой, който си е писал тестове за задачата и иска да получи коментари за тях, ги постне тук :)

  2. Здравейте,

    Ще има ли подобен feedback и за пета задача? Тя беше доста обемна, предполагам има какво да се изпише? Знам, че решението е качено в github - пише го във форума и коментарите, но ще е хубаво да има подобно събрано описание на проблемите. Не всеки може да си направи изводи, като гледа чужд код.

    Поздрави!

Трябва да сте влезли в системата, за да може да отговаряте на теми.