我参与的大多数项目都使用几个开源组件。作为一般原则,始终避免将代码的所有组件绑定到第三方库,而是通过封装包装程序来避免更改的痛苦是个好主意吗?

例如,我们的大多数PHP项目都直接使用log4php作为日志框架,即通过\ Logger :: getLogger()实例化,它们使用-> info()或-> warn()方法等。但是,将来,假设日志记录框架可能会以某种方式出现。就目前而言,与log4php方法签名紧密相关的所有项目都必须在数十个地方进行更改,以适应新的签名。显然,这将对代码库产生广泛的影响,任何更改都是潜在的问题。

对于这种情况下的面向未来的新代码库,我经常考虑(有时实现)一个包装器类,以封装日志记录功能,并使其(尽管不是万无一失)更容易,以最小的更改来改变将来的日志记录工作方式;代码调用包装器,包装器将调用传递给日志记录框架。

请记住,其他库中有更复杂的示例,我是否过度设计还是明智的预防措施?在大多数情况下?

编辑:更多注意事项-使用依赖注入和测试加倍实际上要求我们无论如何都要抽象出大多数API(“我想检查我的代码是否执行并更新其状态,但不编写日志注释/访问真实数据库”)。这不是决定者吗?

评论

log4XYZ是一个如此强大的商标。其API的更改将不早于链表的API更改的时间。两者都是一个长期解决的问题。

此SO问题的精确副本:stackoverflow.com/questions/1916030/…

如果您仅在内部使用它,则是否包装只是当前已知工作与以后可能工作之间的权衡。审判电话。但是其他响应者似乎忽略了的一件事是它是API依赖还是实现依赖。换句话说,您是否正在通过自己的公共API从该第三方API中泄漏类,并将其公开给用户?在这种情况下,转移到其他库已不再是一件容易的事,问题是,现在不破坏自己的API就不可能了。这很糟糕!

供进一步参考:这种模式称为“洋葱体系结构”,其中外部基础结构(您称为外部库)隐藏在接口后面

#1 楼

如果仅使用第三方API的一小部分,则编写包装器是有意义的-这有助于封装和信息隐藏,从而确保您不会在自己的代码中暴露可能庞大的API。它还可以帮助确保您不希望使用的任何功能都被“隐藏”。

使用包装的另一个好理由是,如果您希望更改第三方库。如果您知道这是基础架构的一部分,则不会更改,请不要为其编写包装。

评论


好点了,但是我们被告知紧密耦合的代码是不好的,原因有很多容易理解的原因(难以测试,难以重构等)。问题的另一种措辞是“如果耦合不好,为什么可以耦合到API?”。

–很多空闲时间
2011年9月11日上午8:53

@lotsoffreetime您无法避免与API的某种耦合。因此,最好耦合到您自己的API。这样,您可以更改库,并且通常不需要更改包装程序提供的API。

–乔治·玛丽安(George Marian)
2011年9月11日上午9:06

@ george-marian如果我无法避免使用给定的API,则可以肯定地减少接触点。问题是,我应该一直在尝试这样做吗,还是那是过度的事情?

–很多空闲时间
2011年9月11日上午9:34

@lotsoffreetime这是一个很难回答的问题。为此,我已经扩展了答案。 (基本上,取决于很多if。)

–乔治·玛丽安(George Marian)
2011年9月11日上午9:49

@lotsoffreetime:如果您有很多空闲时间,则可以选择其中之一。但我建议您不要编写API包装器,除非在这种情况下:1)原始API级别很低,因此您编写了更高级别的API可以更好地满足特定项目的需要,或者2)您在计划中有一个计划在不久的将来切换库时,您在寻找更好的库时仅将当前库用作垫脚石。

– Lie Ryan
2011-09-11 9:57



#2 楼

通过包装第三方库,您可以在其上面添加一个抽象层。这具有一些优点:



您的代码库对于更改变得更加灵活

如果您需要用另一个库替换您,只需要在一个包装中更改您的包装中的实现。您可以更改包装程序的实现,而不必更改其他任何东西,换句话说,您有一个松散耦合的系统。否则,您将不得不遍历整个代码库并在各处进行修改-显然这不是您想要的。


您可以独立于库的API定义包装器的API。

不同的库可能具有截然不同的API,但同时它们可能都不是您真正需要的。如果某个图书馆需要在每次调用时传递令牌,该怎么办?您可以在需要使用库的任何地方将令牌传递给您的应用,也可以在更集中的地方对其进行保护,但是无论如何都需要令牌。包装器类使整个事情再次变得简单-因为您可以只将令牌保留在包装器类中,而不必将其公开给应用程序内的任何组件,而完全不需要它。如果您使用的库没有强调良好的API设计,那么这将是一个巨大的优势。


