作者zanyking (最後的六年级生)
看板Soft_Job
标题Re: [请益] Spring boot的依赖注入降低耦合的例子
时间Fri Apr 15 09:46:31 2022
※ 引述《ntpuisbest (阿龙)》之铭言:
: 推文有个连结有解答我的疑惑
: 感谢bron大
: 文章有点长
: 先说说我对依赖注入的理解
: Spring boot
: 依赖注入大致有三种方式
: 透过建构子的 透过setter的 或是 field
: 这三种都可以透过@Autowired注解来达到依赖注入的效果
在这个时代依赖注入最重要的用途,特别是在後端开发是让Application 在多个不同的
环境下(Development, Production, local, etc)
能够根据profile 组出能正确执行的Application
多型在这里当然有他的地位,但是一般来说,大部分不接触system boundary的service
objects 是不太需要多型的,如果是java,那种一个interface 只有一个implementation
class 的情况其实就代表着那个interface 没有用
倒是如何搭配Annotation 与aspect programming在後端开发是更重要的
: 我自己想到的建构子的举例是
: 假设有两个类 Address 和 Employee好了
: 1.
: public class Address {
: String Country;
: String City;
: String Street;
: public Address(String country, String city, String street) {
: Country = country;
: City = city;
: Street = street;
: }
: }
: 2.
: public class Employee {
: String sex;
: String name;
: Address address;
: // 没有依赖注入的方式
: public Employee(String Country,String City,String Street,String
: sex, String name ) {
: this.sex=sex;
: this.address = new Address( Country, City,Street );
: this.name=name;
: }
: // 有依赖注入的方式
: public Employee(String sex, String name, Address address) {
: this.sex = sex;
: this.name = name;
: this.address = address;
: }
: }
: 在上面的例子当中可以发现,如果哪一天
: Address这个类新增了一个属性叫 phoneNumber好了
: 没有依赖注入的方式,必须要更改 Employee 的
: this.address =new Address(Country,City,Street,phoneNumber)
: 而有依赖注入的方式确实降低了耦合
: 因为他不用更改Employee的建构方式
: 所以我理解依赖注入可以降低耦合
这是资料物件,资料物件顶多实现封装而已,这也不是什麽依赖注入,这只是单纯
的物件组装
当你说降低『耦合』的时候,你觉得是什麽东西的『耦合』需要降低?
: 但我的问题是Spring boot 的 autowird annotation 有帮助我们降低耦合吗
: 在常见的开发中 我们经常都会有 Dao 以及 Service
: 假设我有两个 Dao 好了 分别是 Dao1 和 Dao2
: 以及一个Service
: Dao1
: public class Dao {
: public void sayhi() {
: System.out.println("hello");
: }
: }
: Dao2
: public class Dao2 {
: public void saygoodbye() {
: System.out.println("say goodbye");
: }
: }
Data Access Object 是要去存取Data source的,你取这个名字,就表示後面存在
某种DB、file 等总之是需要透过IO去存取获得资料的东西
如果你的service 就是需要存取特定的Data source 才能工作,那你当然没办法
让这两个Dao 消失啊,你顶多再插入一层service ,把他们封装进其他的service object
里
: 如果我不在service上面使用autowired
: 我的service会是
: public class Service {
: Dao1 dao=new Dao1();
: Dao2 dao2=new Dao2();
: public void sayhi() {
: dao.sayhi();
: }
: public void saygoodbye() {
: dao2.saygoodbye();
: }
: }
: 如果我使用了@Autowired注解
: 那我只是将
: Dao1 dao=new Dao1();
: Dao2 dao2=new Dao2();
: 替换成
: @Autowired
: Dao1 dao
: @Autowired
: Dao2 dao2
: 我想请问所以我使用了Autowired注解
: 我知道我可以不需要使用new 来建构实体
: 但 Spring 真的有帮我降低耦合吗
: 即使我换成 setter 配合 autowired的方式好了
: 那个 setter也是要我自己去撰写
: Spring 帮我降低了耦合甚麽?
Spring 在这里帮你:
1. 把物件组装起来
2. DAO 是要存取Data source 的,所以会开Connection,有Connection 就有
lifecycle 要管,而Spring 可以帮你把lifecycle 管好
3. 某些Datasource 的互动是需要transaction 来保证资料一致性的,Spring
也帮你做了
4. localhost、Dev、Production 要连不同的datasource,要开大小不同的connection
pool 这Spring 帮你做了
5. 哪里连不上出差错了,Spring 报给你知
他帮你做了这麽多、唯一没办法帮你做的就是你期待的降低耦合(事实上你期待的是
零耦合),但因为你的service 业务逻辑实作就是需要跟这两个Dao 互动啊,那是要
怎麽降?
问题不是出在Spring,而是出在你对Framework 有错误的认识与期待
更深入探讨,如果你不用Spring 这类的DI 容器,那你肯定就要自己Construct 这两个
DAO,如果你直接在你的Service constructor 创建这两个Dao,那我列的那五件事,
就得完全归service 自己管了,在采用Spring 以至於你不用担心dao 到底该怎麽baby
sitting就可以像自来水电一样转开就用的情况下,你的service 就已经在一个够低的
耦合水平下与这两个dao 互动了
吃米要知道米价啊
: 我的问题简单来说就是
: 我知道依赖注入可以降低耦合
: 但Spring boot透过 @Autowired注解帮我降低耦合在哪
: 谢谢
: p.s 因为面试的时候常常被面试官问说懂不懂甚麽是
: 控制反转还有DI,我基本上举例都举 Address还有 Employee的例子
你举这个例子,我是面试官大概就会直接当作你不懂
因为Address 还有Employee 都是资料物件,而控制反转与DI要解决的问题是:
『如何组织程序?如何把系统中许许多多需要执行的procedure适当的切分,然後
在适当的地方、时机,可以有适当的Context去执行?』
这与资料物件如何透过实现多型而可以任意组装没有关系
我们要注入给Framework或是Container 的不是资料物件,而是行为物件,是那种suffix
会叫做:Filter、Listener、Consumer、Interceptor 的『行为』物件
你要我举例,我大概会用HttpClient 当例子
当我的系统中到处都用得上httpClient,而且他需要可以Configure Circuit Breaker
、有Rate Limiter、遇到response error 知道要retry、而且有预设的Http status
handling 但仍旧可以客制化、response deserialization 要可以从json 与form间
自由切换,然後系统中每个地方用HttpClient 的情境都不太一样,请问要怎麽设计这
个HttpClient?
: 但当我反问下面例子的时候,他们好像也说要再回去想一下...
: 只有其中一个就说更复杂的例子会用到,但也没说甚麽是更复杂的例子QQ
控制反转在实作上,最直白的做法,就是业务逻辑不是由programmer 直接撰写
imperative procedure 执行,而是把这些业务逻辑(一般实现成各种services)包裹
成各式各样代表行为的物件,比如说:Listener、Consumer、Filter 、Interceptor、
visitor 等等,然後实际在什麽时间点、在什麽地方这些物件会被呼叫是由Application
内某种processor、Container 根据当初的Configuration来决定
这些东西应该书上都有写的,我建议你还是要找本书好好的看过一遍才比较会有全面
的认识
--
『你知道人有脑子,所以不要只是单纯的满足它,偶尔也要使用它啊。』
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 67.188.38.188 (美国)
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/Soft_Job/M.1649987202.A.87B.html
※ 编辑: zanyking (67.188.38.188 美国), 04/15/2022 09:47:47
1F:推 ntpuisbest: 推喔我会举那个例子是因为蛮多部落格或是stackoverflo 04/15 10:36
2F:→ ntpuisbest: w都是说di 04/15 10:36
3F:→ ntpuisbest: 分成三种注入,field setter还有constructer 04/15 10:36
6F:→ ntpuisbest: s-dependency-injection-and-inversion-of-control-in 04/15 10:38
7F:→ ntpuisbest: -spring-framework 04/15 10:38
8F:→ ntpuisbest: 看起来外国人说的好像也不见得对 04/15 10:39
Blog文章举例举不好,但他们有很多的篇幅去讲他们真正想讲的东西
你去面试,面试官是听你说而不是看你写的文章,他没有办法来回重复听你讲的东西
所以用对例子在你的情境就比写文章的重要性要大得太多了
9F:推 vi000246: 钓出大神 04/15 11:15
10F:推 CRPKT: 可以从各模组内含的知识与责任来看这件事 04/15 11:36
11F:→ CRPKT: 即使只有一个实作,interface 抽象化在知识面上还是有用 04/15 11:36
12F:→ CRPKT: DI 主要价值在於 dependency 下游不用认识上游实作 04/15 11:37
13F:→ CRPKT: 所以认真讲有帮到解耦也不能说它错,只是不用太纠结这点 XD 04/15 11:40
14F:推 acgotaku: DI核心还是在於不用顾虑class内的其他实例只需专注介面 04/15 12:25
你说的没有错,问题在於如果跟不懂的人用这个说法,他也还是不会懂
所以我才花了大篇幅用他的Dao 去说明他的service 哪边已经透过Spring 获得好处了
15F:推 wulouise: 通常只有一个业务实作的情况,另一个实作是MOCK 04/15 12:38
16F:推 srwhite: 推这篇感觉比较有打到我的点 04/15 19:57
17F:推 baobomb: 非常同意第二段 太多工程师一天到晚要抽象 要interface 04/15 21:35
18F:→ baobomb: 但根本不知道介面的真正用途为何 有时候看到一个module 04/15 21:35
19F:→ baobomb: 里 明明很多元件都没有expose给其他module使用 也被inter 04/15 21:35
20F:→ baobomb: face的到处都是 然後问那些工程师为什麽这边要介面 每个 04/15 21:36
21F:→ baobomb: 都说clean code 但问他到底哪里clean了 就支支吾吾了... 04/15 21:36
22F:→ moom50302: 同意楼上,一个介面一个实体这种设计到底有什麽必要… 04/15 21:43
23F:推 wulouise: 等等...你们的franework mock都不用interface? 04/15 23:11
24F:推 baobomb: 如果用interface的目的是单纯为了mock... 这我不太认同 04/15 23:30
25F:→ baobomb: 啦.. 如果composition做得好 DI graph漂亮 你不需要inter 04/15 23:30
26F:→ baobomb: face也能测试 我以前也有一样的想法 有interface mock起 04/15 23:30
27F:→ baobomb: 来很爽 後来还因为这原因跟另一位资深的同事争执了一阵子 04/15 23:30
28F:→ baobomb: 後来才发现我是错的.. 04/15 23:30
29F:推 bronx0807: 不用interface就用cglib动态代理生成mock物件 04/15 23:32
30F:推 baobomb: 如果让程式去迎合测试 本身很容易进入误区 而是应该思考 04/15 23:33
31F:→ baobomb: 怎麽在省去所有不必要的介面以及抽象化时 还能完整的测 04/15 23:33
32F:→ baobomb: 试 04/15 23:33
我自己做开发的原则:边界存在所以介面才存在,观察不到边界就不该乱创介面
而边界存在是因为有两个不同的域(抱歉我找不到合适的词汇)需要互动,而必须订出双方
都能同意去使用的一套语言,也就是一系列行为与资料的组合
为了测试去开新的interface 也不是不行,但通常会发生在系统对外的交界处,比如说
对DB、对Downstream service 的呼叫的那些client or repository
33F:推 lovdkkkk: 我觉得介面主要的用处不在於有几个实作, 即使 0 个都行 04/15 23:41
34F:→ lovdkkkk: 主要看两点, 一是有没有提供对某部份系统恰当的定义, 二 04/15 23:42
35F:→ lovdkkkk: 是所定义的范围是不是在恰当的边界 04/15 23:43
36F:→ lovdkkkk: 以 JPA 为例, 就是提供了 APP 及资料库间的定义 04/15 23:44
同意你的定义,但不太可能零个实作,如果是SPI的情况,那还是预期有实作可以介入的
如果是透过annotation 或Naming convention 搞meta programming,後面都还是有
code generator在生成行为的,所以有实作,只是不是programmer 自己写而已
37F:推 baobomb: 楼上正解 如果有跨模组间的使用需求 介面的使用是必要的 04/16 00:01
38F:→ baobomb: 同时也能缩短dependence间的critical path 然而很常看 04/16 00:01
39F:→ baobomb: 到一个类别只有一个实作 也没有跨模组间的使用 还偏偏要 04/16 00:01
40F:→ baobomb: 介面化... 就让人很无语 04/16 00:01
我工作的地方有些team 的工作习惯是开interface 就得加个I当开头
所以我code review 的时候就常常碎碎念:
你们这个开一个class 就要I一下到底是什麽病?
※ 编辑: zanyking (67.188.38.188 美国), 04/16/2022 06:00:21
41F:→ foreverk: HttpClient的例子好贴切,不过写不够多或是没有遇到情 04/16 07:34
42F:→ foreverk: 境,光看书或解释,真的不太能体会,所以我也很常用moc 04/16 07:34
43F:→ foreverk: k当说法 04/16 07:34
44F:推 jen1121: 每个class都要I个大概也是职业病了,cglib 动态代理帮你I 04/16 08:14
45F:→ jen1121: 好I满,但也得看框架运用场合适用 04/16 08:14
46F:→ c74319: 一个interface只有一个implement 不是为了让interface 可 04/16 12:50
47F:→ c74319: 以作为参数被传递吗?好像叫做behaviour parameterizatio 04/16 12:50
48F:→ c74319: ns ,重点在於能够reused same method and give it diffe 04/16 12:50
49F:→ c74319: rent behaviors. 04/16 12:50
50F:→ c74319: 主要是回覆发文者说「一个interface 只有一个实作没用」 04/16 12:57
51F:→ c74319: 的这句话,因为之前有看过书(java in action)有强调这个 04/16 12:57
52F:→ c74319: 用法。 04/16 12:57
53F:推 baobomb: reuse same method and give it diff behavior. 就表示这 04/16 14:20
54F:→ baobomb: 个介面 在不同的情况下 会有不同的行为 但如果确定只有一 04/16 14:20
55F:→ baobomb: 个实作 就表示这个介面永远只会有一个行为 那介面的意义 04/16 14:20
56F:→ baobomb: 不大 除非是这个介面会被其他跨模组的功能所使用 那介面 04/16 14:20
57F:→ baobomb: 的确需要 当然也有的人会习惯都预先做好介面化 以便将来s 04/16 14:20
58F:→ baobomb: cale的时候不需要再花时间 这也是可理解的 最怕的是有一 04/16 14:20
59F:→ baobomb: 些很爱什麽都要interface, abstract 类继承的depth超深 04/16 14:20
60F:→ baobomb: 但最後都只有一个实作... 然後也没有模组化因为什麽东 04/16 14:20
61F:→ baobomb: 西都塞在一个module里 这种的看了头真的很痛... 04/16 14:20
62F:推 netburst: 推楼上 04/16 14:33
63F:→ netburst: 但很多大厂的SDK也是都这样就是了 04/16 14:33
64F:→ netburst: 包含古哥~~~~ 04/16 14:34
65F:推 jason82714: 推baobomb大的说法,其实我也曾写文章聊这件事情 04/16 16:09
67F:推 moom50302: 介面一定要I开头这点真的是…Cleancode第一章命名就直 04/16 19:41
68F:→ moom50302: 接强调不要这样了XD 但是旧程式码很常见 04/16 19:41
69F:→ moom50302: 另外推楼上大大说的,这也是旧程式码常见的一个介面一 04/16 19:43
70F:→ moom50302: 个实作的状况… 04/16 19:43
71F:推 devilkool: 哇....现在才知道只有一个实作的介面是错的 04/17 02:05
72F:推 baobomb: 严格来说应该是像楼主所说 介面的必要性在於恰当的边界 04/17 09:35
73F:→ baobomb: 如果合理 那只有一个实作也是合理的 但不要为了介面而 04/17 09:35
74F:→ baobomb: 介面 04/17 09:35
75F:→ superpandal: 其实就是对耦合这个字有误解 但是表达应该是维护的方 04/18 02:36
76F:→ superpandal: 便 spring做了不代表是绝对的最佳范例 根据需要更改 04/18 02:39
77F:→ superpandal: 没错 但有些人会认爲这样一劳永逸 个人不理解 因爲扩 04/18 02:44
78F:→ superpandal: 充性有很多方法 04/18 02:44