Dependency Injection by overriding interfaces

agolangf · · 626 次点击    
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
<p>Hey <a href="/r/golang">/r/golang</a>,</p> <p>I&#39;ve been racking my head on the best way to seed my DB, Cache, and other third party services throughout my application.</p> <p>For sake of speed, I initially just used globally accessable objects within packages dedicated to those issues, but I&#39;m now at a point where we can refactor a better (and more testable) solution into the code.</p> <p>I&#39;ve been researching quite a bit on the best way to do this though, and seen a lot of non-consensus on the topic. The most popular answer seems to be:</p> <blockquote> <p>Define an interface for the dependency and pass it as an argument into each function or method that requires it.</p> </blockquote> <p>This works great in the small examples posted on SO and elsewhere, but I feel like littering my code with a ton of arguments goes against the reasons for DRY - let&#39;s say I decide to start caching my DB reads - now I need to go back through everything that accepts a DB dependency, and add in the dependency for the cache that&#39;s going to be used. It makes things very clear, but it&#39;s not very encapsulated, and given the proclivity of dependencies to change, I feel like that might be purity at the cost of adaptability.</p> <p>I came up with the following solution for dealing with this problem, and want to see if anyone else has thoughts on why this would be a huge mistake, or ways the approach could be improved:</p> <p><a href="http://play.golang.org/p/NOVKZCxpca">Play Example</a></p> <p>I feel like this is a fairly flexible solution. In my case, since all the services I need currently operate like singletons (I don&#39;t need multiple DB backends, caching backends, etc), I can continue using global instances of these services across my code, but it becomes trivial to test my own implimentations by overriding the interfaces. If I ever needed to have multiple instances of a dependency in the future, I could always adapt the code at a later point of time with a service factory and pass it as an argument localized to only the area where it differs from the global dependency.</p> <p>The other big area of concern would be race conditions, but as I don&#39;t plan on changing the implimentations during runtime often (only within tests), I think a mutex would solve that issue.</p> <p>Has anyone else found a more elegant way of peppering multiple dependencies into their code across controllers/models/middlewares/etc? Or see ways I can make this better/other concerns I haven&#39;t anticipated?</p> <p>TL;DR: To solve the issue of dependency injection for third party services and to make my life easier during testing without adding multiple arguments everywhere, I devised an approach of composing a container object with the dependency as an interface and a setter method to overwrite that interface, optionally fulfilling the interface itself, and using that object in my codebase instead. I&#39;m looking for thoughts and/or criticisms from the community.</p> <hr/>**评论:**<br/><br/>printf_hello_world: <pre><p>Absolutely love this pattern. Check <a href="http://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/">this</a> out; I think you&#39;ll find it relevant.</p></pre>kromem: <pre><p>Ohh... that&#39;s neat! Thank you for sharing.</p></pre>breerly: <pre><p>Just use plain-ol DI:</p> <ol> <li>Inject dependencies onto structs where the funcs that need access to the deps are methods on the struct.</li> <li>Wire up the dependency graph by hand in the main function.</li> <li>Eventually, where needed, create factories.</li> <li>Once there is significant wiring and factory logic, consider a DI framework to centralize and streamline initialization logic.</li> </ol></pre>kromem: <pre><p>I think you&#39;re probably right. </p> <p>I also think an advantage of the basic approach would be discouraging the direct access of dependencies beyond the bare minimum, which would be evident in step #2 becoming a nightmare.</p></pre>breerly: <pre><p>Yup. </p> <p>As much as possible you want to avoid a global dependency registry or service locator approach, which is what the example you posted appears to be. </p> <p>Once #2 gets big/complex enough, you&#39;ll want to use a service container (#4).</p></pre>mnsota: <pre><p>This is a slightly cleaner approach and the type of your global will be Commenter rather than Container. <a href="http://play.golang.org/p/-aE6FUmrGU" rel="nofollow">http://play.golang.org/p/-aE6FUmrGU</a></p> <p>A more elegant approach would be to use the factory and driver patterns. That way you can change the type of Commenter that is constructed based on a build tag, a test file, or even a string.</p></pre>jerf: <pre><p>I wrote up something I call the <a href="http://www.jerf.org/iri/post/2929" rel="nofollow">environment object pattern</a>, which I still prefer to anything I see linked in this discussion as of this writing.</p> <p>Also, note that if all your setter does is set something, you might as well just have a public member. You&#39;re doing more paperwork than you need to. (Heck, all these other suggestions are doing more paperwork than they need to.)</p></pre>R2A2: <pre><p>have a look at facebook&#39;s <a href="https://godoc.org/github.com/facebookgo/inject" rel="nofollow">inject</a>. I&#39;m planning to use it to replace a big ugly chunk of wiring code (about 50 of these objects, all wired together in a repetitive way)</p></pre>egonelbre: <pre><p>Could you say/show what the actual example is. It&#39;s very hard to see what the best solution would be from a trivialized example.</p> <p>Regarding your proposed solution - you can just use a global variable. There&#39;s no point in putting a container around it. e.g. <a href="http://play.golang.org/p/RgWeJY7GaN" rel="nofollow">http://play.golang.org/p/RgWeJY7GaN</a>.</p> <p>After a lot of experimenting, using interface(s) as a separation layer seems to work the best. e.g. <a href="https://github.com/raintreeinc/knowledgebase/blob/master/kb/database.go#L22" rel="nofollow">https://github.com/raintreeinc/knowledgebase/blob/master/kb/database.go#L22</a>. <em>Note: WIP, not ready for general consumption.</em> That completely defines my database dependency,although it&#39;s not &#34;database&#34; probably &#34;Services&#34; would be more precise. But it defines most dependencies that the Server has. And it becomes a dependecy to the server <a href="https://github.com/raintreeinc/knowledgebase/blob/master/kb/server.go#L64" rel="nofollow">https://github.com/raintreeinc/knowledgebase/blob/master/kb/server.go#L64</a>.</p> <p>If I need caching I could do it two ways - either put it inside the database and make it transparent or create a separate interface for cache and use that inside. One is better because it&#39;s transparent to the rest of code, of course the separate cache interface might help with integrating with other things.</p> <p>Of course how you design your interfaces is not that clear, but mostly can be designed via constantly pushing code down. I.e. asking the question &#34;Does the current package need to care how exactly X is done? No --&gt; move it to an interface.&#34;</p></pre>

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

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