Ruby string literals VS Value objects. Nadměrné inženýrství?

Původní příspěvek:

Průvodce, jak se zbavit řetězcových literálů v Ruby with Rails 5 Attributes API

V mém předchozím článku jsem ukázal, jak můžete pomocí rozhraní Rails 5 Attributes API s JSONB a hodnotovými objekty vylepšit design vaší aplikace.

Dnes vám chci ukázat, jak lze Atributy API použít na anti-vzorec Primitive Obsession refaktorů.

Primitivní datové typy jsou základní vestavěné stavební bloky jazyka. Obvykle jsou psány jako int, string nebo konstanty. Protože vytváření takovýchto polí je mnohem snazší než vytvoření zcela nové třídy, vede to ke zneužívání. Proto je tento zápach jedním z nejběžnějších.

  • Použití primitivních datových typů k reprezentaci nápadů domény. Například pomocí celého čísla představuje částku peněz nebo řetězec pro telefonní číslo.
  • Použití proměnných nebo konstant pro kódování informací. Případ, který se často vyskytuje, používá konstanty pro odkazování na uživatelské role nebo pověření (například USER_ADMIN = 1;).
  • Použití řetězců jako názvů polí v datových polích.

Podívejme se na tento kód.

třída Plan :: Channel  {where (type: "# {type}")}
  konec

  atribut: úrovně, plán :: kanál :: úrovně :: Type.new
konec

Konstanta CALCULATION_METHODS je definována jako zmrazené pole řetězců a představuje způsob výpočtu kanálu. Na začátku všechno vypadá docela dobře.

třída plán :: CreatePlanForm 

Ale najednou…

třída Provize :: Kalkulačka :: SlidingScale 

A znovu …

třída provize :: Kalkulačka
  #…

  třída << samostatně
    def kalkulačka (preimage)
      case preimage.channel.calculation_method
      když 'sliding_scale_net_origination_fee',
           'sliding_scale_origination_volume'
        Provize :: Kalkulačka :: SlidingScale
      když 'byt'
        Provize :: Kalkulačka :: Byt
      když se šíří
        Provize :: Kalkulačka :: Spread
      konec
    konec
  konec

  #…
konec

To mi opravdu připadá hrozně. Změníme-li hodnoty v CALCULATION_METHODS, existuje velká šance, že je jednoduše zapomeneme aktualizovat v případě operátorů. Bylo by lepší se vyhnout použití takového nebezpečného kódu.

Primitivní posedlosti se rodí ve chvílích slabosti. "Prostě pole pro ukládání některých dat!" Prohlásil programátor. Vytvoření primitivního pole je mnohem snazší než vytvořit zcela novou třídu, že? A tak se to stalo. Pak bylo potřeba další pole a přidáno stejným způsobem.

Primitivy se často používají k „simulaci“ typů. Takže místo samostatného datového typu máte sadu čísel nebo řetězců, které tvoří seznam přípustných hodnot pro nějakou entitu. Snadno pochopitelná jména jsou pak těmto konkrétním číslům a řetězcům dána konstantami, a proto jsou šířena široko daleko.

První přístup spočívá v tom, že refaktoror CALCULATION_METHODS je konstantní na čtyři nezávislé konstanty, takže můžeme dospět k něčemu podobnému.

třída Plan :: Channel 

Ale tento kód mi zase nevypadá dobře. Dalším přístupem by bylo změnit tento kód pomocí AR Enum API, ale my to uděláme jiným způsobem.

Umožňuje znovu použít Atributy API. Ve sloupcích plan_channels, což je String, máme sloupec calculation_method. Měli bychom ji přidat jako atribut do modelu Plan :: Channel a definovat typ.

třída Plan :: Channel  {where (type: "# {type}")}
  konec

  atribut: úrovně, plán :: kanál :: úrovně :: Type.new
  Atribut: calculation_method, Plan :: Channel :: VýpočetMethod :: Type.new
konec
vyžadují „suchý inicializátor“

