Our guidelines for building new applications and managing legacy systems.
For us doesn't not matter the language you choose to develop the application. It's importat to follow some rules when you decide to implement a different language that we are describing in other section of this document.
The main objective of this section is to make sure we follow the rules below independent of the language we choose. We like Sandi Metzs Rules for OOP. These rules have helped us to keep our code simpler to understand and easier to change.
A class can be no longer than 100 lines of code. If a class has more than 100 lines of code, the possibility for this class to have more than one reason to change is pretty high
A method can be no longer than 5 lines of code.
- The first rule of functions is that they should be small. The second rule of functions is that they should be smaller than that. (Clean Code by Robert Martin) *
5 lines of code equals to 1 if statement
So this rule is asking us to
- Do not write logic more complicated than an if statement
- Write only 1 line per branch
- Never use elsif
Every method we write can actually split into 4 basic steps and 2 optional steps
- Collecting input
- Performing work
- Delivering output
- Handling failures
- (Optional) Diagnostics
- (Optional) Cleanup
Base on this assumption, we can extract every step into a helper method and make every method tell a nice story. And every method will only contain ~5 lines of method calls.
Pass no more than 4 parameters into a method.
Hash options are also parameters Do not fool ourselves with a hash full of parameters.
If there are more than 4 parameters, most of the time we can pull a new object out of some of the parameters.
A view template should only refer to 1 object.
Facade Pattern: A facade is an object that provides a simplified interface to a larger body of code. In Rails world, it's usually called Presenter.
class ViewFacade
def initialize(params)
@object = params[:id]
@params = params
end
def object_attribute
object.attribute
end
def associated_object_attribute
object.associated_object.attribute
end
private
attr_accessor :params, :object
end
Testing will provide a few main benefits when done correctly. First, it will help you develop the behaviors within your app. You may write a test that your model is validating the presence of an email. Then realizing you needed to include validations for the password or add a “retype password” input box.
Another great reason to test is to protect yourself against regressions within your application as your modifying code or adding new features. This will also give you confidence and assurance when your refactoring classes, collapsing code or removing extraneous code. If written tests pass both before and after changes you should be good to go! Another testing method is to write a test after you find an issue or bug in your application. Ensuring the fix works and does not plague your application in the future.
In our application we are using RSPEC 3 for unit tests and integration tests. We are following the pattern define in the better specs website. http://www.betterspecs.org/
Rails follows a Model-View-Controller pattern. This raises questions around where programming logic should go once a Ruby on Rails application reaches a certain size. Generally, the principles are:
- Forget fat Models (don’t allow them to become bloated)
- Keep Views dumb (so don’t put complex logic there)
- And Controllers skinny (so don’t put too much there)
So where should you put anything that’s more than a simple query or action?
One way to support Object-Oriented Design is with the Service Object. This can be a class or module in Ruby that performs an action. It can help take out logic from other areas of the MVC files.
Our template for service looks like the code below:
- Strong initializer to receive parameters
- One public method to expose the service. The other methods should be private.
- Strong result from our service. Our services always return a Result objecti with the result of the operation, the error and the object
- Our service always handle expections
class ServiceName < ApplicationService
def initialize(params)
@params = params
end
def call
Result.new(true, nil, object)
rescue StandardError => e
Result.new(false, e.message, nil)
end
private
attr_accessor :params
end
- 100 lines per class: Simplified version of SLOC
- 5 lines per method: Simplified version of SLOC and Cyclomatic Complexity
- 4 parameters per method: Simplified version of ABC metric
- 1 object per view: Simplified version of ABC metric
Our application is hosted at Heroku. Our deployment process is manual. So, you need to add in your git configuration two new remotes.
[remote "production"]
url = https://git.heroku.com/padrepauloricardo.git
fetch = +refs/heads/*:refs/remotes/production/*
[remote "staging"]
url = https://git.heroku.com/stg-padrepauloricardo.git
fetch = +refs/heads/*:refs/remotes/staging/*
We have travisCI for continuos integration. We didn't implement any hook for continuous delivery, so you need to run the commands below in order to deploy our application to staging server or production server.
git push production:master
git push staging:master
- We want to start working with continuous delivering for our application. We are improving the test coverage in order to stat that.
- Using Kubernetes. Our application already runs using Docker, so our next step would be configure our rails application to run using Kubernetes
- Choose another host. We are considering three new possibilities: Amazon AWS, Google Cloud or Digital Ocean.
We prefer to adopt "API First" and "API as a Product" as key engineering principles. In a nutshell, API First encompasses a set of quality-related standards (including the API guidelines and tooling) and fosters a peer review culture; it requires two aspects:
- define APIs outside the code first using a standard specification language (Open API 2.0)
- get early review feedback from peers and client developers
We prefer REST-based APIs with JSON payloads to SOAP. Distributed SOAs following the REST style have a looser coupling between client and server implementations and comes with less rigid client/server contracts that do not break if either side make certain changes. Hence it is easier to build interoperating distributed systems that can be evolved in parallel by different teams while continuing to work. REST-like APIs with JSON payload is the most widely accepted and used service interfacing style in the internet web service industry.
Be conservative in what you send, be liberal in what you accept. APIs must be evolved without breaking any consumers.
For an easy understanding use this structure for every resource:
Do not use verbs:
- /getAllCars
- /createNewCar
- /deleteAllRedCars
Use PUT, POST and DELETE methods instead of the GET method to alter the state. Do not use GET for state changes:
- GET /users/711?activate or
- GET /users/711/activate
Do not mix up singular and plural nouns. Keep it simple and use only plural nouns for all resources.
- /cars instead of /car
- /users instead of /user
- /products instead of /product
- /settings instead of /setting
If a resource is related to another resource use subresources.
- GET /cars/711/drivers/ Returns a list of drivers for car 711
- GET /cars/711/drivers/4 Returns driver #4 for car 711
Both, client and server, need to know which format is used for the communication. The format has to be specified in the HTTP-Header.
Content-Type defines the request format. Accept defines a list of acceptable response formats.
Filtering
Use a unique query parameter for all fields or a query language for filtering.
Sorting
Allow ascending and descending sorting over multiple fields.
Paging
Use limit and offset. It is flexible for the user and common in leading databases. The default should be limit=20 and offset=0
- GET /cars?offset=10&limit=5
To send the total entries back to the user use the custom HTTP header: X-Total-Count.
Links to the next or previous page should be provided in the HTTP header link as well. It is important to follow this link header values instead of constructing your own URLs.
Make the API Version mandatory and do not release an unversioned API. Use a simple ordinal number and avoid dot notation such as 2.5.
We are using the url for the API versioning starting with the letter „v“
It is hard to work with an API that ignores error handling. Pure returning of a HTTP 500 with a stacktrace is not very helpful.
All exceptions should be mapped in an error payload. Here is an example how a JSON payload should look like.
{
"errors": [
{
"userMessage": "Sorry, the requested resource does not exist",
"internalMessage": "No car found in the database",
"code": 34,
"more info": "http://dev.mwaysolutions.com/blog/api/v1/errors/12345"
}
]
}
Some proxies support only POST and GET methods. To support a RESTful API with these limitations, the API needs a way to override the HTTP method.
Use the custom HTTP Header X-HTTP-Method-Override to overrider the POST Method.
- https://blog.mwaysolutions.com/2014/06/05/10-best-practices-for-better-restful-api/
- https://github.com/zalando/engineering-principles
- https://opensource.zalando.com/restful-api-guidelines/
- https://opensource.zalando.com/restful-api-guidelines/index.html#principles
- https://jobs.zalando.com/tech/blog/so-youve-heard-about-radical-agility...-video/