单元测试更简单

单元测试只能测试一件事。如果要对类进行单元测试,则必须模拟其依赖项。如果该类进行网络调用或访问软件之外的其他资源,则这一点变得尤为重要。通过包装第三方库,可以轻松模拟那些调用并返回测试数据或任何单元测试所需的内容。如果没有这样的抽象层,则执行此操作将变得更加困难-并且在大多数情况下,这将导致大量难看的代码。


创建松散耦合的系统

包装器的更改对软件的其他部分没有影响-至少在不更改包装器行为的情况下。通过引入像该包装程序这样的抽象层,您可以简化对库的调用,并几乎完全删除应用程序对该库的依赖。您的软件将只使用包装器,而包装器的实现方式或工作方式不会改变。



实际示例

说实话。人们可以在几个小时内争论这种事情的优缺点-这就是为什么我只想给你看一个例子。

假设您有某种Android应用程序,需要下载图像。那里有很多库,例如Picasso或Universal Image Loader,使加载和缓存图像变得轻而易举。

我们现在可以定义一个接口,用于包装最终使用的库:

public interface ImageService {
    Bitmap load(String url);
}


这是接口我们现在可以在需要加载图像时在整个应用程序中使用。我们可以创建此接口的实现,并在所有使用ImageService的地方使用依赖项注入来注入该实现的实例。假设我们最初决定使用Picasso。现在,我们可以为ImageService编写一个内部使用毕加索的实现:

public class PicassoImageService implements ImageService {

    private final Context mContext;

    public PicassoImageService(Context context) {
        mContext = context;
    }

    @Override
    public Bitmap load(String url) {
        return Picasso.with(mContext).load(url).get();
    }
}


如果你问我的话,很简单。库周围的包装不必复杂就可以使用。接口和实现只有不到25行代码,因此创建它几乎不需要花什么力气,但是通过这样做,我们已经有所收获。看到实施中的Context字段吗?您选择的依赖项注入框架将已经可以在我们使用ImageService之前注入该依赖项,您的应用现在不必关心图像的下载方式以及库可能具有的任何依赖项。您的应用程序看到的只是一个ImageService,当需要图像时,它会调用带有网址的load()-简单明了。

但是,当我们开始改变事物时,真正的好处就出现了。假设我们现在需要用Universal Image Loader替换Picasso,因为Picasso不支持我们现在绝对需要的某些功能。现在我们是否必须梳理我们的代码库,并单调乏味地替换所有对Picasso的调用,然后由于忘记了几个Picasso调用而处理了几十个编译错误?不需要。我们要做的就是创建一个新的ImageService实现,并告诉我们的依赖项注入框架从现在开始使用该实现:

public class UniversalImageLoaderImageService implements ImageService {

    private final ImageLoader mImageLoader;

    public UniversalImageLoaderImageService(Context context) {

        DisplayImageOptions defaultOptions = new DisplayImageOptions.Builder()
                .cacheInMemory(true)
                .cacheOnDisk(true)
                .build();

        ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
                .defaultDisplayImageOptions(defaultOptions)
                .build();

        mImageLoader = ImageLoader.getInstance();
        mImageLoader.init(config);
    }

    @Override
    public Bitmap load(String url) {
        return mImageLoader.loadImageSync(url);
    }
}


您可以看到实现可能有很大不同,但这并不重要。我们无需在应用程序中的其他任何地方更改任何代码。我们使用完全不同的库,该库可能具有完全不同的功能或使用方式可能非常不同,但我们的应用程序不在乎。与之前我们的应用程序其余部分相同,只是使用ImageService方法看到了load()接口,但是实现此方法不再重要。

至少对我来说,这一切听起来已经不错了,但是等等!还有更多。想象一下,您正在为正在处理的类编写单元测试,并且该类使用ImageService。当然,您不能让单元测试对位于其他服务器上的某些资源进行网络调用,但是由于您现在正在使用ImageService,因此您可以通过实现模拟的load()轻松地让Bitmap返回用于单元测试的静态ImageService: br />
public class MockImageService implements ImageService {

    private final Bitmap mMockBitmap;

    public MockImageService(Bitmap mockBitmap) {
        mMockBitmap = mockBitmap;
    }

    @Override
    public Bitmap load(String url) {
        return mMockBitmap;
    }
}



总而言之,通过包装第三方库,您的代码库对于更改变得更加灵活,整体更简单,易于测试,并且减少了不同组件的耦合在您的软件中-维护软件的时间越长,所有事情变得越来越重要。

