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?
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