你应该把你采用的第三方库封装到你的项目中吗?
我今天和一位同事讨论过。
他声称,每当你使用第三方库,你应该总是写一个包装。 所以你可以随时改变事情,并容纳你的具体用途。
我不同意总是这个词,关于log4j的讨论引起了,我声称log4j已经经过了良好的testing和时间validation的API和实现,并且所有可以想象的事情都可以被事后configuration,没有什么东西你应该包装的。 即使你想打包,也有像commons-logging和log5j这样的经过validation的包装。
我们在讨论中碰到的另一个例子是Hibernate。 我声称它有一个非常大的API被包装。 此外,它有一个分层的API,可以让你调整它的内部,如果你需要的话。 我的朋友声称,他仍然认为应该封装,但他没有做到这一点,因为API的大小(在我们目前的项目中,这个同事比我更老)。
我声明这一点 ,并且应该在特定情况下进行包装:
- 你不确定图书馆将如何满足你的需求
- 你只会使用一小部分库(在这种情况下,你可能只暴露了一部分API)。
- 您不确定图书馆API或实施的质量。
我也认为,有时你可以包装你的代码,而不是库。 例如,把你的数据库相关的代码放在一个DAO层,而不是抢先包装所有的hibernate。
那么,最后这不是一个真正的问题,但你的见解,经验和意见是高度赞赏的。
这是YAGNI的一个完美的例子:
- 这是更多的工作
- 它膨胀了你的项目
- 这可能会使您的devise复杂化
- 它没有直接的好处
- 你所写的情景可能永远不会显现
- 当它的时候,你的包装很可能需要被完全重写,因为它与你正在使用的具体库绑定的太紧密了,新的API与你的API不匹配。
那么,切换技术显然是有好处的。 如果你有一个已经被弃用的库,并且想要切换,那么你最终可能会重写很多代码来适应这个变化,而如果它被包装了,你可以更容易地为新的库写一个新的包装器,而不是改变你的所有代码。
另一方面,这意味着你必须为你包含的每一个微不足道的库编写一个包装,这可能是一个不可接受的开销。
我的行业全是速度问题,所以我唯一能够certificate自己是封装的是,如果是在一个关键的图书馆周围,可能会经常发生戏剧性的变化。 或者更常见的是,如果我需要build立一个新的图书馆,并将其用于旧的代码,这是一个不幸的现实。
这绝对不是一个“永远”的情况。 这是可取的。 但时间并不总是在那里,最后,如果写一个包装需要花费几个小时,而且长期的代码库变化将会很less,而且是微不足道的。为什么呢?
这里的问题部分是“包装”这个词,部分是虚假的二分法,部分是JDK和其他一切之间的错误区分。
单词'包装'
如你所说,包装所有的Hibernate是一个完全不切实际的企业。
另一方面,将Hibernate依赖关系限制在一个已识别的,受控的源文件集合中,可能是实际可行的,并且会达到相同的结果。
错误的二分法
错误的二分法是没有认可第三个select:标准。 如果你使用JPA批注,你可以把Hibernate换成其他的东西。 如果您正在编写Web服务并使用JAX-WS注释和JAX-B,则可以在JDK,CXF,Glassfish或其他任何服务之间进行切换。
错误的区别
当然,JDK变化缓慢,不太可能死亡。 但是主要的开源软件包也变化缓慢,不太可能死亡。 无数的开发人员和项目使用Hibernate。 Hibernate没有更多的风险消失,或者做出与Java本身不同的API不可比的改变。
如果你计划包装的图书馆在同一领域的其他产品的“访问原则,隐喻和成语”中是独一无二的,那么你的包装将非常类似于这个图书馆,如果不是,你有一天会切换到不同的库,因为你将需要一个新的包装。
如果以类似于其他库的方式访问库,并且相同的包装器可以应用于这些库,那么它们可能是基于现有的一些标准编写的,并且有一些已经存在的用于访问它们的公共层。
如果我确实知道我将不得不在生产中支持多个和实质上不同的库,那么我只能用包装器去。
我同意所有的说法。
包装第三方代码的唯一时间是有用的(酒吧违反YAGNI)是unit testing。
模仿静态等需要你包装代码,这是编写第三方代码包装的有效理由。
在日志代码的情况下,它不需要。
决定是否打包一个库的主要因素是库变化对代码的影响。 当一个图书馆只从一个class级被调用时,改变图书馆的影响将是最小的。 另一方面,如果在所有类中调用一个库,包装器的可能性要大得多。
-
任何关于第三方库select的不确定性应该在项目开始时使用原型来testing可扩展性/适用性/第三方库的任何内容。
-
如果您决定继续提供全面的解耦/抽象支持,那么它应该被成本计算并最终被项目发起人批准 – 最终这是一个商业决策,因为有人必须为此付费,并且需要完成这项工作(除非绝对微不足道,在这种情况下,api可能是低风险的)。
-
一般来说,一位经验丰富的架构师会select一种技术,他们可以相当自信,有经验,而且他们有信心能够持续到应用程序的整个生命周期,否则他们会在项目早期的决策中消除任何风险,从而消除了大部分时间需要做的事情
不,Javabuild筑师/想要的蜜蜂太忙于devise反对想象的变化。
用现代的IDE,当你需要改变的时候,这是一块蛋糕。 在此之前,保持简单。
我倾向于同意你的观点。 使用绝对值经常让你陷入麻烦,并说你应该“总是”做一些限制你的灵活性。 我会添加更多的点到你的清单。
当你在一个非常通用的API(比如Hibernate或者log4j)中使用包装代码的时候,使得新开发人员变得更加困难。 新的开发者现在必须学习一个全新的API,如果你没有包装代码,他们会马上变得非常熟悉。
另一方面,您也将开发人员的视图限制在API中。 使用API的高级function需要更多的时间,因为您必须确保您的包装器是以可以处理它的方式实现的。
我见过的许多包装层也是非常具体的底层实现。 所以,如果你写一个log4j的日志包装器,你正在用log4j术语来思考。 如果一个新的酷炫的框架出来,它可能会改变整个范例,所以你的包装代码不会像你想象的那样迁移。
我绝对不是说包装代码总是不好,但正如你所说,有很多因素你必须考虑。
包装一个经过充分testing和久经考验的第三方库的目的是,你可能决定在未来的某个时候切换库。 包装它使得在不更改核心应用程序中的任何代码的情况下更容易切换。 只有包装需要改变。
如果你确定绝对不会(另一个绝对)在你的项目中使用不同的日志框架,请继续并跳过这个包装器。 即使说了这样的话,我可能会迟迟不会写封皮,直到我知道我需要它,就像我第一次需要切换。
这是一个有趣的问题。
我曾经在系统中发现过我们正在使用的库中有哪些缺陷,哪些上游不再维护,或者对修复不感兴趣。 在像Java这样的语言中,你通常不能修复来自包装的内部错误。 (幸运的是,如果他们是开源的,至less可以自己修复它们)。所以这里没有帮助。
但是我经常使用一种语言,在任何时候都可以轻松修改库,而无需查看甚至没有源代码 – 例如,我通常会将新方法添加到现有类中。 所以在这种情况下,包装是没有意义的:只要做出你想要的改变即可。
另外,你的同事是否在所谓的“图书馆”上划线? Java本身呢? 他是否包装内置类? 他是否包装文件系统? 线程调度程序? 内核? (也就是说,用他自己的包装 – 从某种意义上说, 一切都是围绕着CPU的包装,但是听起来好像他在讨论你的源代码仓库中的包装,完全在你的控制之下)。我有内置的function新版本出现时会改变或消失。 Java 不能免于此。
所以总是写一个包装的想法归结为一个赌注。 假设他只是包装第三方库,他似乎暗中下注:
- “第一方”function(如Java本身,内核等)永远不会改变
- 当“第三方”function发生变化时,将始终以可以在包装器中修复的方式完成
你的情况真的如此吗? 我不知道。 在我所做的大中型Java项目中,对我来说很less。 我不会花费精力包装所有的第三方库,因为这似乎是一个不好的select,但是你的情况与我的情况是完全不同的。
有一种情况,你有理由可以包装。 即如果你需要testing的东西,并且默认的第三方对象是重量级的。 然后有一个接口可以真正有所作为。
请注意,这不是要取代图书馆,而是使它在无关紧要的情况下易于pipe理。
包装整个图书馆在大多数情况下是模板化的,无效的和错误的。 这可以用一个非常聪明的方式来完成。 我想说,包装一个库大部分是在UI组件库的情况下是合适的,而且你必须添加一些额外的核心function到你需要的所有组件。
- 如果需要太多的修改和添加,这很可能不是你正在寻找的库
- 如果有适量的添加和修改 – 总是有这些devise模式在这些情况下派上用场。 例如, 装饰者模式 (允许将新的/额外的行为dynamic地添加到现有的对象)在大多数情况下是相当合适的。
- IDE的search/replace和重构function提供了一个简单的方法来改变你的代码在所有需要的地方,如果一些重要的变化是必要的,并出现一个包装对象。 (当然,unit testing在这里会有所帮助;))
你应该经常这样做,有时甚至是很less的,或者从不。 即使你的同事总是这样做,但有启发性的情况永远是永不止息的。 假设有时是必要的。 如果你从来没有包过一个图书馆,最糟糕的结果就是有一天你发现你在各地都有一个图书馆是必要的。 需要一些时间来包装图书馆,并对客户进行霰弹枪手术。 问题是,这种可能性是否会比习惯性地提供很less需要的包装纸花费更多的时间,但是从来没有执行过霰弹枪手术。
我的本能是要吸引YAGNI(你不会需要)原则,select“很less”。
我不会把它作为一对一的东西来包装,但我会分层的应用程序,以便尽可能地更换每个部分。 ISO OSI模式适用于所有types的软件:-)
根据我的经验,如果你充分使用抽象的话,这个问题就变得相当实际了。 耦合到一个库就像耦合到任何其他接口。 因此,如果您需要更换实施,您希望减less偶然的耦合以及必要的重写范围。 不要将你的应用程序逻辑绑定到某个构造上,而是不要仅仅围绕某些东西形成一堆愚蠢的(字面上的)包装,并希望获得任何好处。
一个包装通常不会为你带来任何东西,除非它回答了一个特定的目的(比如多态非结构化)。 他们经常出现重构,但我不会build议在他们身上build立一个架构。 当然有一些例外,但是有一些原则。
这不适用于适配器。 适配器可以是一个非常重要的组件,当你想要实际改变一个库的接口和它的使用,以符合你的项目中的体系结构,代码或领域概念。