No começo sua aplicação era perfeita


Sua aplicação vai mudar. E pode virar uma bagunça.


O design pode salvá-lo

Mas como?

Design Stamina Hypothesis

Quais são os indicios de um design ruim?

Rígido

Difícil de mudar (cada mudança causa muitas mudanças em outras partes do sistema)

Frágil

Facilmente quebrável (Cada mudança quebra coisas distantes e não relacionadas)

Imóvel

Reutilizar é impossível (o código está irremediavelmente emaranhado)

Viscoso

Dureza na preservação do design (fazer as coisas certas é mais difícil do que fazer as coisas erradas)

O que é SOLID

Isso nos ajuda a escrever um código com:

  • Injeção de dependência

  • Responsabilidade única

  • Pode ser reorganizado

  • Reutilizável

  • Facilmente testável

  • Single Responsibility

  • Open Closed

  • Liskov Substitution

  • Interface Segregation

  • Dependency Inversion

Principio da Responsábilidade unica

Nunca deve haver mais de um motivo para uma classe mudar.

class FinancialReportMailer
  def initialize(transactions, account)
    @transactions = transactions
    @account = account
    @report = ''
  end

  def generate_report!
    @report = @transactions.map {
      |t| "amount: #{t.amount} type: #{t.type} date: #{t.created_at}"
    }.join("\n")
  end

  def send_report
    Mailer.deliver(
      from: 'reporter@example.com',
      to: @account.email,
      subject: 'your report',
      body: @report
    )
  end
end

mailer = FinancialReportMailer.new(transactions, account)
mailer.generate_report!
mailer.send_report
class FinancialReportMailer
  def initialize(report, account)
    @report = report
    @account = account
  end

  def deliver
    Mailer.deliver(
      from: 'reporter@example.com',
      to: @account.email,
      subject: 'Financial report',
      body: @report
    )
  end
end

class FinancialReportGenerator
  def initialize(transactions)
    @transactions = transactions
  end

  def generate
    @transactions.map { |t| "amount: #{t.amount} type: #{t.type} date: #{t.created_at}"
    }.join("\n")
  end
end

report = FinancialReportGenerator.new(transactions).generate
FinancialReportMailer.new(report, account).deliver

Principio Aberto-Fechado

[Open Closed Principle]

  • Um módulo deve ser aberto para extensão, mas fechado para modificação
class Logger
  def initialize(format, delivery)
    @format = format
    @delivery = delivery
  end

  def log(string)
    deliver format(string)
  end

  private

  def format(string)
    case @format
    when :raw
      string
    when :with_date
      "#{Time.now} #{string}"
    when :with_date_and_details
      "Log was creates at #{Time.now}, please check details #{string}"
    else
      raise NotImplementedError
    end
  end

  def deliver(text)
    case @delivery
    when :by_email
      Mailer.deliver(
        from: 'emergency@example.com',
        to: 'admin@example.com',
        subject: 'Logger report',
        body: text
      )
    when :by_sms
      client = Twilio::REST::Client.new('ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', 'your_auth_token')
      client.account.messages.create(
        from: '+15017250604',
        to: '+15558675309',
        body: text
      )
    when :to_stdout
      STDOUT.write(text)
    else
      raise NotImplementedError
    end
  end
end

logger = Logger.new(:raw, :by_sms)
logger.log('Emergency error! Please fix me!')
class Logger
  def initialize(formatter: DateDetailsFormatter.new, sender: LogWriter.new)
    @formatter = formatter
    @sender = sender
  end

  def log(string)
    @sender.deliver @formatter.format(string)
  end
end

class LogSms
  def initialize
    @account_sid = 'ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
    @auth_token = 'your_auth_token'
    @from = '+15017250604'
    @to = '+15558675309'
  end

  def deliver(text)
    client.account.messages.create(from: @from, to: @to, body: text)
  end

  private

  def client
    @client ||= Twilio::REST::Client.new(@account_sid, @auth_token)
  end
end

class LogMailer
  def initialize
    @from = 'emergency@example.com'
    @to = 'admin@example.com'
    @sublect = 'Logger report'
  end

  def deliver(text)
    Mailer.deliver(
      from: @from,
      to: @to,
      subject: @sublect,
      body: text
    )
  end
end

class LogWriter
  def deliver(log)
    STDOUT.write(text)
  end
end

class DateFormatter
  def format(string)
    "#{Time.now} #{string}"
  end
end

class DateDetailsFormatter
  def format(string)
    "Log was creates at #{Time.now}, please check details #{string}"
  end
end

class RawFormatter
  def format(string)
    string
  end