评论


这同样适用于不稳定的API。我们的代码不会在1000个地方发生变化,仅仅是因为基础库发生了变化。很好的答案。

–RubberDuck
2015年9月25日,下午2:56

非常简洁明了的答案。我在网络上进行前端工作。那片风景的变化是疯狂的。人们“认为”他们不会改变的事实并不意味着不会有任何改变。我看到有人提到YAGNI。我想添加一个新的缩写YDKYAGNI,您不知道自己是否会需要它。特别是与Web相关的实现。通常,我总是包装仅公开小型API(如select2)的库。较大的库会影响您的体系结构,并且将它们包装起来意味着您期望体系结构可能会有所更改,但是这样做的可能性较小。

–再见
16 Dec 17'在10:08

您的回答非常有帮助,并通过示例展示概念使概念更加清晰。

– Anil Gorthy
18年2月2日在19:26

#3 楼

如果不知道这个所谓的未来改进记录器将具有哪些超伟大的新功能,您将如何编写包装器?最合乎逻辑的选择是让包装器实例化某种记录器类,并使用->info()->warn()之类的方法。换句话说,与您当前的API基本相同。

我宁愿使用“过去证明”,也不用我可能永远都不需要更改它们,或者无论如何都需要不可避免的重写。码。就是说,在极少数情况下,我确实需要更改组件,那就是我编写包装程序以使其与过去的代码兼容。但是,任何新代码都使用新API,并且无论何时或在计划允许的情况下,只要对同一个文件进行更改,我都会重构旧代码以使用它。几个月后,我可以删除包装器,并且此更改是逐步而稳健的。

换一种说法,包装器仅在您已经知道需要包装的所有API时才有意义。如果您的应用程序当前需要支持许多不同的数据库驱动程序,操作系统或PHP版本,就是很好的例子。

评论


“ ...只有当您已经知道需要包装的所有API时,包装器才真正有意义。”如果我在包装器中匹配了API,这将是正确的。也许我应该比包装器更强烈地使用术语“封装”。我将抽象这些API调用以“以某种方式记录此文本”,而不是“使用此参数调用foo :: log()”。

–很多空闲时间
2011年9月11日上午8:34

“如果不知道这个所谓的未来改进的记录器将具有哪些超伟大的新功能,您将如何编写包装器?”下面的@ kevin-cline提到了性能更高的记录器,而不是更新的功能。在这种情况下,无需包装新的API,只需使用其他工厂方法即可。

–很多空闲时间
2011-09-11 8:35



#4 楼

我认为,今天包装第三方库以防明天出现更好的情况是对YAGNI的非常浪费的违反。如果您以应用程序特有的方式重复调用第三方代码,则将(应该)将这些调用重构为包装类以消除重复。否则,您将完全使用库API,并且任何包装程序都将看起来像库本身。

现在假设出现了一个性能优越的新库。在第一种情况下,您只需重写新API的包装。没问题。

在第二种情况下,您将创建一个包装器,以适应旧接口来驱动新库。要做更多的工作,但是没有问题,并且比以前编写包装程序要完成的工作还多。

评论


我认为YAGNI不一定适用于这种情况。这与构建功能无关,以防将来可能需要。这是关于在架构中构建灵活性。如果不需要这种灵活性,那么可以使用YAGNI。但是,这种决定往往会在将来某个时候做出,因为做出更改可能会很痛苦。

–乔治·玛丽安(George Marian)
2011年9月11日上午9:29

@乔治·玛丽安:问题是95%的时间,您将永远不需要灵活性来改变。如果您需要切换到性能更高的未来新库,那么在需要时搜索/替换调用或编写包装器应该是相当简单的。另一方面,如果您的新库具有不同的功能,则包装器将成为一个障碍,因为现在您有两个问题:移植旧代码以利用新功能并维护包装器。

– Lie Ryan
2011年9月11日上午10:09

@lotsoffreetime:“好的设计”的目的是使应用程序的整个生命周期内的总成本降至最低。为预期的未来变化添加间接层是非常昂贵的保险。我从未见过有人从这种方法中获得任何节省。它只是为程序员创造了琐碎的工作,而他们的时间会花费在满足客户特定要求上的时间要多得多。在大多数情况下,如果编写的代码不是特定于客户的,那是在浪费时间和金钱。

–kevin cline
2011年9月11日14:14在

@乔治:如果这些变化很痛苦,我认为那是一种过程的气味。在Java中,我将使用与旧类相同的名称创建新类,但在不同的程序包中,更改所有出现的旧程序包名称,然后重新运行自动化测试。

