best practice in development

27
Best Practice in development CDD, MVC principles, Rails hints 1 / 27

Upload: mirrec

Post on 07-Jan-2017

23 views

Category:

Software


0 download

TRANSCRIPT

Page 1: Best Practice in Development

Best Practice in developmentCDD, MVC principles, Rails hints

1 / 27

Page 2: Best Practice in Development

Our WorkflowComment Driven Development

1. write integration test in comments

2. rewrite comments to excecutable integration test

3. implement basic test infrastrucuture

4. think about architecture and design

5. implement object using TDD

2 / 27

Page 3: Best Practice in Development

NeutrinoHandling bounced emails using Amazon SNS

Background

application for sending promo emailssimilar to MailChimp

emails are send by Amazon::SESwe have to handle bounce emails due to our reputation in Amazonwe desided to implement it using Amazon::SNSwe have to handle request with json data

3 / 27

Page 4: Best Practice in Development

Write integration test in commentsrequire "spec_helper"

describe "Amazon bounce handling process" do # We have subscriptions on email "[email protected]" in different clients # When last email to him was returned with "permanent failure" # Then all subscription on email "[email protected]" are marked as bouncedend

rewrite task to commentsyou will ensure that you understand what you are suppose to docomments can be send to project manager to ensure task was understoodproperlychanges on this level cost you nothingsomething like cucumber, but without parsing

4 / 27

Page 5: Best Practice in Development

Rewrite comments to parsable testdescribe "Amazon bounce handling process" do it "marks all subcriptions as bounced on permanent failure notifications" do # Given different_active_subscription_on("[email protected]")

# When receive_permanent_failure_notification(on_receiver: "[email protected]")

# Then all_subscriptions_on("[email protected]").should be_bounced endend

just get comments to some ruby parseable formyou express all you customed matchestest should be as readable as possible

5 / 27

Page 6: Best Practice in Development

Implement test infrastrucutureconverts error test to failing test

implement all helper methods and custom matchesimplement basic application infrastrucutreat the end you have failing test

matcher :be_bounced do match do |actual_subscriptions| actual_subscriptions.all? do |subscription| subscription.status == "bounced" end endend

6 / 27

Page 7: Best Practice in Development

def different_active_subscription_on(email) subscriber_1 = create(:subscriber, email: email, client: create(:client)) subscriber_2 = create(:subscriber, email: email, client: create(:client)) # ... subscription1, subscription2end

def receive_permanent_failure_notification(options) post "/amazon_sns/bounces", permanent_failure_raw_data(options), { "CONTENT_TYPE" => 'text/plain' }end

def permanent_failure_raw_data(options) '{ "notificationType":"Bounce",' # ....end

namespace :amazon_sns do post "bounces", :to => "bounce_listeners#handle"end

class AmazonSns::BounceListenersController < ApplicationController def handle render :nothing => true endend

7 / 27

Page 8: Best Practice in Development

Think about architecture and designclass AmazonSns::BounceListenersController < ApplicationController def handle bounce_object = bounce_parser(request_data) bounce_handler.handle(bounce_object)

# bounce_handler bounce_object.emails.each do |email| subscription_marker.mark_as_bounced(email) end

render :nothing => true endend

you think how you wish to have object communicationas least conditions as possibledesigning object messenging is as writing test

8 / 27

Page 9: Best Practice in Development

Implement object using TDDnow you know what methods and objects do you needjust implement them in TTD style

�rst write unitestthen write implementationif you found test hard to write you should once again think aboutarchitecture

at the end you should have all tests (unit, integration) in green

9 / 27

Page 10: Best Practice in Development

How do we codeskinny controller, skinny model as wellbusiness logic in separate classes

avoid callbacks in model to send emailORM is just input / output layerextract simple pure ruby objects (puro)keep views simple & stupid (kiss)

10 / 27

Page 11: Best Practice in Development

How do we testmodels (active record classes) are tested in integration testsdata objects are tested using unit testsservice objects are tested using mocks, to test object interactioncontrollers do not have special testshole stack (user interface, application, database, etc.) is covert by integrationtests

