07. Втора задача и няколко принципа

07. Втора задача и няколко принципа

07. Втора задача и няколко принципа

31 октомври 2012

Днес

Решенията

Принципи

Интерфейс vs. имплементация

Code Smells

...или защо кода ви мирише

Single Responsibility Principle

Решението

Collection.parse

class Collection
  def self.parse(text)
    songs = text.lines.each_slice(4).map do |name, artist, album|
      Song.new name.chomp, artist.chomp, album.chomp
    end

    new songs
  end
end

Ще обърнем внимание на няколко неща

Collection.parse

краткост

Може да го запишем по-кратко, но предпочитаме предния запис, защото е по-разбираем

text.lines.each_slice(4).map do |lines|
  Song.new(*lines.take(3).map(&:chomp))
end

Collection.parse

конструиране на песен

Song.new name.chomp, artist.chomp, album.chomp

Обърнете внимание:

Song

class Song
  attr_reader :name, :artist, :album

  def initialize(name, artist, album)
    @name   = name
    @artist = artist
    @album  = album
  end
end

Collection#initialize

Нищо драматично:

class Collection
  def initialize(songs)
    @songs = songs
  end
end

Collection < Enumerable

class Collection
  include Enumerable

  def each(&block)
    @songs.each(&block)
  end
end

Collection#artists

и приятели

class Collection
  def artists
    @songs.map(&:artist).uniq
  end

  def names
    @songs.map(&:name).uniq
  end

  def albums
    @songs.map(&:album).uniq
  end
end

Duplicated Code

Code smell

Сух код

class Collection
  def artists
    collect_unique :artist
  end

  private

  def collect_unique(attribute)
    @songs.map(&attribute).uniq
  end
end

Collection#filter

class Collection
  def filter(criteria)
    Collection.new @songs.select { |song| criteria.met_by? song }
  end
end

Обърнете внимание:

Събития тази седмица

Collection#adjoin

class Collection
  def adjoin(other)
    adjoined_songs = (songs + other.songs).uniq
    Collection.new adjoined_songs
  end
end

Равенство на песни

Равенство на песни

class Song
  def ==(other)
    name == other.name and
      artist == other.artist and
      album == other.album
  end

  alias eql? ==

  def hash
    [@name, @other, @album].hash
  end
end

Критерии

@songs.select { |song| criteria.met_by? song }

Първо решениe

Criteria.name

module Criteria
  def self.name(name)
    NameMatches.new name
  end

  class NameMatches < Criterion
    def initialize(name)
      @name = name
    end

    def met_by?(song)
      song.name == @name
    end
  end
end

Criteria.artist

class ArtistMatches < Criterion
  def initialize(artist)
    @artist = artist
  end

  def met_by?(song)
    song.artist == @artist
  end
end

Criteria.album

class AlbumMatches < Criterion
  def initialize(album)
    @album = album
  end

  def met_by?(song)
    song.album == @album
  end
end

Обърнете внимание

Базовия клас

class Criterion
  def &(other)
    Conjunction.new self, other
  end

  def |(other)
    Disjunction.new self, other
  end

  def !
    Negation.new self
  end
end

Conjunction

class Conjunction < Criterion
  def initialize(left, right)
    @left  = left
    @right = right
  end

  def met_by?(song)
    @left.met_by?(song) and @right.met_by?(song)
  end
end

Disjunction

class Disjunction < Criterion
  def initialize(left, right)
    @left  = left
    @right = right
  end

  def met_by?(song)
    @left.met_by?(song) or @right.met_by?(song)
  end
end

Negation

class Negation < Criterion
  def initialize(criterion)
    @criterion = criterion
  end

  def met_by?(song)
    not @criterion.met_by?(song)
  end
end

За първия вариант

Малка вариация

def Criteria.artist(artist)
  AttributeMatches.new :artist, artist
end

class AttributeMatches
  def initialize(attribute, value)
    @attribute = attribute
    @value     = value
  end

  def met_by?(song)
    song.send(@attribute) == value
  end
end

Втори вариант

Criteria.artist

module Criteria
  def self.artist(artist)
    Criterion.new { |song| song.artist == artist }
  end
end

Criterion

class Criterion
  def initialize(&block)
    @matcher = block
  end

  def met_by?(song)
    @matcher.call song
  end
end

Операции за комбиниране

class Criterion
  def &(other)
    Criterion.new { |song| self.met_by?(song) and other.met_by?(song) }
  end

  def |(other)
    Criterion.new { |song| self.met_by?(song) or other.met_by?(song) }
  end

  def !
    Criterion.new { |song| not met_by?(song) }
  end
end

Първи срещу втори вариант

Inappropriate intimacy

Code smell

Въпроси