在上篇文章中我们完成了技术选型,我们放弃了成熟的选用了 sevenzipjbind
作为压缩的核心。
sevenzipjbind
提供的 API 还残留着 C++
风味,我们有必要自己再包装一层,把它
变成易用的 Scala
API。所以我们干脆另起一个新项目,先来实现一个 SevenZ4S
(Seven Zip for Scala) Library。
一开始,决定包装策略。先阅读项目 User Guide,了解到 sevenzipjbind
依赖于 7Z 的原生动态链接库工作,
即依赖于 net.sf.sevenzipjbinding.sevenzipjbinding-all-platforms。sevenzipjbind 在此
基础上包装了调用 native 方法的逻辑。所以我们的包装也应当单单基于 sevenzipjbind
库本身,
让我们的 Scala
Library 在内部生成对应逻辑,调用 sevenzipjbind
的 API 完成工作。
sevenzipjbinding-all-platforms
甚至 sevenzipjbind
应该对于我们的库的使用者基本不可见。
接着,分析被包装库。鉴于我已经通过 Gradle
引入依赖来运行 demo,
可以在 Gradle
Local Repo 中找到 sevenzipjbind
的 Java
源码。
哪怕有文档,
直接读源码的工作量还是很大的。所以要灵活运用 IDEA
的 diagram 功能生成类的 UML 图,以快速从整体上认清库的设计层次、大致结构。
在一个符号(类或接口)上右击,还可选择“find usages”功能找到它的使用处、父类、子类和
实现类等,以理清单个类或接口的作用。例如,通过这些方法,我们很快就能发觉,
被包装库中大量存在钻石继承的
接口(interface),而且只有继承树最下方的子类有对应的实现类。不难想到,这是为了方便
与原生的 C++
API 交互而采取的手段,也是我们的包装类一定要改造的部分。我们还能发现,
有一些名字中带有 Callback 的接口在库中没有具体实现类,他需要用户用匿名类实现并
补充相关方法以供 native 方法回调;但是 Scala
一般不用匿名类干这种事情,而是转而
使用高阶函数这种优雅的途径,这也是我们需要改造的地方。
通过对被包装库的细致分析,我们对自己要做的工作就有了清晰的认识。是时候总体设计一下
我们的库 SevenZ4S
了。sevenzipjbind
支持对压缩包的三种操作:压缩、部分更新与解压缩;
相应地,我们设计三个类来处理:ArchiveCreator
、ArchiveUpdater
与 ArchiveExtractor
。
sevenzipjbind
的解压缩模块可以自动识别压缩包格式(format),且支持格式多,
因而为所有格式提供通用的接口;那么我们的库就把 ArchiveExtractor
设计成具体(concrete)类。
但是,压缩和部分更新功能仅支持 5 种格式(7z、zip、gzip、tar 与 bzip2),且不同格式各有
特点;所以 ArchiveCreator
、ArchiveUpdater
设计成抽象的 trait。各个格式的压缩和部分更新
处理逻辑类似,但是泛型的 type parameter 不同;所以 trait 也要带类型参数。
如下所示,Scala
的 trait 有两个地方可以放类型参数,声明处与 type memeber;
一般把概念上与 trait 自身性质有关的类型参数放在声明处,而把其他的当作普通的 trait 内部成员(memeber),
以 protected
修饰,用作子类多态。
trait AbstractArchiveCreator {
protected type TEntry <: CompressionEntry
protected type TItem <: IOutItemBase
// ...
}
不同格式的压缩包支持一些不同的 feature,我们为 feature 用 trait 建模。
trait SetEncryptHeader {
protected val archive: IOutFeatureSetEncryptHeader
def setHeaderEncryption(b: Boolean): Unit = {
archive.setHeaderEncryption(b)
}
}
trait SetLevel {
protected val archive: IOutFeatureSetLevel
def setLevel(compressionLevel: Int): Unit = {
archive.setLevel(compressionLevel)
}
}
trait SetMultithreading {
protected val archive: IOutFeatureSetMultithreading
def setThreadCount(threadCount: Int): Unit = {
archive.setThreadCount(threadCount)
}
}
因为这些 trait 没有什么代码层面的共性,所以没有必要过度设计只有标记作用的父接口。
它们本质上都是代理,只是转发 sevenzipjbind
里面的 archive
类型的对应方法。
AbstractArchiveCreator
的具体的子类根据需要设置 archive
,并继承这些 feature 即可
自动获得对应的方法。
final class ArchiveCreator7Z() extends AbstractArchiveCreator
with SetEncryptHeader
with SetLevel
with SetMultithreading {
override protected type TEntry = CompressionEntry7Z
override protected type TItem = IOutItem7z
override protected val archive: IOutCreateArchive7z = ???
}
sevenzipjbind
原生的使用方法大概像这样:
import ...
public class CompressNonGenericGZip {
/**
* The callback provides information about archive items.
*/
private final class MyCreateCallback
implements IOutCreateCallback<IOutItemGZip> {
public void setOperationResult(boolean operationResultOk)
throws SevenZipException {
// Track each operation result here
}
public void setTotal(long total) throws SevenZipException {
// Track operation progress here
}
public void setCompleted(long complete) throws SevenZipException {
// Track operation progress here
}
public IOutItemGZip getItemInformation(int index,
OutItemFactory<IOutItemGZip> outItemFactory) {
IOutItemGZip item = outItemFactory.createOutItem();
item.setDataSize((long) content.length);
return item;
}
public ISequentialInStream getStream(int i) throws SevenZipException {
return new ByteArrayStream(content, true);
}
}
byte[] content;
public static void main(String[] args) {
if (args.length == 1) {
new CompressNonGenericGZip().compress(args[0]);
return;
}
System.out.println("Usage: java CompressNonGenericGZip <archive>");
}
private void compress(String filename) {
RandomAccessFile raf = null;
IOutCreateArchiveGZip outArchive = null;
content = CompressArchiveStructure.create()[0].getContent();
try {
raf = new RandomAccessFile(filename, "rw");
// Open out-archive object
outArchive = SevenZip.openOutArchiveGZip();
// Configure archive
outArchive.setLevel(5);
// Create archive
outArchive.createArchive(new RandomAccessFileOutStream(raf),
1, new MyCreateCallback());
} catch (Exception e) {
System.err.println("Error occurs: " + e);
} finally {
// close resources
}
}
}
Callback 匿名类包含了若干方法,其中 setOperationResult
、setTotal
与 setCompleted
都是具体操作的钩子(hook function),而 getItemInformation
提供有关压缩包内部条目(称作 Item)的属性信息(如路径、上次访问时间等),getStream
提供对应 Item 的数据流(data)。
我们可以用多个高阶函数的组合取代匿名类。
trait AbstractArchiveCreator{
// ...
private var onProcess: (Long, Long) => Unit = (_, _) => {}
private var onEachEnd: Boolean => Unit = _ => {}
def onProcess(progressTracker: (Long, Long) => Unit): Unit = {
this.onProcess = progressTracker
}
def onEachEnd(f: Boolean => Unit): Unit = {
this.onEachEnd = f
}
// ...
}
真正进行具体操作时再由 SevenZ4S
进行装配:
trait AbstractArchiveCreator{
// ...
def compress(): Unit = {
// ...
val output: IOutStream = ???
val numEntry: Int = ???
archive.createArchive(output, numEntry, new IOutCreateCallback[TItem] {
private var total: Long = -1
override def setTotal(l: Long): Unit = this.total = l
override def setCompleted(l: Long): Unit =
if (this.total != -1) onProcess(l, this.total)
override def setOperationResult(b: Boolean): Unit = onEachEnd(b)
override def getItemInformation(
i: Int,
outItemFactory: OutItemFactory[TItem]
): TItem = {
// ...
}
override def getStream(i: Int): ISequentialInStream = {
// ...
}
})
// ...
}
}
现在我们的类可以配置 feature 与钩子函数,显然它们都是可选的,并且它们独立、不相互依赖,
配置无顺序要求。显然我们应用建造者模式(builder pattern
)组织这些方法。
这样,我们的 API 用起来就会像这样:
new ArchiveCreator7Z()
// ...
.onEachEnd {
ok =>
if (ok)
println("one success")
else
println("one failure")
}.onProcess {
(completed, total) =>
println(s"$completed of $total")
}.setLevel(5)
.setHeaderEncryption(true)
.setThreadCount(3)
.compress()
为了做到这一点,ArchiveCreator7Z
的每个方法最后都必须返回自己。但是注意,它的方法
都是在 trait AbstractArchiveCreator
里实现的。trait 里的 this 的实际类型是 trait 自己
而非子类。这会破坏子类的链式调用,因为子类拥有 trait 所没有的方法。
// AbstractArchiveCreator.scala
trait AbstractArchiveCreator{
// ...
def onProcess(progressTracker: (Long, Long) => Unit): AbstractArchiveCreator = {
this.onProcess = progressTracker
this // 类型是 AbstractArchiveCreator
}
def onEachEnd(f: Boolean => Unit): AbstractArchiveCreator = {
this.onEachEnd = f
this
}
}
// ArchiveCreator7Z.scala
final class ArchiveCreator7Z() extends AbstractArchiveCreator
with SetEncryptHeader
with SetLevel
with SetMultithreading {
// ...
}
// Main.scala
new ArchiveCreator7Z()
// ...
.onEachEnd {
ok =>
if (ok)
println("one success")
else
println("one failure")
}.onProcess {
(completed, total) =>
println(s"$completed of $total")
} // ERROR, onProcess returns AbstractArchiveCreator, which doesn't have these methods
.setLevel(5)
.setHeaderEncryption(true)
.setThreadCount(3)
.compress()
为此,子类需要把自己的类型提交给 trait,因而修改 trait 为:
trait AbstractArchiveCreator[E <: AbstractArchiveCreator[E]] {
this: E =>
protected type TEntry <: CompressionEntry
protected type TItem <: IOutItemBase
// ...
def onProcess(progressTracker: (Long, Long) => Unit): E = {
this.onProcess = progressTracker
this
}
def onEachEnd(f: Boolean => Unit): E = {
this.onEachEnd = f
this
}
// ...
}
声明处的类型参数附带约束 E <: AbstractArchiveCreator[E]
,是在提醒
开发者传入的是子类类型。同理修改 feature 的有关 trait:
trait SetEncryptHeader[T <: AbstractArchiveCreator[T] with SetEncryptHeader[T]] {
this: T =>
protected val archive: IOutFeatureSetEncryptHeader
def setHeaderEncryption(b: Boolean): T = {
archive.setHeaderEncryption(b)
this
}
}
trait SetLevel[T <: AbstractArchiveCreator[T] with SetLevel[T]] {
this: T =>
protected val archive: IOutFeatureSetLevel
def setLevel(compressionLevel: Int): T = {
archive.setLevel(compressionLevel)
this
}
}
trait SetMultithreading[T <: AbstractArchiveCreator[T] with SetMultithreading[T]] {
this: T =>
protected val archive: IOutFeatureSetMultithreading
def setThreadCount(threadCount: Int): T = {
archive.setThreadCount(threadCount)
this
}
}
子类继承时这样写:
final class ArchiveCreator7Z() extends AbstractArchiveCreator[ArchiveCreator7Z]
with SetEncryptHeader[ArchiveCreator7Z]
with SetLevel[ArchiveCreator7Z]
with SetMultithreading[ArchiveCreator7Z] {
override protected type TEntry = CompressionEntry7Z
override protected type TItem = IOutItem7z
override protected val archive: IOutCreateArchive7z = ???
然后到了最核心的部分:对于压缩包内部条目的抽象。站在 sevenzipjbind
的抽象层上看,
压缩包(archive)由通过有序列表(seq)组织的条目(item)构成,item 包括自身属性(property)
与数据(data)。sevenzipjbind
通过 getItemInformation
与 getStream
两个方法
分开处理 property 与 data,这让我感觉很割裂。所以我把内部条目重新建模为 entry,
用户使用我的 API 时需要提供同时具有 property 与 data 的 entry,SevenZ4S 会在
内部负责拆开 entry 给 getItemInformation
与 getStream
。自然,我们还需要
实现 item 与 entry 间的转化器。即:
trait AbstractAdapter[T <: CompressionEntry, R <: IOutItemBase] {
protected def adaptEntryToItem(entry: T, template: R): R
protected def adaptItemToEntry(item: R): T
}
为了避免让转化器了解 sevenzipjbind
的 item 是如何创建的,entry 至 item 的转化器
接受一个额外的 template 参数,调用者负责创建一个默认的 item 传入,这样转化器仅需
知道 item 的 setters 即可工作,简化了设计。
为了避免用户在自己构造 entry 时无从下手,我们还可以提供一些常用的辅助函数,比如, 自本地文件系统创建 entry 的函数。
最后,entry 将这样工作:
// AbstractArchiveCreator.scala
trait AbstractArchiveCreator[E <: AbstractArchiveCreator[E]] {
this: E =>
protected type TEntry <: CompressionEntry
protected type TItem <: IOutItemBase
// ...
protected def adaptItemToEntry(item: TItem): TEntry
protected def adaptEntryToItem(entry: TEntry, template: TItem): TItem
def compress(entries: IndexedSeq[TEntry]): Unit = {
// ...
val output: IOutStream = ???
val numEntry: Int = ???
archive.createArchive(output, numEntry, new IOutCreateCallback[TItem] {
// ...
override def getItemInformation(
i: Int,
outItemFactory: OutItemFactory[TItem]
): TItem = {
val templateItem = outItemFactory.createOutItem()
adaptEntryToItem(entries(i), templateItem)
}
override def getStream(i: Int): ISequentialInStream = {
entries(i).source
}
})
// ...
}
}
// ArchiveCreator7Z.scala
final class ArchiveCreator7Z() extends AbstractArchiveCreator[ArchiveCreator7Z]
with Adapter7Z
with SetEncryptHeader[ArchiveCreator7Z]
with SetLevel[ArchiveCreator7Z]
with SetMultithreading[ArchiveCreator7Z] {
// ...
}
// Adapter7Z.scala
trait Adapter7Z extends AbstractAdapter[CompressionEntry7Z, IOutItem7z] {
protected def adaptEntryToItem(entry: CompressionEntry7Z, template: IOutItem7z): IOutItem7z = {
template.setPropertyPath(entry.path)
template.setPropertyLastModificationTime(entry.lastModificationTime)
// ...
template
}
protected def adaptItemToEntry(item: IOutItem7z): CompressionEntry7Z = {
CompressionEntry7Z(
source = null, // source is not required for library generated entries
path = item.getPropertyPath,
lastModificationTime = item.getPropertyLastModificationTime,
// ...
)
}
}
// Main.scala
val path = new File(getClass.getResource("/root").getFile).toPath
val entries = SevenZ4S.get7ZEntriesFrom(path)
new ArchiveCreator7Z()
// ...
.setLevel(5)
.setHeaderEncryption(true)
.setThreadCount(3)
.compress(entries)
这里主要是以 ArchiveCreator7Z
为例说明设计,其他的类同理。
最后,SevenZ4S
的代码可以在这里找到,在这个系列的下一篇文章中,我们将把 SevenZ4S
发布到 Maven Central
中去。
推荐阅读:
Scala Mixins: The Right Way
如果你喜欢我的文章,请我吃根冰棒吧 (o゜▽゜)o ☆
最后附上 GitHub:https://github.com/gonearewe