11 / 27

Page 12: Best Practice in Development

UniligaShowcase of code samples

Background

small portal for sport players in Bratislavarandom players are put into the groups according to their skills, then theyplay matches with each otheraccording to their results, they go to upper or bottom league

sample is from season ticket ordering partplayer can order ticket from their pro�leplayer is noti�ed by email

12 / 27

Page 13: Best Practice in Development

Skinny Controllers def create @season_ticket = SeasonTicket.new(params[:season_ticket]) @season_ticket.season = SeasonManager.for_ordering @season_ticket.player = current_customer

@notifiers = [ MailerNotifier.new(SeasonTicketMailer, :ordered_notification) ]

if @season_ticket.valid? SeasonTicketOrderer.new(@season_ticket, @notifiers).order! notice_message redirect_to wnm_core.profile_path else # ... end end

only creates objectsall if branches are covered in integration test

13 / 27

Page 14: Best Practice in Development

Service objectclass SeasonTicketOrderer def initialize(ticket, notifiers = []) @ticket, @notifiers = ticket, notifiers end

def order! @ticket.order! @notifiers.each{ |notifier| notifier.notify(@ticket) } endend

class SeasonTicketMailer < ActionMailer::Base def ordered_notification(ticket) @ticket = ticket mail :to => @ticket.player_email, :subject => t("mailers.season...") end

MailerNotifier.new(SeasonTicketMailer, :ordered_notification)

no coupling between model and emailany other noti�er can by passed to orderer class

14 / 27

Page 15: Best Practice in Development

ORM as output targetclass SeasonTicket < ActiveRecord::Base def order! self.price = calculate_price self.status = :ordered save! self end

class Group < ActiveRecord::Base def self.create_with_players(number, players, league, round) create!( :sequence => number, :league_id => league.id, :round_id => round.id, :players => players ) end

subs, mocks in services classes are easycreates thin wrapper around active record

15 / 27

Page 16: Best Practice in Development

ORM as input sourceclass SeasonTicket < ActiveRecord::Base # ... scope :payed, lambda{ where(:status => "payed") } scope :for_sport_region, lambda{ |s_region_id| where(:sport_region_id => s_region_id) }

def self.payed_for_region_season(region, season) payed.for_region_season(region, season) end

def self.for_region_season(region, season) for_sport_region(region).for_season(season) end # ... end

select index are clearly visiblesubs, mocks in services classes are easy

16 / 27

Page 17: Best Practice in Development

Extract PURE from modelclass SeasonTicket < ActiveRecord::Base # ... def due_date @due_date ||= DueDateCalculator.new(season).calculate endend

class DueDateCalculator def initialize(season) @season = season end

def calculate @season.upcoming_round.start_date - 1.day endend

unit tests for DueDateCalculator do not have to load railssingle responsibility principle for object classes

17 / 27

Page 18: Best Practice in Development

Use exceptionclass SeasonManager NoSeason = Class.new(StandardError)

def self.active Season.active end

def self.upcoming Season.upcoming end

def self.for_ordering if active && active.upcoming_round? active elsif upcoming upcoming else raise SeasonManager::NoSeason.new( "we are missing upcoming season, create one in admin" ) end endend

18 / 27

Page 19: Best Practice in Development

Handle exception properlyclass SeasonTicketsController < ApplicationController rescue_from SeasonManager::NoSeason, :with => :no_season_apologize

def create @season_ticket = SeasonTicket.new(params[:season_ticket]) # this call can raise SeasonManager::NoSeason @season_ticket.season = SeasonManager.for_ordering # ... end

def no_season_apologize flash[:alert] = t("season_tickets.messages.no_season_apologize.alert") redirect_to main_app.profile_path endend

19 / 27

Page 20: Best Practice in Development

Views Principleskeep it simple and stupid

no logicnot deep object chaningtest in emails do not create database objectsuse objects to simplify views

20 / 27

Page 21: Best Practice in Development