třída plán :: Kanál :: VýpočetMethod
  prodloužit Dry :: Initializer

  FLAT = 'flat'.freeze
  private_constant: FLAT

  SPREAD = 'spread'.freeze
  private_constant: SPREAD

  SLIDING_SCALE_NET_ORIGINATION_FEE = 'sliding_scale_net_origination_fee'.freeze
  private_constant: SLIDING_SCALE_NET_ORIGINATION_FEE

  SLIDING_SCALE_ORIGINATION_VOLUME = 'sliding_scale_origination_volume'.freeze
  private_constant: SLIDING_SCALE_ORIGINATION_VOLUME

  SLIDING_SCALE = [
    SLIDING_SCALE_NET_ORIGINATION_FEE,
    SLIDING_SCALE_ORIGINATION_VOLUME
  ].zmrazit
  private_constant: SLIDING_SCALE

  param: value, proc (&: to_s)

  def self.values
    [FLAT, SPREAD, SLIDING_SCALE] .flatten
  konec

  def to_s
    value.to_s
  konec

  hodnoty.each do | v |
    define_method "# {v}?" dělat
      value == v
    konec
  konec

  def sliding_scale?
    SLIDING_SCALE.include? (Value)
  konec

  class Type 

Plan :: Channel :: VýpočetMethod se stal objektem Value. Co vývojář získal z refaktoringu?

  • Místo sady primitivních hodnot má programátor plnohodnotnou třídu se všemi výhodami, které nabízí objektově orientované programování (zadávání dat podle názvu třídy, typu hinting atd.).
  • Ověření dat není třeba se obávat, protože lze nastavit pouze očekávané hodnoty.
  • Když se logika VýpočetMethod rozšíří, bude shromážděna na jednom místě, které je jí věnováno.
třída Provize :: Kalkulačka :: SlidingScale 
třída provize :: Kalkulačka
  třída << samostatně
    výpočet def (předběžné)
      get (preimage) .calculate
    konec

    def get (preimage)
      kalkulačka (preimage) .new (preimage)
    konec

    def kalkulačka (preimage)
      if preimage.channel.calculation_method.sliding_scale?
        Provize :: Kalkulačka :: SlidingScale
      elsif preimage.channel.calculation_method.flat?
        Provize :: Kalkulačka :: Byt
      elsif preimage.channel.calculation_method.spread?
        Provize :: Kalkulačka :: Spread
      konec
    konec
  konec

  attr_reader: preimage

  delegát: skupina,: půjčka,: kanál,: do =>: preimage

  def inicializovat (preimage)
    @preimage = preimage
  konec

  def
    preimage.amount = sazba * účtováno_množství / 100
  konec
konec

Totéž by mělo být provedeno s TYPY konstantou. A poté vypadá náš model v suchu a čistotě.

třída Plan :: Channel  {where (type: "# {type}")}
  konec

  atribut: úrovně, plán :: kanál :: úrovně :: Type.new
  Atribut: calculation_method, Plan :: Channel :: VýpočetMethod :: Type.new
  Atribut: type, Plan :: Channel :: Type :: Type.new
konec

Poslední věcí, kterou je třeba udělat, je přejmenovat sloupec plan_channels # type. Myslím, že zdrojem by bylo správné jméno a poté máme konečnou verzi našeho modelu.

třída Plan :: Channel  {where (source: "# {source}")}
  konec

  atribut: úrovně, plán :: kanál :: úrovně :: Type.new
  Atribut: calculation_method, Plan :: Channel :: VýpočetMethod :: Type.new
  Atribut: source, Plan :: Channel :: Source :: Type.new
konec

Není to nadměrné inženýrství? Celá nová třída místo řetězce? Opravdu? Odpověď na takovou otázku je vždy závislá na kontextu, ale zřídka jsem zjistila, že je nadměrná.

Můžete tvrdit, že 15 minut je hodně, ve srovnání s nulovými minutami by to trvalo, kdybyste použili řetězec. Na povrchu se to jeví jako platný protiargument, ale možná zapomínáte, že s primitivním řetězcem stále musíte psát validační a obchodní logiku „kolem“ řetězce, a musíte si pamatovat, abyste tuto logiku důsledně uplatňovali napříč celou vaší základnou kódu. Domnívám se, že na to budete věnovat více než 15 minut a na odstraňování závad, ke kterým dojde, když někdo zapomene použít jedno z těchto pravidel na řetězec v jiné části základny kódu.

Prozkoumejte další obsah na serveru JetRockets.pro

Igor Alexandrov
CTO ve společnosti JetRockets