└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Rails Refactoring Cheatsheet 2 | Based on [How To Refactor Big Rails Projects](https://medium.com/@korolvs/how-to-refactor-big-rails-projects-12fc4e4ddcd2) 3 | 4 | ## Table of Content 5 | - [General](#general) 6 | - [Model](#model) 7 | - [Repository](#repository) 8 | - [Operation](#operation) 9 | - [Controller](#controller) 10 | - [View](#view) 11 | 12 | ## General 13 | - Add comments and documentation using [YARD](https://yardoc.org/) 14 | ``` 15 | # Creates a dog and booth for a dog 16 | # 17 | # @param context [Hash] context of operation(current user, current city etc.) 18 | # @param params [Hash, ApplicationController::Params] attributes of new dog 19 | # 20 | # @return [Dog] 21 | # 22 | # @raise [UnauthorizedError] if action is forbidden 23 | # @raise [ValidationError] if dog or booth is not valid 24 | # 25 | # @see DogsRepository 26 | # @see BoothsRepository 27 | def call(context:, params:) 28 | #... 29 | end 30 | ``` 31 | - Try to write code that will be easy to test(create small methods that you can easy stub) 32 | ``` 33 | # In main code 34 | def repo 35 | DogsRepository.new 36 | end 37 | 38 | def call(params) 39 | dog = repo.create(params) 40 | #... 41 | end 42 | 43 | # In specs 44 | let(:repo) { instance_double('DogsRepository') } 45 | before do 46 | allow(create_operation).to receive(:repo).and_return(repo) 47 | allow(repo).to receive(:create).and_return(dog) 48 | # or 49 | allow(repo).to receive(:create).and_raise(ValidationError) 50 | end 51 | ``` 52 | - Don't forget to add integration tests if you use stubbing and mocking a lot 53 | - Follow some styleguide. Check your code with [Rubocop](https://github.com/rubocop-hq/rubocop) 54 | - Ask your teammembers if you hesitating at any point 55 | 56 | ## Model 57 | ### Good 58 | - Place in the model only validations, associations, and methods that actually are related to the model. 59 | ``` 60 | class Dog < ApplicationRecord 61 | belongs_to :breed 62 | has_one :booth 63 | validate :name, presence: true 64 | validates :age, numericality: { greater_than: 0 }, presence: true 65 | 66 | # Barking 67 | def bark 68 | #... 69 | end 70 | 71 | private 72 | 73 | def am_i_cute? 74 | true 75 | end 76 | end 77 | ``` 78 | - You can keep in the model all "extentions" you use 79 | ``` 80 | class Dog < ApplicationRecord 81 | acts_as_paranoid 82 | #... 83 | dragonfly_accessor :image 84 | end 85 | ``` 86 | 87 | ### Bad 88 | - Don't use callbacks like `after_*` and `before_*`. Put this code in `Repository` if it is DB-related code and in `Operation` if it is business logic 89 | ``` 90 | class Dog < ApplicationRecord 91 | before_save :calculate_size # Bad - Move it to Repository 92 | after_create :create_booth # Bad - Move it to Operation 93 | end 94 | ``` 95 | - Don't use scopes in the model. Move them to `Repository` 96 | ``` 97 | class Dog < ApplicationRecord 98 | scope :all, -> { where('age > 1') } # Bad - Move it to Repository 99 | 100 | # Bad - Move it to Repository 101 | def self.puppies 102 | Dog.where('age < 1') 103 | end 104 | end 105 | ``` 106 | - Don't use nested attributes for associations. Move creating, updating and deleting associations to `Operation` 107 | ``` 108 | class Dog < ApplicationRecord 109 | accepts_nested_attributes_for :booth # Bad - Move this logic to Operation 110 | end 111 | ``` 112 | - Don't call Repositories and Operations from the model. Repositories should be called from Operations and Operations should be called from other operations or layer above them(Controller, API, Console etc.) 113 | ``` 114 | class Dog < ApplicationRecord 115 | def bark() 116 | similar_dog = DogsRepository.find_similar_to(self) # Bad - Move to Operation 117 | DogsOperations::Bark.new.call(similar_dog) # Bad - Move to Operation 118 | end 119 | end 120 | ``` 121 | 122 | ## Repository 123 | ### Good 124 | - Place here all your code which is responsible for querying(such as scopes) and managing entities(like creating and updating) 125 | ``` 126 | class DogsRepository 127 | attr_reader :model 128 | 129 | def initialize(model: Dog) 130 | @model = model 131 | end 132 | #... 133 | def all() 134 | model.where('age > 1.0') # was a default scope 135 | end 136 | 137 | #... 138 | def with_breed(breed) 139 | all.where(breed: breed) 140 | end 141 | 142 | #... 143 | def pugs() 144 | all.joins(breed: {name: 'Pug'}) 145 | end 146 | 147 | #... 148 | def puppies() 149 | Dog.where('age <= 1.0') 150 | end 151 | 152 | #... 153 | def find_by_id(id) 154 | dog = all.find_by(id: id) 155 | raise NotFoundError, {id: id} unless dog 156 | dog 157 | end 158 | 159 | #... 160 | def create(params) 161 | dog = model.new(params) 162 | dog.size = dog.get_size_from_breed_and_age # was a callback before 163 | raise ValidationError, dog unless dog.save 164 | dog 165 | end 166 | end 167 | ``` 168 | - It's ok to have different repositories for one model 169 | ``` 170 | class Dogs::SmallDogsRepository 171 | #... 172 | end 173 | 174 | class Dogs::OldDogsRepository 175 | #... 176 | end 177 | ``` 178 | ### Bad 179 | - Don't change other models in the repository of the model. It should be done in `Operation` and use another `Repository` 180 | ``` 181 | class DogsRepository 182 | #... 183 | def create(params) 184 | dog = Dog.create(params) 185 | booth = Booth.create(dog) # Bad - Move it to operation and use another repository 186 | #... 187 | end 188 | end 189 | ``` 190 | - Don't call other Repositories and Operations from the repository. Repositories should be called from Operations and Operations should be called from other operations or layer above them(Controller, API, Console etc.) 191 | ``` 192 | class DogsRepository 193 | #... 194 | def create(params) 195 | dog = Dog.create(params) 196 | booth = BoothsRepository.new.create(dog: dog) # Bad - Move to Operation 197 | DogsOperations::Bark.new.call(dog) # Bad - Move to Operation 198 | #... 199 | end 200 | end 201 | ``` 202 | ## Operation 203 | ### Good 204 | - Place here a code related to one business operation. Operation should have one public method `call`. 205 | ``` 206 | module DogsOperations 207 | class Create 208 | attr_reader :dogs_repo, :booths_repo 209 | 210 | def initialize(dogs_repo: DogsRepository.new, booths_repo: BoothsRepository.new) 211 | @dogs_repo = dogs_repo 212 | @booths_repo = booths_repo 213 | end 214 | 215 | #... 216 | def call(context:, params:) 217 | authorize(context) 218 | dog_params = prepare_params(params) 219 | dog = dogs_repo.create(dog_params) 220 | create_booth_for(dog) 221 | dog 222 | end 223 | 224 | #... 225 | end 226 | end 227 | ``` 228 | - It's ok to call other operations from `Operation`. Place all dependencies in `initialize` method and create an `attr_reader` for each dependency to make it easy to mock in tests. 229 | ``` 230 | module DogsOperations 231 | class Create 232 | attr_reader :dogs_repo, :bark_operation 233 | 234 | def initialize(dogs_repo: DogsRepository.new, bark_operation: DogsOperations::Bark.new) 235 | @dogs_repo = dogs_repo 236 | @bark_operation = bark_operation 237 | end 238 | 239 | #... 240 | end 241 | end 242 | 243 | # In tests 244 | describe DogsOperations::Create do 245 | let(:repo) { instance_double('DogsRepository') } 246 | let(:bark) { instance_double('DogsOperations::Bark') } 247 | let(:create_operation) { DogsOperations::Create.new(dogs_repo: repo, bark_operation: bark) } 248 | #... 249 | end 250 | ``` 251 | ### Bad 252 | - Don't call model methods that are database related. Use `Repository` instead. 253 | ``` 254 | module DogsOperations 255 | class Create 256 | #... 257 | 258 | def call(context:, params:) 259 | #... 260 | dog = Dog.create(dog_params) # Bad - Use DogsRepository#create instead 261 | #... 262 | end 263 | 264 | #... 265 | end 266 | end 267 | ``` 268 | - Don't add to many different actions in one operation. Divide it to smaller operations and call them. Keep your operations rather small and clean 269 | ``` 270 | module DogsOperations 271 | class Create 272 | #... 273 | 274 | def call(context:, params:) 275 | action_1() 276 | action_2() 277 | action_3() 278 | #... 279 | action_42() # Bad - Divide to smaller operations 280 | end 281 | 282 | #... 283 | end 284 | end 285 | ``` 286 | ## Controller 287 | ### Good 288 | - Place here code responsible for calling operations and deciding what to render depending on results and happened errors. 289 | ``` 290 | class DogsController < ApplicationController 291 | #... 292 | def create 293 | @dog = DogOperations::Create.new.(params: params) 294 | render_dog() 295 | rescue UnauthorizedError => e 296 | handle_unauthorized_error(e) 297 | rescue ValidationError => e 298 | handle_validation_error(e) 299 | end 300 | 301 | #... 302 | end 303 | ``` 304 | - It's ok to have methods used in your views in `Controller` while you keep it clear 305 | ``` 306 | class DogsController < ApplicationController 307 | #... 308 | 309 | helper_method :can_create? 310 | 311 | private 312 | 313 | def can_create? 314 | #... 315 | end 316 | end 317 | ``` 318 | ### Bad 319 | - Don't call Repositories and Model methods from the controller. They should be called from Operations. Controller only get results of Operations and render it 320 | ``` 321 | class DogsController < ApplicationController 322 | def create 323 | @dog = Dog.create(params) # Bad - Call an Operation instead 324 | @dog = DogsRepository.create(params) # Bad - Call an Operation instead 325 | end 326 | end 327 | ``` 328 | ## View 329 | ### Good 330 | - Use views as templates not as a place where you decide what to render 331 | ``` 332 | .title 333 | = title 334 | .main 335 | - if can_create? 336 | = a 337 | - else 338 | = b 339 | ``` 340 | ### Bad 341 | - Don't put any business logic in views 342 | ``` 343 | .title 344 | = title 345 | .main 346 | - if current_user.can?(:whatever) # Bad - Create a method in controller and call it here 347 | = a 348 | - else 349 | = b 350 | ``` 351 | - Don't use helpers for not global methods. Add methods to contoller and use `helper_method` instead 352 | ``` 353 | class DogsController < ApplicationController 354 | #... 355 | 356 | helper_method :can_create? 357 | 358 | private 359 | 360 | def can_create? 361 | #... 362 | end 363 | end 364 | ``` 365 | 366 | --------------------------------------------------------------------------------