Bad patternsTied couplingclass Shop < ActiveRecord::Base after_save :when_tags_changed

def when_shop_created if id_changed? ShopMailer.shop_created(self).deliver end endend

solve with service class

21 / 27

Page 22: Best Practice in Development

Callback hell

solve with PURE and service objects

class Shop < ActiveRecord::Base before_validation :uniform_shop_url, :fill_in_catalog_name, :add_url_protocol before_save :update_service_level, :create_shop_user, :when_ppc_daily_limit_changed after_save :when_tags_changed, :when_xml_changed, :when_shop_changed, :when_shop_created after_destroy :clean_shop_leftovers, :recache_shop_categories, :add_category_url_to_history

def uniform_shop_url @invalid_url = false return unless url.present?

local_url = url.strip local_url = local_url[0..-2] if local_url[-1] == '/' if local_url[0..6] != "http://" && local_url[0..7] != "https://" local_url = "http://"+local_url end

uri = Addressable::URI.parse(local_url) # ... rescue Addressable::URI::InvalidURIError @invalid_url = true endend

22 / 27

Page 23: Best Practice in Development

it will workclass GalleryItem < ActiveRecord::Base def upload_thumbnail_from_video vid = YouTubeIt::Client.new.video_by(video_url) self.remote_image_url = vid.thumbnails.first save! endend

solve with thin wrapper VideoThumbnailGrabbertest wrapper in integrationthen you can stub your wrapper

describe "upload_thumbnail_from_video" do it "should set thumbnail from youtube if video was changed, not blank, and image was not changed" gi = GalleryItem.new

YouTubeIt::Client.any_instance.should_receive(:video_by) .with("http://www.youtube.com/watch?v=Nba3Tr_GLZU").and_return(double()

gi.video_url = "http://www.youtube.com/watch?v=Nba3Tr_GLZU" gi.upload_thumbnail_from_video.should be_true endend

23 / 27

Page 24: Best Practice in Development

heavy view

solve with null object, methods, new objects

<% if profile.add_to_shadowbox?(profile.payed_subcategory_id(@category_id) || @category_subtree_ids) %> <%= link_to profile.gallery_item_opened_url( profile.payed_subcategory_id(@category_id) || @category_subtree_ids), :title => profile.gallery_item(profile.payed_subcategory_id(@category_id) || <%= image_tag(profile.gallery_item_thumb_url( profile.payed_subcategory_id(@category_id) || @category_subtree_ids), :alt => profile.name.to_s) %> <% end %><% end %>

24 / 27

Page 25: Best Practice in Development

<% if profile.has_image_or_video? %> <%= link_to profile.gallery_item_zoom_url, :rel => gallery_name do %> <%= image_tag(profile.gallery_item_thumb_url, :alt => profile.name.to_s) %> <% end %><% end %>

class ProfilePresenter attr_reader :profile, :category

delegate :id, :name, :phone, :email, :url, :link, :to => :profile delegate :has_video?, :has_image_or_video?, :to => :gallery_item delegate :zoom_url, :thumb_url, :description, :to => :gallery_item, :prefix =>

def initialize(profile, category) @profile = profile @category = category end

def gallery_item @gallery_item ||= profile.gallery_items_ordered(category).first || NullGalleryItem.new endend

25 / 27

Page 26: Best Practice in Development

Why ?easier maintenance: lots of small objects with one responsibility are betterthan one big object with a lot of code and a lot of responsibilitybetter architecture [not sure about what you want to say here]fewer bugsinterfaces (object borders) are well de�nedit is much easier to swap some object for another if I want to changesomethingonly few request tests are neededunit testing is much easiertests that do not load the whole Rails stack are extremely fast

26 / 27

Page 27: Best Practice in Development

Referenceshttps://www.destroyallsoftware.com/screencastshttp://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/http://www.confreaks.com/videos/1314-rubyconf2012-boundarieshttp://www.confreaks.com/videos/759-rubymidwest2011-keynote-architecture-the-lost-years

27 / 27