As promised, I talk about how we migrated our backend to Go and were able to reduce the amount of business logic on the client by more than a third.
For whom : small companies, Go and mobile developers, as well as everyone who is in trend or just interested in this topic.
What about : the reasons for switching to Go, the difficulties encountered, as well as instructions and tips for improving the architecture of a mobile application and its backend.
Level : junior and middle.
For a long time, our mobile outsourcing development team worked on third-party projects that had their own backend developers, and we acted as a contractor for a specific product. Despite the fact that the agreements have always been clearly stated that it was we, as mobile developers, who dictated the music and API, but this did not always help.
So not always, that recently I made a small collection of traumatic situations that I posted in one of my past articles.
It so happened that we had a pretty strong Java (Spring) developer in the team, and we decided to firmly declare to each new customer: we write the backend ourselves, or look for someone else. At first, they were afraid that such a principled position would frighten off, and we would end up remaining naked on bread and water. But it turned out that if we already liked someone at the negotiation stage and want to work with us, then we can agree on almost everything. Even when the client already has his own people in the team, whom he initially planned to use. Then we learned such a clever word as microservices, and what can be done by a separate server with business logic, performing tasks strictly for a mobile application. I will not argue that such an approach is not appropriate everywhere, but this will not be further discussed.
After several successful Java projects, it was too hard for us. A lot of time was spent on routine to make everything as convenient as possible for the application.
I don’t want to say anything bad about Spring and Java in general, it’s an amazing tool for serious tasks, like a huge bulky Spanish galleon. And we were looking for something more like a lightweight pirate clipper.
We had to quickly implement features, change them easily and not warm our heads in search of the most optimal solution in every situation. You know how it happens when you google for a long time on the subject of a typical solution of your task, so that it is the most suitable, then it turns out that 5 out of 10 of them are already outdated. And then you spend half an hour choosing a name for a variable.
Go has no such problem. From the word at all. Sometimes even through too much: you sit, you look for the perfect solution, and StackOverflow answers you: “Well, yes, just with a for loop, and what did you wait for?”
Over time, you get used to it and you stop to google all sorts of trivia over trifles, and you start to turn on your head and just write code.
To begin with, there is no inheritance. At first, it just took out the brain. You have to break all your ideas about OOP and get used to duck typing . Formulating in simple terms: if it looks like a duck, swims like a duck and quacks like a duck, then this may be the duck.
In essence, there is only interface inheritance.
And secondly, from essential minuses, - a small amount of ready tools, but there are a lot of bugs. Many things can not be done in the usual way, but something is missing as a class. For example, there is no normal framework for IoC ( dependency inversion ). Experienced gopher will say that there is either from Facebook. But maybe I just do not know how to cook it, or after all its convenience actually leaves much to be desired. It simply cannot compare with Spring and therefore has to work a lot with its hands.
Another of the small limitations of Go in general, for example, cannot make an API of the form:
/cards/:id /cards/something
As for the existing http router, these are mutually exclusive requests. It is confused between the wildcard variable and the specific address something. Stupid limitation, but you have to live with it. If someone knows the solution, I will be glad to hear.
There is also no hibernate or less adequate counterparts. Yes, there are many ORM, but all of them are still quite weak. The best I've met during development on Go is gorm . Its main advantage is the most convenient mapping of the response from the base into the structure. And requests will have to be written on bare sql if you don’t want to spend long hours debugging dubious behavior.
Ps . I want to separately share the workaround that arose in the process of working with this liby. If you need to write the id after insert into some variable using gorm, and not into the structure as usual, the following crutch will help. At the request level, rename the returning result to any other than id:
... returning id as value
With a subsequent scan to variable:
... Row().Scan(&variable)
It turns out that the 'id' field is perceived by the gorm as a specific field of the object. And to be unleashed, you need to rename it to something else at the request level.
I want to start at the threshold of entry: it is minimal. Remembering what rattle caused in the development of the same Spring, Go, compared with him, can be taught in elementary grades, it is so simple.
And this simplicity lies not only in the language, but also in the environment that it carries. You do not need to read long mana by gradle and maven, you do not need to write long configs so that at least everything will start at once. Here, everything costs a couple of teams, and a decent builder and profiler is already part of the language and does not require in-depth research to start.
As they say: easy to learn, hard to master. This is something that I have always personally lacked in modern technologies: they seem to be made not for people.
It also follows the speed of development. The language was made for one purpose:
In fact, it is a backend language for business. He is fast, he is simple and allows you to solve complex problems in understandable ways. As for complex tasks and clarity, this is a separate topic for conversation, because Go has such a cool thing as gorutinki and channels. This is the most convenient multithreading with minimal opportunity to shoot yourself in the foot.
As a web framework, Gin was chosen. There is also Revel , but it seemed to us too narrow and unshakably dictating its paradigm. We prefer a little more untied hands so that you can be flexible.
Gin seduced by a convenient API and the absence of unnecessary difficulties. The threshold of entry for him is just low. So much so that the interns understood it literally in a day. There is simply nowhere to get confused. All the necessary functionality in full view.
Of course, he is not without problems. Some decisions, such as cache, are made by a third party. And there is a conflict of imports, if you are accustomed to using import through github, and they have done through gopkg and vice versa. As a result, the two plug-ins can simply be mutually exclusive.
If someone knows a solution to this problem, please write in the comments.
I will not write for a long time, but I will immediately say that this is without a doubt Glide . If you have worked with gradle or maven, then you probably know the paradigm of dependency declarations in a certain file and their subsequent use as necessary. So Glide is a hamster gradle, with conflict resolution and other goodies.
By the way, if you have any problems with testing, when the go test climbs into the vendor folder, eagerly testing each lib, then the problem is solved simply:
go test $(glide novendor)
This option excludes the vendor folder from testing. In the repository itself is enough to put glide.yaml and glide.lock files.
Mobile development is not going to help, but just so you know)
This will be a voluminous section on transferring and storing data from the backend to the client. Let's start with Go, smoothly moving to the mobile platform.
If you have never encountered Realm and do not understand what it is about, then the spoiler was correctly opened.
Realm is a mobile database that makes it easier to work with data synchronization throughout the entire application. It does not have such problems as in CoreData, where you constantly have to work in contexts, even when the object has not been saved yet. Easier to maintain consistency.
It is enough just to create an entity and work with it as with an ordinary object, transferring it between threads and juggling it as you please.
It does a lot of operations for you, but, of course, it also has jambs: there is no case-insensitive search, in general, the search is not completed normally, it consumes memory as not in itself (especially on android), there is no grouping like in the FRC, and etc.
We felt that it was worthwhile to put up with these problems and it is worth it.
In order not to repeat, let me briefly say that we use Gorm as an orm and will give a couple of recommendations:
Probably, this is all applicable to any technology, here I’m a little skapitanil, but still. Remind once again does not hurt, this is important.
Now, as for the mobile application. Your main task is to make the fields returned in the queries have the same name with the corresponding names on the client. This can be easily achieved using so-called tags:
Make sure the json tag has the correct name. And it is desirable that he had the omitempty flag set, as in the example. This avoids cluttering up the response with empty fields.
You may well have a question: what’s the point of Go in general, if the same names can be made in any language? And you will be right, but one of the advantages of Go is the easiest formatting of anything through reflection and structure. Let reflection be in many languages, but Go is the easiest to work with.
By the way, if you need to hide an empty structure from the response, then the best way is to overload the MarshalJSON method on the structure:
// , Pharmacy Object func (r Object) MarshalJSON() ([]byte, error) { type Alias Object var pharmacy *Pharmacy = nil // id != 0, . - nil if r.Pharmacy.ID != 0 { pharmacy = &r.Pharmacy } return json.Marshal(&struct { Pharmacy *Pharmacy `json:"pharmacy,omitempty"` Alias }{ Pharmacy: pharmacy, Alias: (Alias)(r), }) }
Many do not bother and immediately write a pointer instead of a value in the structures, but this is not the Go way. Go doesn't like pointers where they are not needed. This makes it impossible to optimize your code and use its full potential.
In addition to the names of the fields still pay attention to their types. Numbers should be numbers, and strings should be strings (thanks, cap). In terms of dates, RFC3339 is best used. On the server, the date can also be formatted via overload:
func (c *Comment) MarshalJSON() ([]byte, error) { type Alias Comment return json.Marshal(&struct { CreatedAt string `json:"createdAt"` *Alias }{ CreatedAt: c.CreatedAt.Format(time.RFC3339), Alias: (*Alias)(c), }) }
And on the client, this is done through date formatting using the following template:
"yyyy-MM-dd'T'HH:mm:ssZ"
Another advantage of RFC3339 is that it acts as the default date format for Swagger. And the date itself formatted in this way is quite readable for a person, especially in comparison with posix time.
On the client (the example is for iOS, but on Android is similar), with the perfect match of the names of all the fields and class relationships, saving can be done by one generic method:
func save(dictionary: [String : AnyObject]) -> Promise<Void>{ return Promise {fulfill, reject in let realm = Realm.instance // , . // . try! realm.write { realm.create(T.self, value: dictionary, update: true) } fulfill() } }
For arrays, the situation is similar, but saving will have to be driven already in the loop. Often, inexperienced developers make a mistake and wrap the entire write block into a loop:
array.forEach { object in try! realm.write { realm.create(T.self, value: object, update: true) } }
Which is just fundamentally wrong, because this is how you open a new transaction for each object, instead of saving everything together. And if you have more notifications for updates, then everything becomes even more 'fun'. It is more correct to do this as follows, bringing the transaction to a higher level:
try! realm.write { array.forEach { object in realm.create(T.self, value: object, update: true) } }
As you can see, the intermediate layer responsible for the mapping has completely fallen off. When the data is sufficiently prepared, they can immediately peel into the database without additional processing. And the better the backend you have, the less this additional processing will be required. Ideally, only the date convert to the object. Everything else must be done in advance.
By the way, moving away from the topic. If you do not need to have a persistent database on the client, then this is not a reason to refuse Realm. It allows you to work with yourself strictly in RAM, dropping its content on demand.
→ Links for iOS and Android .
This approach allows you to use all the advantages of the reactive database and the above mapping.
I also want to add for those who are especially attentive to trifles: there is no assertion that Go is the only right decision and a panacea for mobile development. Everyone can solve this problem in their own way. We chose this path.
Now there will be a lot of code for Go developers. If you are a mobile developer, you can freely browse to the next section.
Now we come to the most interesting, if you are a Go developer. Suppose you are writing a backend for some typical application: you have a layer with the REST API, some business logic, model, database logic, utilities, migration scripts, and a config with resources. You somehow have to tie all this together in your project by classes and daddies, observe the principles of SOLID and, preferably, do not go crazy with it.
For now, we distribute abstractly, without plunging too deeply, but so that the overall structure is clear. If it is interesting, then I will devote to this a full-fledged separate material. Still, now we are talking about a mobile application in conjunction with Go.
At once I will make a reservation that I do not pretend to the dogma of my statements, everyone is free to work in his project as he sees fit.
Let's start with a screenshot of our structure:
(What a cute hamster in Intellij Idea, isn't it? Every time I am moved)
Non-expanded directories contain either Go files or resource files immediately. Simply put, everything is disclosed so as to see the maximum immersion.
In this article I will tell only about what is responsible for the business logic: api, services, work with the database and how it all depends on each other. If you, dear public, show interest in this topic, then I'll sign for the rest, because there is too much information for one article.
So, in order:
Web
Everything that is responsible for processing requests is stored in the web: binders, filters and controllers - and their entire spike occurs in api.go. An example of such bonding:
regions := r.Group("/regions") regions.GET("/list", Cache.Gin, rc.List) regions.GET("/list/active", Cache.Gin, regionController.ListActive) regions.GET("", binders.Coordinates, regionController.RegionByCoord)
In the same place there is an initialization of controllers and an injection of dependences. In fact, the entire api.go file consists of the Run method, where a router is formed and started, and heaps of auxiliary methods for creating controllers with all dependencies and their groups.
Web.Binders
In the binders folder, there are binders that parse the parameters from the requests, convert them to a convenient format and put them into context for further work.
An example of the method from this package. It takes a parameter from the query, converts to bool, and puts it in context:
func OpenNow(c *gin.Context) { openNow, _ := strconv.ParseBool(c.Query(BindingOpenNow)) c.Set(BindingOpenNow, openNow) }
The easiest option without error handling. Just for clarity.
Web.Controllers
Usually at the level of controllers, they make the most mistakes: they cram excess logic, forget about interfaces and isolation, and then generally slide into functional programming. In general, Go controllers suffer from the same disease as in iOS: they are constantly being overloaded. Therefore, we will immediately determine which tasks they should perform:
c.IndentedJSON(http.StatusCreated, gin.H { "identifier": m.ID })
Take an example of a typical controller.
The class, if you omit imports, begins with the controller interface. Yes, yes, we keep the letter 'D' in the word SOLID, even if you always have only one implementation. This makes testing much easier, giving you the opportunity to replace the controller with its mock:
type Order interface { PlaceOrder(c *gin.Context) AroundWithPrices(c *gin.Context) }
Next we have the controller structure itself and its constructor, which takes dependencies, which we will call when creating the controller in api.go :
// , type order struct { service services.Order } func NewOrder(service services.Order) Order { return &order { service: service, } }
And finally, the method that processes the request. Since we have successfully completed the binding layer, we can be sure that we have all the parameters guaranteed and we can get them with the help of MustGet, without fear of panic attacks:
func (o order)PlaceOrder(c *gin.Context) { m := c.MustGet(BindingOrder).(*model.Order) o.service.PlaceOrder(m) c.IndentedJSON(http.StatusCreated, gin.H { "identifier": m.ID, }) }
With the optional parameters, the same story, but only at the Binder level, it is worth laying a certain zero value, which you will check in the controller, substituting the default value in the absence, or simply ignoring it.
Services
The situation with services is largely identical, they also begin with the interface, structure and constructor, followed by a set of methods. I want to focus on one detail - this is the principle of working with the base.
The service designer must accept the set of repositories with which it will work and the transaction factory:
func NewOrder(repo repositories.Order, txFactory TransactionFactory) Order { return &order { repo: repo, txFactory: txFactory } }
A transaction factory is simply a class that generates transactions; nothing complicated here:
type TransactionFactory interface { BeginNewTransaction() Transaction }
type TransactionFactory interface { BeginNewTransaction() Transaction } type transactionFactory struct { db *gorm.DB } func NewTransactionFactory(db *gorm.DB) TransactionFactory { return &transactionFactory{db: db} } func (t transactionFactory)BeginNewTransaction() Transaction { tx := new(transaction) tx.db = t.db tx.Begin() return tx }
But on the transactions themselves stop worth. Let's start with what it is all about. A transaction is the same interface with an implementation that contains methods for starting a transaction, completing, rolling back, and accessing the engine implementation a level below:
type Transaction interface { Begin() Commit() Rollback() DataSource() interface{} }
type Transaction interface { Begin() Commit() Rollback() DataSource() interface{} } type transaction struct { Transaction db *gorm.DB tx *gorm.DB } func (t *transaction)Begin() { t.tx = t.db.Begin() } func (t *transaction)Commit() { t.tx.Commit() } func (t *transaction)Rollback() { t.tx.Rollback() } func (t *transaction)DataSource() interface{} { return t.tx }
If everything should be clear with begin , commit , rollback , then Datasource is just a crutch for accessing a low-level implementation, because working with any database in Go is designed so that the transaction is just a copy of the accessor to the database with its changed settings. We will need it later when working in repositories.
Actually, here is an example of working with transactions in the service method:
func (o order)PlaceOrder(m *model.Order) { tx := o.txFactory.BeginNewTransaction() defer tx.Commit() o.repo.Insert(tx, m) }
Started a transaction, accessed the database, commited, or rolled back as you like.
Of course, the whole advantage of transactions is especially revealed in several operations, but even if you have only one, as in the example, it will not be worse.
I know that there is no control over isolation levels.
If you have found any other shoals - write in the comments.
As an additional advice to juniors, I want to say that the transaction should be open as soon as possible. Try to prepare all the data so that in the period between begin and commit you have the minimum amount of logic and computation.
It happens that the transaction is opened and go to smoke, sending, for example, a request to Google. And then they wonder why it was all stuffed up with the deadlock.
Interesting fact
In many modern databases, deadlock is determined as simply as possible: by timeout. With a heavy load, scanning resources for blocking is expensive. Therefore, the usual timeout is often used instead. For example, in mysql . If you do not know this feature, then you can give yourself the most wonderful hours of fun debugging.
Repositories
The same: interface, structure, constructor, which, as a rule, already without parameters.
Just give an example of the Insert operation that we called in the service code:
func (order)Insert(tx Transaction, m *model.Order) { db := tx.DataSource().(*gorm.DB) query := "insert into orders (shop_id) values (?) returning id" db.Raw(query, m.Shop.ID).Scan(m) }
We received a low-level access modifier from the transaction, made a request, executed it. Is done.
All this should be enough to not ruin the architecture. At least too fast. If you have any questions or objections, then write in the comments, I will be glad to discuss.
Okay, gophers are cute, but now how to work with this on the client?
We start, as with Go, from our stack. In general, we actively use the reagent almost everywhere, but now I’ll tell you about a more benign version of the architecture so as not to injure the psyche so immediately.
Network layer :
Alamofire for Swift projects and AFNetworking for Objective-C.
By the way, did you know that Alamofire is AFNetworking? AF prefix means Alamofire, as can be seen by looking at the AFNetworking license:
Closures :
callback- / . , . , .
. iOS: PromiseKit . — , , , success/failure , always, , / . , .
, — . flow , . , , , :
func details(id: Int) -> Promise<Void> { return getDetails(id) .then(execute: parse) .then(execute: save) }
getDetails, :
func getDetails(id: Int) -> Promise<DataResponse<Any>> { return Promise { fulfill, reject in Alamofire.request(NetworkRouter.drugDetails(id: id)).responseJSON { fulfill($0) } } }
, . , . , . , .
func parseAsDictionary(response: DataResponse<Any>) -> Promise<[String:AnyObject]> { return Promise {fulfill, reject in switch response.result { case .success(let value): let json = value as! [String : AnyObject] guard response.response!.statusCode < 400 else { let error = Error(dictionary: json) reject(error) return } fulfill(json) break case .failure(let nserror): let error = Error(error: nserror as NSError) reject(error) break } } } // , func save(items: [[String : AnyObject]]) -> Promise<Int> { return Promise {fulfill, reject in let realm = Realm.instance try! realm.write { items.forEach { item in // generic realm.create(Item.self, value: item, update: true) } } fulfill(items.count) } }
, MVC, :
_ = service.details().then {[weak self] array -> Void in // Success. Do w/e you like. }
Database
, ORM Go-side, , . - , . , datasource . , .
fine-grained notifications , .
class ViewController: UITableViewController { var notificationToken: NotificationToken? = nil override func viewDidLoad() { super.viewDidLoad() let realm = try! Realm() let results = realm.objects(Person.self).filter("age > 5") // Observe Results Notifications notificationToken = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in guard let tableView = self?.tableView else { return } switch changes { case .initial: // Results are now populated and can be accessed without blocking the UI tableView.reloadData() break case .update(_, let deletions, let insertions, let modifications): // Query results have changed, so apply them to the UITableView tableView.beginUpdates() tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), with: .automatic) tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), with: .automatic) tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), with: .automatic) tableView.endUpdates() break case .error(let error): // An error occurred while opening the Realm file on the background worker thread fatalError("\(error)") break } } } deinit { notificationToken?.stop() } }
, ApiManager.swift . , , — extension ApiManager, .
singleton, . , , .
SOA (service oriented architecture). Rambler , , , .
. — . , . , viewDidLoad. , , . , , , , , .
:
, . , 200-300 . , , .
, : , . , .
. Realm- mobile-side , . , -, . , iOS Android, — !
, . , , - .
. .
, . , , , , , , ? MVP MVVM , .
, , : “, ?” : “, .”
.
, , . , , .
PS . ? , , . , , , , .
Source: https://habr.com/ru/post/331456/
All Articles