–kevin cline
2011年9月11日17:08

@kevin与简单地更新包装程序和运行测试相比,这需要更多的工作,因此带来更多的风险。

–乔治·玛丽安(George Marian)
2011-09-12 6:50

#5 楼

围绕第三方库编写包装器的基本原因是,您可以交换该第三方库而无需更改使用它的代码。您无法避免与某事物耦合,因此有理由认为,最好耦合至您编写的API。

值得付出努力的是另一回事。该辩论可能会持续很长时间。

对于小型项目而言,需要进行此类更改的可能性很小,这可能是不必要的努力。对于较大的项目,灵活性可能远远超过包装库所付出的额外努力。但是,很难事先知道是否是这种情况。

另一种看待它的方法是抽象出可能发生变化的基本原理。因此,如果第三方库建立良好且不太可能更改,则最好不包装它。但是,如果第三方库相对较新,则很有可能需要替换它。也就是说,建立图书馆的开发已经被放弃了很多次。因此,这不是一个容易回答的问题。

评论


在进行单元测试的情况下,能够注入模拟API可以最大程度地减少被测单元的数量,“改变潜力”并不是一个因素。话虽如此,这仍然是我最喜欢的答案,因为它与我的想法最为接近。鲍伯叔叔会怎么说? :)

–很多空闲时间
2011年9月11日11:26在

同样,小型项目(没有团队,基本规范等)有其自己的规则,在这些规则中,您可以违反诸如此类的良好实践并在一定程度上逃避它。但这是一个不同的问题...

–很多空闲时间
2011年9月11日上午11:27

#6 楼

我坚决参与包装工作,而不是能够以最大的优先级来替代第三方库(尽管这是一个奖励)。我赞成包装的主要理由很简单



第三方库不是为满足我们的特定需求而设计的。




这通常以大量代码重复的形式体现出来,例如开发人员编写8行代码只是为了创建一个QButton并按照应有的外观来设计应用程序的样式,这不仅对于设计人员而言,不仅外观以及按钮的功能对于整个软件来说都是完全改变的,最终需要返回并重写成千上万行代码,或者发现渲染管道的现代化需要史诗般的重写,因为代码库撒满了低级的临时性固定流水线的OpenGL代码无处不在,而不是集中实时渲染器设计,而仅将OGL严格用于其实现。

这些设计并非针对我们的特定设计需求量身定制。它们倾向于提供实际需要的大量超集(而设计的不重要部分甚至比其重要的更多),并且它们的接口并非旨在专门满足我们的高级需求。 “思想=一个请求”这种方式,如果我们直接使用它们,则会剥夺我们所有中央设计控制权。如果开发人员最终写出了比表达他们的需求所需的代码低得多的代码,则有时他们最终可能会以临时方式将它们自己包装起来,这样一来,您最终会得到数十个草率编写的粗略的代码,设计并记录了文档的包装程序,而不是一种设计合理,记录良好的方法。

当然,我会将强例外应用于库,其中包装器几乎是第三方API所提供内容的一对一翻译。在那种情况下,可能没有寻求更直接表达业务和设计要求的更高层次的设计(对于类似于“实用程序”库的东西可能就是这种情况)。但是,如果有更多的量身定制的设计可以更直接地表达我们的需求,那么我坚决参与包装工作,就像我坚决支持使用更高级别的功能并在内联汇编代码中重用它一样到处都是。

奇怪的是,我与开发人员发生冲突时,他们似乎对我们的设计能力(例如创建按钮并返回按钮的功能)感到如此不信任和悲观。 d在设计和使用上述功能时,宁愿编写8行低级代码,重点放在按钮创建的微观细节上(最终将需要在以后重复更改)。如果我们不能相信自己以合理的方式设计这类包装,我什至没有看到我们试图设计任何东西的目的。

换一种方式,我看到第三种政党图书馆,可以节省大量实施时间,而不是设计系统的替代方法。

#7 楼

除了@Oded所说的以外,我还想为日志记录的特殊目的添加此答案。


我总是有一个日志记录接口,但是我不必替代一个log4foo框架呢。

提供界面和编写包装程序只需要半小时,所以我想如果不必要的话,您不会浪费太多时间。

这是YAGNI的特例。尽管我不需要它,但并不需要花费很多时间,我对此感到更安全。如果真的可以交换记录器的日子到了,我会很高兴我花了半个小时,因为与真实世界项目中的电话交换相比,这可以节省我一天以上的时间。而且我从来没有编写或看到过用于日志记录的单元测试(除了记录器实现本身的测试之外),因此在没有包装的情况下会出现缺陷。