end

logger = Logger.new(formatter: RawFormatter.new, sender: LogSms.new)
logger.log('Emergency error! Please fix me!')

Principio da substituição de Liskov

[Liskov Substitution Principle]

As subclasses devem ser substituíveis por suas classes base

class UserStatistic
  def initialize(user)
    @user = user
  end

  def posts
    @user.blog.posts
  end
end

class AdminStatistic < UserStatistic
  def posts
    user_posts = super

    string = ''
    user_posts.each do |post|
      string += "title: #{post.title} author: #{post.author}\n" if post.popular?
    end

    string
  end
end
class UserStatistic
  def initialize(user)
    @user = user
  end

  def posts
    @user.blog.posts
  end
end

class AdminStatistic < UserStatistic
  def posts
    user_posts = super
    user_posts.select { |post| post.popular? }
  end

  def formatted_posts
    posts.map { |post| "title: #{post.title} author: #{post.author}" }.join("\n")
  end
end

Principio da segregação da interface

[Interface Segregation Principle]

Muitas interfaces específicas do cliente são melhores do que uma interface de propósito geral

class CoffeeMachineInterface
  def select_drink_type
      # select drink type logic
  end

  def select_portion
     # select portion logic
  end

  def select_sugar_amount
     # select sugar logic
  end

  def brew_coffee
     # brew coffee logic
  end

  def clean_coffee_machine
    # clean coffee machine logic
  end

  def fill_coffee_beans
    # fill coffee beans logic
  end

  def fill_water_supply
    # fill water logic
  end

  def fill_sugar_supply
    # fill sugar logic
  end
end

class Person
  def initialize
    @coffee_machine = CoffeeMachineInterface.new
  end

  def make_coffee
    @coffee_machine.select_drink_type
    @coffee_machine.select_portion
    @coffee_machine.select_sugar_amount
    @coffee_machine.brew_coffee
  end
end

class Staff
  def initialize
    @coffee_machine = CoffeeMachineInterface.new
  end

  def serv
    @coffee_machine.clean_coffee_machine
    @coffee_machine.fill_coffee_beans
    @coffee_machine.fill_water_supply
    @coffee_machine.fill_sugar_supply
  end
end
class CoffeeMachineUserInterface
  def select_drink_type
      # select drink type logic
  end

  def select_portion
     # select portion logic
  end

  def select_sugar_amount
     # select sugar logic
  end

  def brew_coffee
     # brew coffee logic
  end
end

class CoffeeMachineServiceInterface
  def clean_coffee_machine
    # clean coffee machine logic
  end

  def fill_coffee_beans
    # fill coffee beans logic
  end

  def fill_water_supply
    # fill water logic
  end

  def fill_sugar_supply
    # fill sugar logic
  end
end

class Person
  def initialize
    @coffee_machine = CoffeeMachineUserInterface.new
  end

  def make_coffee
    @coffee_machine.select_drink_type
    @coffee_machine.select_portion
    @coffee_machine.select_sugar_amount
    @coffee_machine.brew_coffee
  end
end

class Staff
  def initialize
    @coffee_machine = CoffeeMachineServiceInterface.new
  end

  def serv
    @coffee_machine.clean_coffee_machine
    @coffee_machine.fill_coffee_beans
    @coffee_machine.fill_water_supply
    @coffee_machine.fill_sugar_supply
  end
end

Principio da inversão da dependencia

[Dependency Inversion Principle]

Depende de abstrações. Não dependa de concreções

class Printer
  def initialize(data)
    @data = data
  end

  def print_pdf
    PdfFormatter.new.format(@data)
  end

  def print_html
    HtmlFormatter.new.format(@data)
  end
end

class PdfFormatter
  def format(data)
    # format data to Pdf logic
  end
end

class HtmlFormatter
  def format(data)
    # format data to Html logic
  end
end
class Printer
  def initialize(data)
    @data = data
  end

  def print(formatter: PdfFormatter.new)
    formatter.format(@data)
  end
end

class PdfFormatter
  def format(data)
    # format data to Pdf logic
  end
end

class HtmlFormatter
  def format(data)
    # format data to Html logic
  end
end

Porque Designer

TDD não é suficiente DRY não é suficiente

Projete porque você espera que seu aplicativo tenha sucesso (e mude no futuro)

Referências

Sandy Metz - SOLID Design Principle in Ruby

SOLID com TypeScript // Live #37

SOLID (O básico para você programar melhor) // Dicionário do Programador

SOLID Object-Oriented Design Principles

SOLID Object-Oriented Design