How to avoid code repetition when writing API

xuanbao · · 455 次点击    
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
<p>Hello, I am new to Go and I&#39;ve been writing a small API in Go, and so far compared to other language I&#39;ve used it seems I am stuck doing a lot of code repetition to do things like creating and updating resources in the database.</p> <p>For example If I want to create a User I need a few required fields, while to update a User, I need a few of the same fields and some different ones but they are not required. To accomplish this I created 1 struct and 1 validation function for each use case like this (random made up example to keep it short):</p> <pre><code>type ( CreateUser struct { FullName string `db:&#34;full_name&#34; json:&#34;fullName&#34;` Country string `db:&#34;country&#34; json:&#34;country&#34;` Username string `db:&#34;username&#34; json:&#34;username&#34;` } UpdateUser struct { FullName string `db:&#34;full_name,omitempty&#34; json:&#34;fullName&#34;` Country string `db:&#34;country,omitempty&#34; json:&#34;country&#34;` Nickname string `db:&#34;nickname,omitempty&#34; json:&#34;nickname&#34;` } ) func (u CreateUser) Validate() error { return validation.StructRules{}. Add(&#34;FullName&#34;, validation.Required, validation.Length(3, 20)). Add(&#34;Country&#34;, validation.Required, validation.In(countries...)). Add(&#34;Username&#34;, validation.Required, validation.Length(4, 10)). Validate(u) } func (u UpdateUser) Validate() error { return validation.StructRules{}. Add(&#34;FullName&#34;, validation.Length(3, 20)). Add(&#34;Country&#34;, validation.In(countries...)). Add(&#34;Nickname&#34;, validation.Length(2, 10)). Validate(u) } </code></pre> <p>but this means I have to duplicate all the validation logic, the db field name, the json field name for fields that are used in both structs</p> <p>Is there a better way to handle this in Go?</p> <hr/>**评论:**<br/><br/>YoungMacBo: <pre><p>My first intuitions are these: a little duplication beats an unwanted abstraction, and just because you /can/ create an abstraction somewhere doesn&#39;t mean you automatically /should/.</p> <p>Suppose you have 10K lines, you can add an abstraction and reduce that to 8K but now you have additional complexity in your project. You do that a few times and now the next developer has to learn 7 new abstractions before he&#39;s able to work with the code. Compare a big bland piece of work with a highly intricate (shiny!!) one. Now imagine your team has to maintain ten of those.</p> <p>I am /ALWAYS/ /EXTREMELY/ sceptic of /ANY/ additional complexity on top of what your platform (in this case the generous golang stdlib) provides. So from the outset my response would be: yes, just copy and paste the validation code, it&#39;s quite simple, it&#39;s not hard to maintain, it&#39;s fine.</p></pre>condanky: <pre><p>Well in terms of your validation they appear to be two different validations. That being said for your structs you can do something like this</p> <pre><code> type ( User struct { FullName string `db:&#34;full_name&#34; json:&#34;fullName&#34;` Country string `db:&#34;country&#34; json:&#34;country&#34;` Username string `db:&#34;username&#34; json:&#34;username&#34;` } CreateUser struct { User } UpdateUser struct { User } ) </code></pre></pre>exch: <pre><p>Careful with this. The <code>Username</code> field has two different tag definitions in OP&#39;s post. So it should probably be placed in each type separately, instead of being part of the <code>User</code> struct.</p></pre>condanky: <pre><p>ahhh yes, I missed that. You are correct</p></pre>homeless-programmer: <pre><p>Could you apply your additional validation rules based on the presence of an ID field? In your example:</p> <pre><code>type User struct { Id int `db:&#34;id&#34; json:&#34;id&#34;` FullName string `db:&#34;full_name,omitempty&#34; json:&#34;fullName&#34;` Country string `db:&#34;country,omitempty&#34; json:&#34;country&#34;` Nickname string `db:&#34;nickname,omitempty&#34; json:&#34;nickname&#34;` } func (u User) Validate() error { validator := validation.StructRules{}. Add(&#34;FullName&#34;, validation.Length(3, 20)). Add(&#34;Country&#34;, validation.In(countries...)). Add(&#34;Nickname&#34;, validation.Length(2, 10)) if (u.Id == 0) { //We assume that a 0 Id means it&#39;s a new resource. validator.Add(&#34;FullName&#34;, validation.Required). Add(&#34;Country&#34;, validation.Required). Add(&#34;Nickname&#34;, validation.Required) } return validator.Validate(u) } </code></pre> <p>Not tested this - writing on a machine without go installed, but does this help?</p></pre>0xA0BF: <pre><p>Thank you for your suggestions <a href="/u/condanky" rel="nofollow">/u/condanky</a> and <a href="/u/homeless-programmer" rel="nofollow">/u/homeless-programmer</a>, I didn&#39;t know you could extend structs like that. I&#39;ve realized I can probably have only 1 struct with all the db tag having omitempty but the problem is with the json, I sometimes need to be able to set a field and sometime not, is there a way to whitelist which fields will be populated? I&#39;d like to avoid having 2 version of the same field one with json:&#34;-&#34; and one with json:&#34;name&#34; but it might be the only way</p></pre>goomba_gibbon: <pre><p>It took me a while to understand this particular part of go. The example shows a concept called embedded types. It&#39;s also often referred to as composition. CreateUser doesn&#39;t inherit from User, it&#39;s composed by it.</p> <p>As you&#39;ve seen, the UpdateUser and CreateUser structs have access to all the fields of User.</p> <p>If User has any methods, you can call them directly on a struct of the parent types.</p> <p>Finally, because we have any methods from the embedded type, we also implement the same interfaces.</p></pre>condanky: <pre><p>why don&#39;t you just combine the DB fields and specify the json type like you have. Not sure I understand you having 2 versions of the same field.</p></pre>0xA0BF: <pre><p>Here is something similar to what I have now:</p> <pre><code>type ( User struct { FullName string `db:&#34;full_name,omitempty&#34; json:&#34;fullName&#34;` Country string `db:&#34;country,omitempty&#34; json:&#34;country&#34;` Username string `db:&#34;username,omitempty&#34; json:&#34;username&#34;` Nickname string `db:&#34;nickname,omitempty&#34; json:&#34;nickname&#34;` } CreateUser struct { User } UpdateUser struct { User } ) func validRules() validation.StructRules { return validation.StructRules{}. Add(&#34;FullName&#34;, validation.Length(3, 20)). Add(&#34;Country&#34;, validation.In(countries...)). Add(&#34;Username&#34;, validation.Length(4, 10)). Add(&#34;Nickname&#34;, validation.Length(2, 10)). } func (u CreateUser) Validate() error { return validRules(). Add(&#34;FullName&#34;, validation.Required). Add(&#34;Country&#34;, validation.Required). Add(&#34;Username&#34;, validation.Required). Validate(u) } func (u UpdateUser) Validate() error { return validRules().Validate(u) } </code></pre> <p>I want to whitelist which fields will be set into the struct from the JSON body of the request, for UpdateUser it should not be possible to change the username, in this example I could simply do something like user.username = &#34;&#34; after loading the data into the struct but my actual use case has much more fields and is more complicated so thats not an option. </p></pre>condanky: <pre><p>ahh i see, you should pull out the Username field into the CreateUser struct. Basically only have shared fields in the user struct which maybe you should change to &#39;Shared(something)&#39; or whatever you want. Then for specific values you should include them in their respective structs. Does that make sense?</p> <pre><code>User struct { FullName string `db:&#34;full_name,omitempty&#34; json:&#34;fullName&#34;` Country string `db:&#34;country,omitempty&#34; json:&#34;country&#34;` } type CreateUser struct { User Username string `db:&#34;username,omitempty&#34; json:&#34;username&#34;` } UpdateUser struct { User Nickname string `db:&#34;nickname,omitempty&#34; json:&#34;nickname&#34;` } </code></pre></pre>mistretzu: <pre><p>DRY doesn&#39;t necessarily mean you should try to merge (at an early stage) any entities that have some properties in common. They might differ/diverge in usage/role later and you will end up with a very confusing code. Also creation validation might differ from the update validation even if they might have common functionality (which should be reused).</p></pre>Asdayasman: <pre><p>What are these backtick strings in the struct? Not seen those yet.</p> <p>(I&#39;m new).</p></pre>Uncaffeinated: <pre><p>They are field tags. A relatively obscure feature that&#39;s mainly useful for reflection.</p> <blockquote> <p>A field declaration may be followed by an optional string literal tag, which becomes an attribute for all the fields in the corresponding field declaration. An empty tag string is equivalent to an absent tag. The tags are made visible through a reflection interface and take part in type identity for structs but are otherwise ignored.</p> </blockquote></pre>goomba_gibbon: <pre><p>In general they are for string literals, they aren&#39;t restricted to struct field tags.</p></pre>goomba_gibbon: <pre><p>I haven&#39;t tried it but I&#39;m fairly sure you can define custom struct field tags. What about using those to create update_required and create_required tags? It might reduce repetition at the cost of boilerplate.</p></pre>joncalhoun: <pre><p>Untested, but this should give you an idea of another approach that uses maps and first class functions to do this: <a href="https://gist.github.com/joncalhoun/9e51a380b62dc24522154fdd92d9a49d" rel="nofollow">https://gist.github.com/joncalhoun/9e51a380b62dc24522154fdd92d9a49d</a></p> <p>To use it you could do things like <code>err := validate(user, onCreate())</code> and <code>err := validate(user, onUpdate())</code> and any other context you want. I also prefer to have a single struct mapped to my DB data, and then I use things like <code>omitempty</code> to ignore them when they aren&#39;t set. </p></pre>

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

455 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传