Conflitto nei nomi dei metodi getter e setter in ruby api design

1

Sto ereditando una decisione API in un SDK che sto scrivendo dove sono richiesto di recuperare oggetti di dominio (voci) dal server in questo modo:

blogEntries = client.content_type('blog').entries

Come puoi vedere, il setter per la proprietà content_type qui è parametrizzato. Per implementare questo design la mia classe Client ha un metodo come questo che imposta la variabile di istanza @content_type prima di passarla ad altri oggetti:

def content_type(content_type_uid)
  @content_type = content_type_uid
  // do something with @content_type
end

Ora altrove nella classe quando ho bisogno di recuperare il content_type non posso più chiamare un metodo attr_reader come configuration.content_type perché è in conflitto con il metodo sopra. Ora che mi costringe ad avere un metodo getter separato chiamato get_content_type che è in realtà un ruby non idiomatico.

Come faccio a uscire da questa situazione conflittuale in cui invece di un setter convenzionale come content_type= ho un setter con una firma diversa? Che tipo di trade-off avrebbe più senso?

    
posta Amit Erandole 17.01.2017 - 13:41
fonte

2 risposte

3

Idealmente, dovresti evitare di avere getter o setter del tutto. Il principio fondamentale di OO è astrazione comportamentale , i tuoi oggetti dovrebbero fare cose, non solo cose "store".

Se assolutamente devi avere getter e setter, la cosa migliore da fare sarebbe attenersi alle convenzioni standard Ruby:

def content_type=(content_type_uid)
  @content_type = content_type_uid
  # do something with @content_type
end

Se ciò non è possibile, puoi provare a "sovraccaricare" il metodo basato su arity:

def content_type(content_type_uid = getter = true)
  return @content_type if getter
  @content_type = content_type_uid
  # do something with @content_type
end

Se lo fai, dovresti cercare come documentare questo metodo come due distinti metodi "sovraccaricati" in qualunque sistema di documentazione tu stia usando. Per esempio. YARD ha un @overload tag:

# @overload content_type
#   Gets the content type
#   @return [ContentType] The content type
# @overload content_type(content_type_uid)
#   Sets the content type to +content_type_uid+
#   @param content_type_uid [ContentType::Uid] The UID of the content type to set
def content_type(content_type_uid = getter = true)
  return @content_type if getter
  @content_type = content_type_uid
  # do something with @content_type
end

Oppure, potresti usare il supporto di YARD per gli attributi / metodi sintetici:

#   Sets the content type to +content_type_uid+
#   @param content_type_uid [ContentType::Uid] The UID of the content type to set
def content_type(content_type_uid = getter = true)
  return @content_type if getter
  @content_type = content_type_uid
  # do something with @content_type
end

# !attribute [r] content_type
#   Gets the content type
#   @return [ContentType] The content type

Nota comunque che questo "sovraccarico" ha uno svantaggio significativo: mentre puoi rappresentarlo nella documentazione, non c'è modo di rappresentarlo nella lingua. Quindi, quando tu (come faccio spesso) usi la reflection per esplorare alcune API non familiari, vedrai solo un metodo con un parametro opzionale senza alcuna indicazione del fatto che quell'unico metodo è in realtà due metodi diversi travestiti:

method(:content_type).parameters
#=> [[:opt, :content_type_uid]]

Normalmente, un parametro opzionale viene utilizzato per fornire, beh, argomenti facoltativi, non per passare da due comportamenti completamente diversi.

    
risposta data 17.01.2017 - 14:45
fonte
2

La risposta a questa domanda potrebbe essere nella risposta a Che cos'è un livello di anti-corruzione e come viene utilizzato .

Una citazione di una citazione dalla risposta accettata :

Above example is based on how Anticorruption Layer is explained at c2 wiki:

If your application needs to deal with a database or another application whose model is undesirable or inapplicable to the model you want within your own application, use an AnticorruptionLayer to translate to/from that model and yours.

Hai una classe con un comportamento funky. Potrebbe essere una buona idea creare una classe di wrapping che "ripari" il comportamento strano e nasconda il client:

class ClientWrapper
  attr_reader :content_type

  CONTENT_TYPE_BLOG = 'blog'

  def initialize(client)
    @client = client
  end

  def blog_entries
    @content_type = CONTENT_TYPE_BLOG
    @client.content_type(@content_type).entries
  end
end

Ora puoi fare:

client = ClientWrapper.new SomeClient.new
blog_entries = client.blog_entries
puts client.content_type # -> 'blog'

In alternativa, forse hai bisogno di comprimere insieme il tipo di contenuto e i dati, insieme a un livello anticorruzione:

class ClientWrapper
  CONTENT_TYPE_BLOG = 'blog'

  def initialize(client)
    @client = client
  end

  def blog_entries
    ClientData.new CONTENT_TYPE_BLOG, @client.content_type(CONTENT_TYPE_BLOG).entries
  end
end

class ClientData
  attr_reader :content_type, :data

  def initialize(content_type, data)
    @content_type = content_type
    @data = data
  end
end

Ora l'uso di questo aspetto è:

client = ClientWrapper.new SomeClient.new
blog_entries = client.blog_entries
puts blog_entries.content_type # -> 'blog'
puts blog_entries.data

Il mio Ruby è un po 'arrugginito, ma puoi creare la tua versione del metodo each sui dati del cliente usando una sottoclasse:

class ClientArrayData < ClientData
  def each(&block)
    if data.respond_to? :each
      data.each block
    else
      block.call data
    end
  end
end

O semplicemente aggiungi il metodo giusto nella classe ClientData . Quindi puoi usarlo in modo simile ad altri oggetti iterabili:

client.blog_entries.each do |entry|
  # Do stuff with 'entry'
end
    
risposta data 17.01.2017 - 14:58
fonte

Leggi altre domande sui tag