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