1--- 2stage: none 3group: unassigned 4info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments 5--- 6 7# Guidelines for reusing abstractions 8 9As GitLab has grown, different patterns emerged across the codebase. Service 10classes, serializers, and presenters are just a few. These patterns made it easy 11to reuse code, but at the same time make it easy to accidentally reuse the wrong 12abstraction in a particular place. 13 14## Why these guidelines are necessary 15 16Code reuse is good, but sometimes this can lead to shoehorning the wrong 17abstraction into a particular use case. This in turn can have a negative impact 18on maintainability, the ability to easily debug problems, or even performance. 19 20An example would be to use `ProjectsFinder` in `IssuesFinder` to limit issues to 21those belonging to a set of projects. While initially this may seem like a good 22idea, both classes provide a very high level interface with very little control. 23This means that `IssuesFinder` may not be able to produce a better optimized 24database query, as a large portion of the query is controlled by the internals 25of `ProjectsFinder`. 26 27To work around this problem, you would use the same code used by 28`ProjectsFinder`, instead of using `ProjectsFinder` itself directly. This allows 29you to compose your behavior better, giving you more control over the behavior 30of the code. 31 32To illustrate, consider the following code from `IssuableFinder#projects`: 33 34```ruby 35return @projects = project if project? 36 37projects = 38 if current_user && params[:authorized_only].presence && !current_user_related? 39 current_user.authorized_projects 40 elsif group 41 finder_options = { include_subgroups: params[:include_subgroups], only_owned: true } 42 GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute 43 else 44 ProjectsFinder.new(current_user: current_user).execute 45 end 46 47@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) 48``` 49 50Here we determine what projects to scope our data to, using three different 51approaches. When a group is specified, we use `GroupProjectsFinder` to retrieve 52all the projects of that group. On the surface this seems harmless: it is easy 53to use, and we only need two lines of code. 54 55In reality, things can get hairy very quickly. For example, the query produced 56by `GroupProjectsFinder` may start out simple. Over time more and more 57functionality is added to this (high level) interface. Instead of _only_ 58affecting the cases where this is necessary, it may also start affecting 59`IssuableFinder` in a negative way. For example, the query produced by 60`GroupProjectsFinder` may include unnecessary conditions. Since we're using a 61finder here, we can't easily opt-out of that behavior. We could add options to 62do so, but then we'd need as many options as we have features. Every option adds 63two code paths, which means that for four features we have to cover 8 different 64code paths. 65 66A much more reliable (and pleasant) way of dealing with this, is to simply use 67the underlying bits that make up `GroupProjectsFinder` directly. This means we 68may need a little bit more code in `IssuableFinder`, but it also gives us much 69more control and certainty. This means we might end up with something like this: 70 71```ruby 72return @projects = project if project? 73 74projects = 75 if current_user && params[:authorized_only].presence && !current_user_related? 76 current_user.authorized_projects 77 elsif group 78 current_user 79 .owned_groups(subgroups: params[:include_subgroups]) 80 .projects 81 .any_additional_method_calls 82 .that_might_be_necessary 83 else 84 current_user 85 .projects_visible_to_user 86 .any_additional_method_calls 87 .that_might_be_necessary 88 end 89 90@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) 91``` 92 93This is just a sketch, but it shows the general idea: we would use whatever the 94`GroupProjectsFinder` and `ProjectsFinder` finders use under the hoods. 95 96## End goal 97 98The guidelines in this document are meant to foster _better_ code reuse, by 99clearly defining what can be reused where, and what to do when you can not reuse 100something. Clearly separating abstractions makes it harder to use the wrong one, 101makes it easier to debug the code, and (hopefully) results in fewer performance 102problems. 103 104## Abstractions 105 106Now let's take a look at the various abstraction levels available, and what they 107can (or cannot) reuse. For this we can use the following table, which defines 108the various abstractions and what they can (not) reuse: 109 110| Abstraction | Service classes | Finders | Presenters | Serializers | Model instance method | Model class methods | Active Record | Worker 111|:-----------------------|:-----------------|:---------|:------------|:--------------|:------------------------|:----------------------|:----------------|:-------- 112| Controller | Yes | Yes | Yes | Yes | Yes | No | No | No 113| Service class | Yes | Yes | No | No | Yes | No | No | Yes 114| Finder | No | No | No | No | Yes | Yes | No | No 115| Presenter | No | Yes | No | No | Yes | Yes | No | No 116| Serializer | No | Yes | No | No | Yes | Yes | No | No 117| Model class method | No | No | No | No | Yes | Yes | Yes | No 118| Model instance method | No | Yes | No | No | Yes | Yes | Yes | Yes 119| Worker | Yes | Yes | No | No | Yes | No | No | Yes 120 121### Controllers 122 123Everything in `app/controllers`. 124 125Controllers should not do much work on their own, instead they simply pass input 126to other classes and present the results. 127 128### Grape endpoint 129 130Everything in `lib/api`. 131 132### Service classes 133 134Everything that resides in `app/services`. 135 136Services should consider inheriting from: 137 138- `BaseContainerService` for services scoped by container (project or group) 139- `BaseProjectService` for services scoped to projects 140- `BaseGroupService` for services scoped to groups 141 142or, create a new base class and update the list above. 143 144Legacy classes inherited from `BaseService` for historical reasons. 145 146In Service classes the use of `execute` and `#execute` is preferred over `call` and `#call`. 147 148Classes that are not service objects should be [created elsewhere](directory_structure.md#use-namespaces-to-define-bounded-contexts), such as in `lib`. 149 150#### ServiceResponse 151 152Service classes usually have an `execute` method, which can return a 153`ServiceResponse`. You can use `ServiceResponse.success` and 154`ServiceResponse.error` to return a response in `execute` method. 155 156In a successful case: 157 158```ruby 159response = ServiceResponse.success(message: 'Branch was deleted') 160 161response.success? # => true 162response.error? # => false 163response.status # => :success 164response.message # => 'Branch was deleted' 165``` 166 167In a failed case: 168 169```ruby 170response = ServiceResponse.error(message: 'Unsupported operation') 171 172response.success? # => false 173response.error? # => true 174response.status # => :error 175response.message # => 'Unsupported operation' 176``` 177 178An additional payload can also be attached: 179 180```ruby 181response = ServiceResponse.success(payload: { issue: issue }) 182 183response.payload[:issue] # => issue 184``` 185 186### Finders 187 188Everything in `app/finders`, typically used for retrieving data from a database. 189 190Finders can not reuse other finders in an attempt to better control the SQL 191queries they produce. 192 193### Presenters 194 195Everything in `app/presenters`, used for exposing complex data to a Rails view, 196without having to create many instance variables. 197 198See [the documentation](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/presenters/README.md) for more information. 199 200### Serializers 201 202Everything in `app/serializers`, used for presenting the response to a request, 203typically in JSON. 204 205### Model class methods 206 207These are class methods defined by _GitLab itself_, including the following 208methods provided by Active Record: 209 210- `find` 211- `find_by_id` 212- `delete_all` 213- `destroy` 214- `destroy_all` 215 216Any other methods such as `find_by(some_column: X)` are not included, and 217instead fall under the "Active Record" abstraction. 218 219### Model instance methods 220 221Instance methods defined on Active Record models by _GitLab itself_. Methods 222provided by Active Record are not included, except for the following methods: 223 224- `save` 225- `update` 226- `destroy` 227- `delete` 228 229### Active Record 230 231The API provided by Active Record itself, such as the `where` method, `save`, 232`delete_all`, and so on. 233 234### Worker 235 236Everything in `app/workers`. 237 238Use `SomeWorker.perform_async` or `SomeWorker.perform_in` to schedule Sidekiq 239jobs. Never directly invoke a worker using `SomeWorker.new.perform`. 240