使用第三方库-始终使用包装器吗?


78

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

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

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

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

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


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


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

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

Answers:


42

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

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


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

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

@ george-marian如果我无法避免使用给定的API,则可以肯定地减少接触点。问题是,我应该一直尝试执行此操作,还是执行过度?
lotsoffreetime 2011年

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

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

28

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

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

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


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

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

27

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

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

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

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

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

  • 单元测试更简单

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

  • 您创建一个松耦合的系统

    包装器的更改对软件的其他部分没有影响-至少在不更改包装器行为的前提下。通过引入诸如此类包装程序的抽象层,您可以简化对库的调用,并几乎完全删除应用程序对该库的依赖。您的软件将只使用包装器,而包装器的实现方式或执行方式将不会产生任何影响。


实际例子

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

假设您有某种Android应用程序,并且需要下载图片。那里有大量的库,例如PicassoUniversal Image Loader,使加载和缓存图像变得轻而易举。

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

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

这是我们现在在需要加载图像时可以在整个应用程序中使用的界面。我们可以创建此接口的实现,并使用依赖项注入在每个使用的地方注入该实现的实例ImageService

假设我们最初决定使用毕加索。现在,我们可以编写一个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()使用url进行调用-简单明了。

但是,当我们开始改变事物时,真正的好处就来了。假设我们现在需要用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

public class MockImageService implements ImageService {

    private final Bitmap mMockBitmap;

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

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

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


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

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

您的回答非常有帮助,并通过示例展示概念使概念更加清晰。
阿尼尔·戈尔西(Annil Gorthy),

24

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

现在,假设出现了具有更高性能或其他性能的新库。在第一种情况下,您只需重写新API的包装。没问题。

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


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

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

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

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

1
@kevin比起简单地更新包装器和运行测试,这需要更多的工作,因此带来更多的风险。
乔治·玛丽安

9

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

是否值得付出努力是另外一个故事。这场辩论可能会持续很长时间。

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

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


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

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

1

除了@Oded已经说过的内容外,我还想添加此答案以用于日志记录的特殊目的。


我总是有一个用于记录日志的接口,但是我从来不需要替代log4foo框架。

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

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


我不希望更改log4foo,但是它已广为人知,并作为示例。有趣的是,到目前为止,这两个答案是如何互补的?“以防万一”。
lotsoffreetime 2011年

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

1

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

因此,我想说这个决定与第三方库到底在做什么以及更改它会带来多大的痛苦有很大关系。如果更改所有API调用很容易,那么可能不值得这样做。但是,如果以后很难更改库,那么我现在可以将其包装。


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

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


1

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

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

这通常以大量代码重复的形式体现出来,例如开发人员编写8行代码只是为了创建一个A QButton并对其进行样式设计,而其外观则应为应用程序,而设计人员不仅希望外观而且按钮的功能要针对整个软件进行完全更改,最终需要返回并重写数千行代码,或者发现现代化的渲染管道需要进行史诗般的重写,因为代码库撒满了低级的临时固定代码,到处都可以使用管道OpenGL代码,而不是集中实时渲染器设计,而将OGL严格用于其实现。

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

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

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

换句话说,我将第三方库视为可以节省大量实施时间的方法,而不是设计系统的替代方法。


0

我对第三方库的想法:

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

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

考虑第三方库的原因:

开发人员考虑使用第三方库的原因似乎有两个:

  1. 缺乏技能或知识。假设您正在开发照片共享应用。您不必先滚动自己的加密货币。
  2. 缺乏时间或兴趣来建造东西。除非您有无限的时间(尚无人),否则您必须确定优先级。

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

如果这是一项核心业务功能-不管它是什么,请自己完成。

控件/视图几乎有两种类型:

  1. 泛型,可让您在其创建者甚至没有想到的许多不同情况下使用它们,例如UICollectionViewfrom UIKit
  2. 具体而言,设计成用于单次使用的情况下,例如UIPickerView。大多数第三方库往往属于第二类。而且,它们通常是从现有代码库中提取出来的,针对这些代码库进行了优化。

未知的早期假设

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

通常,学习该想法比获得结果代码本身更有益。

你不能隐藏它

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

未来时间成本

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

结论:

根据我的个人经验,大多数第三方UI代码的使用都归结为交换较小的灵活性以获得一定的时间。

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


0

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.