评论


我不希望更改log4foo,但是它已广为人知,并作为示例。到目前为止,这两个答案是如何互补的,也很有趣-“不要总是包装”; “换行以防万一”。

–很多空闲时间
2011-09-10 21:34



@Falcon:你把所有东西都包好吗? ORM,日志界面,核心语言类?毕竟,永远无法判断何时需要更好的HashMap。

–kevin cline
2011-09-12 23:50

#8 楼

我正在处理我当前正在处理的项目中的确切问题。但就我而言,该库用于图形,因此我可以将它的使用限制为处理图形的少数类,而不是将其散布在整个项目中。因此,如果需要的话,以后切换API相当容易。对于记录器而言,事情变得更加复杂。

因此,我要说的决定与第三方库到底在做什么以及与之相关的痛苦有很大关系。改变它。如果更改所有API调用很容易,那么可能不值得这样做。但是,如果以后要更改库真的很难,那么我现在就可以包装它。


除此之外,其他答案也很好地涵盖了主要问题,所以我只想重点讨论一下最后,关于依赖注入和模拟对象。当然,这取决于您的日志记录框架的工作原理,但是在大多数情况下,这不需要包装器(尽管它可能会受益于包装器)。只需使您的模拟对象的API与第三方库完全相同,然后就可以轻松地交换模拟对象进行测试。

这里的主要因素是第三方是否库甚至通过依赖项注入(或服务定位器或某些此类松散耦合的模式)来实现。如果通过单例或静态方法或其他方法访问库函数,则需要将其包装在可以在依赖项注入中使用的对象中。

#9 楼

我对第三方库的想法:

iOS社区中最近有一些关于使用第三方依赖项的利弊(好的,主要是利弊)的讨论。我看到的许多论点都相当笼统-将所有第三方库归为一篮子。不过,与大多数事情一样,它并不是那么简单。因此,让我们尝试着眼于单个案例

我们应该避免使用第三方UI库吗?

考虑第三方库的原因:

开发人员考虑使用第三方库的主要原因可能有两个:



缺乏技能或知识。假设您正在开发照片共享应用。您不必先使用自己的加密货币。

缺乏时间或兴趣来构建某些东西。除非您有无限制的时间(尚无人),否则您必须确定优先级。

大多数UI库(不是全部!)都属于第二类。这些东西不是火箭科学,但是要正确地构建它需要花费时间。



如果这是一项核心业务功能–那就自己动手,不管它是什么。



控件/视图有两种类型:




泛型,允许您在其创建者甚至没有想到的许多不同的上下文,例如

具体针对单个用例设计,例如UICollectionView
大多数第三方库往往属于第二类。而且,它们通常是从现有的代码库中提取出来的,并对其进行了优化。

早期未知的假设

许多开发人员都会对其内部代码进行代码审查,但可能认为第三方源代码的质量是理所当然的。只是花一些时间浏览图书馆的代码是值得的。您可能最终会惊讶地看到一些危险信号,例如在不需要的地方使用毛刷。



通常,学习这种想法比获取结果代码本身更有益。



您无法隐藏它

由于UIKit的方式设计您很可能将无法隐藏第三方UI库,例如在适配器后面。库将与您的UI代码交织在一起,成为您的项目的事实。

未来的时间成本

UIKit在每个iOS版本中都会更改。事情会破裂。您对第三方的依赖性不会像您期望的那样免维护。

结论:

根据我的个人经验,大多数第三方UI代码的使用都沸腾了到交换较小的灵活性以获取一些时间。

我们使用现成的代码更快地发布当前版本。不过,迟早我们会达到图书馆的极限,站在一个艰难的决定之前:下一步该怎么做?

#10 楼

对于开发人员团队而言,直接使用该库更为友好。当新的开发人员加入时,他可能对所使用的所有框架都有充分的经验,但在学习您自己的API之前将无法做出有贡献的贡献。当年轻的开发人员尝试在您的团队中取得进步时,他将被迫学习您在其他任何地方都没有的特定API,而不是获得更有用的通用能力。如果有人知道原始API的有用功能或可能性,则可能无法覆盖不了解它们的人所编写的层。如果某人在找工作时要完成编程任务,可能无法证明他多次使用的基本知识,仅仅是因为所有这些时间,他都在通过包装器访问所需的功能。

我认为这些问题可能比以后使用完全不同的库的可能性要大得多。我唯一要使用包装器的情况是,肯定要计划迁移到另一个实现,或者包装的API不够冻结并不断变化。