Method / Function Chaining makes debugging slower.

agolangf · · 470 次点击    
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
<p>I am starting to have a change of heart about chaining functions together. </p> <p>My argument is, if you encounter an unexpected panic or runtime error, your stack trace will point to the line and it will be immediately obvious what function called the error, whereas when you chain, it is harder to unwrap.</p> <p>What are your thoughts?</p> <hr/>**评论:**<br/><br/>natefinch: <pre><p>Yes, 100x yes.</p> <p>This is often called method chaining, where a method returns the original object just so that you can call another method from the return value of the first. e.g.</p> <pre><code>a.Foo().Bar().Baz() </code></pre> <p>However, this is really bad practice in Go (and honestly, in other languages, but that&#39;s another story).</p> <p>In go, this means that your methods can only return a single value - the receiver. Not only does this prevent you from returning other useful values (like errors), it also means that your methods now &#34;lie&#34; about their return values.</p> <pre><code>func (f *Foo) Bar() *Foo </code></pre> <p>What does this method do? I don&#39;t know, but it looks like it might transform f and return a different value. That&#39;s usually the only reason you&#39;d return a pointer. But if this is just for method chaining, then it doesn&#39;t return a different value, it returns the same value. This is making the signature of the method misleading. </p> <p>Often times, these methods will be able to fail (most interesting things you can do can fail) which means that then people do heinous things to get around the fact that they don&#39;t have multiple return values anymore... like stashing an error value away in the receiver, so then you have to do something like</p> <pre><code>a.Foo().Bar().Baz() if a.Err != nil { return a.Err } </code></pre> <p>This is hugely non-idiomatic, and just a bad idea. What part failed? Foo, Bar, or Baz? You don&#39;t know. It also means that now there&#39;s internal state that needs to be tracked around that error.... it&#39;s just a disaster.</p> <p>All this just to save a few line returns. even assuming these things can&#39;t error, just make them boring old functions:</p> <pre><code>a.Foo() a.Bar() a.Baz() </code></pre> <p><sup>^</sup> this is not the end of the world.</p></pre>jerf: <pre><blockquote> <p>However, this is really bad practice in Go</p> </blockquote> <p>Yes, it &#34;works&#34; much better in languages where all functions implicitly have two return values, the thing they return and the exception they may throw.</p> <p>If you do want to do something similar to this, I&#39;d suggest that <a href="https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis" rel="nofollow">functional arguments</a> is a much better way to go. I don&#39;t find it useful very often because A: having something that takes dozens of parameters is already a bad sign and B: functional options come into their own when you need validation checking on the arguments as well, which makes it even more unusual (if you don&#39;t need that, just use a struct and call it a day). There&#39;s a certain amount of controversy around functional parameters, so note I&#39;m only claiming <em>they&#39;re better than method chaining</em>, not that they are necessarily &#34;good&#34;. Still, I&#39;ve got a couple of these in my code bases.</p> <p>The other thing I&#39;ve come to hate about this pattern, even in other languages, is that something about it makes people&#39;s brains check out and they think somehow they&#39;re working in a different language all of a sudden. They stop considering the chains themselves as abstraction targets, they stop realizing it&#39;s perfectly valid to:</p> <pre><code>val := X.Chain1().Chain2() if cond { val.Chain3() } commonChains(val) val.DoTheThing() func commonChains(val *val) { // &#34;Consider this as a single thing for my purposes&#34; val.Chain4().Chain5().Chain6() } </code></pre> <p>Alternatively, you get chain-based APIs where the API authors themselves forgot those are all syntactically valid and things blow up if you take copies of a half-instantiated chain or something.</p> <p>I find method chaining distasteful for reasons I&#39;m still not sure I can 100% articulate, but I <em>loathe</em> what it does to programmers. I can&#39;t explain why it does that to programmers; perhaps I just encountered my first chaining API far too late in my development (I think I was ~7-8 years in before I first saw one) so they were never the limits of my abstraction capability because I could always &#34;see through&#34; them to the implementation underneath. It&#39;s hard for me to explain why something happens when it never happened to me.</p></pre>very-little-gravitas: <pre><p>There are some places I think this pattern is useful. For example in a query builder, where you&#39;re just setting state in a memo, and don&#39;t need to return errors. </p> <p>It&#39;s nicer to do something like this:</p> <pre><code>query := users.Order(&#34;status desc, name asc&#34;).Where(&#34;status is not null&#34;) if q { query.Where(&#34;name=?&#34;,q) } if tag { query.Join(&#34;tags&#34;).Where(&#34;tag_id=?&#34;,tag) } </code></pre> <p>than to build the sql by hand IMO (remembering to put order last etc), and there&#39;s no reason for a struct like that to return errors, it just stores state in a convenient way. </p> <p>Another example might be to filter collections - sometimes it&#39;s nice to be able to define some filters on a collection which can be chained so that you can combine different filters succinctly in order to filter a collection down to the elements you want (for example a dom filtering language). </p> <p>That said there are many cases where it is not a useful pattern and gets in the way, and it&#39;s not something I&#39;d use often. It shouldn&#39;t be used with code that might cause errors or panic. </p></pre>anaerobic_lifeform: <pre><blockquote> <p>That&#39;s usually the only reason you&#39;d return a pointer. </p> </blockquote> <p>Not necessarily, you are assuming too much based on the signature, method chaining is also a possible use cases (which predates Go, btw).</p> <blockquote> <p>This is making the signature of the method misleading.</p> </blockquote> <p>Only if you assume that No True Method will ever do method chaining.</p> <blockquote> <p>Often times, these methods will be able to fail [...] which means that then people do heinous things [...] like stashing an error value away in the receiver [...] This is hugely non-idiomatic, and just a bad idea. What part failed? Foo, Bar, or Baz? You don&#39;t know. It also means that now there&#39;s internal state that needs to be tracked around that error.... it&#39;s just a disaster.</p> </blockquote> <p>Isn&#39;t that the whole point of <a href="https://blog.golang.org/errors-are-values" rel="nofollow">Errors are values</a>? Find a way to manage errors like you do with other values, using state variables if necessary? Maybe the receiver is able to keep track of which method failed.</p> <blockquote> <p>All this just to save a few line returns. even assuming these things can&#39;t error, just make them boring old functions:</p> </blockquote> <p>Agree, the fluent interface is useless when used only for style, but there could be good use cases for it, including handling errors.</p></pre>natefinch: <pre><p>So, you have a good point about it not being misleading if you expect method chaining. </p> <p>I do think that usually when people try to do fancy things with caching errors, what happens is that you get a weird non-idiomatic interface that confuses people, and thus gets used incorrectly (totally forgetting to check error returns, or checking them at the wrong time, etc).</p></pre>anaerobic_lifeform: <pre><p>Every other week, there is a library or proposal to &#34;solve&#34; error handling in Go. At least method chaining does not require language extensions, it already works within the language. It <em>is</em> unfamiliar, but if you cannot assume that people at least read the documentation, you can&#39;t write anything.</p> <p>Could it help if the object (a) is of a type which implements the error interface?</p></pre>natefinch: <pre><p>Most things trying to solve error handling are just trying to avoid writing if statements, which to me seems like a silly thing to worry about.</p> <p>Method chaining seems to exist to avoid writing line returns... which is possibly even more silly to worry about.</p> <p>We have idioms for a reason, it means that you can make assumptions about how code will work and generally be right. When you stray outside those bounds, you introduce opportunity for misunderstanding, even if there is sufficient documentation. Docs don&#39;t help when you&#39;re looking at the code in a diff or a code review. </p> <p>Would it help if a implements the error interface? Yeesh, no, please don&#39;t do that.</p></pre>daydreamdrunk: <pre><p>Not a fan of the style in general, though it is sometimes useful.</p> <p>Regardless, this is legal (and how gofmt fmts it)</p> <pre><code>a.Foo(). Bar(). Baz() </code></pre></pre>tcrypt: <pre><p>It doesn&#39;t matter much for method calls anyways because the stack trace will show the line in the method that panic&#39;d: <a href="https://play.golang.org/p/PDTgvn4mjh" rel="nofollow">https://play.golang.org/p/PDTgvn4mjh</a></p></pre>Redundancy_: <pre><p>Could you provide an example of each? It would make discussing it a little easier without worrying about the definition that people are using.</p></pre>cameronjerrellnewton: <pre><p>Just any place you would use chaining, but I guess a concrete example would be with database/sql, say you had a map of prepared statements and you choose one to call <code>QueryRow()</code> and you chain <code>Scan()</code></p> <p>so,</p> <p><code>line 15: err := statementMap[&#34;somePreparedQuery&#34;].QueryRow().Scan(&amp;someDest)</code></p> <p>Then you run this and get a panic - invalid memory or pointer access.</p> <p>Is it<br/> a: you have a typo in your map accessor?<br/> b: your destination variable is not defined or nil/something else? </p> <p>I am afraid by giving a specific example the conversation will drift towards the one specific instance. There are some other places I have encountered this. I will say however the panic does typically end up being memory access related</p></pre>

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

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