d作者d语言中组件式编程
D编程语言中组件编程
作者:沃尔特.布莱特(Walter Bright)
我们一直在尝试编写可重用软件.作者35年的编程经验,但可重用的很少.复制/粘贴不算,我在某处可能错过了,其他程序员也经常有同样感觉.不是缺少实践,也不是现在比过去强,应该有深入理解.
有问题的样儿:抽象有漏洞,依赖其他代码,组件太具体(对A适用,但对B不适用),要回到问题:
什么是组件?
● 不仅仅是可重用软件,有很多非真正组件的可重用代码库.
● 组件有预定义接口.这样,即使单独开发的,也可交换,添加和组合他们.
● 多数库暴露接口,但连接至其他库,要求助手.
那么,
什么是通用组件接口?
● 读输入
● 处理输入
● 写输出
即使程序不是,但子系统适用该模型,伪码表示:
源 => 算法 => 汇
或这样:
源 => 1算法 => 2算法 => 汇
在联操的文件和过滤命令行模型上见过.
● 非常成功和强大
● 按"文件接口"的对接C
● 文件既是源又是汇
● 算法是"过滤器"
● 管道连接它们
● 甚至还有伪文件系统优势
缺点
● 进化而非设计
● 按字节流对待数据
● 对随机访问算法,不适用
● 但它显示了组件是什么,及分发.
看我代码
void main(string[] args)
{
string pattern = args[1];
while (!feof(stdin))
{
string line = getLine(stdin);
if (match(pattern, line))
writeLine(stdout, line);
}
}
它不像源 => 算法 =>汇.进入旋涡而非组装厂.这样更好:
void main(string[] args)
{
string pattern = args[1];
stdin => byLines => match(pattern) => stdout; //输入,按行,匹配,输出
}
下一个设计
● C++面向对象,未导致更好组件编程
● C++ iostreams,通过重载>>运算符,开始看起来像源 => 算法 =>汇,但仍然是读/写文件.
● 许多成功的C++库,但它们不是这里讨论的组件.
● 革命性的C++迭代器和算法,STL标准模板库:不仅仅是文件,算法,通用接口,编译成高效代码:
for (i = L.begin(); i != L.end(); ++i)
... 用(*i)干活 ...
仍像循环,然后有std::for_each(),std::transform(),但不能组合,因为迭代器要成对出现.
回到绘图板
● 源:流,容器,生成器
● 算法:过滤,映射,缩减,排序
● 汇:流,容器
源 | 算法 | 汇 |
|---|---|---|
file | sort | file |
tree | filter | tree |
array | map | array |
socket | reduce | socket |
list | max | list |
iota | search | |
随机数 | 奇数,数单词 |
总结要求
● 可组合性
● 支持强大封装
● 生成工业品质高效代码
● 自然语法:源=>算法=>汇
● 可同未知类型一起用
输入区间要求
● 是否有可用数据?bool为空;
● 读当前输入数据:E front;.
● 步进至下一数据:void popFront();.
输入区间非类型
● 它是概念.
● 需要3个原语:empty,front,popFront
从stdin读符:
private import core.stdc.stdio;
struct StdinByChar {//三个原语
@property bool empty() {
if (hasChar)
return false;
auto c = fgetc(stdin);
if (c == EOF)
return true;
ch = cast(char)c;
hasChar = true;
return false;
}
@property char front() { return ch; }
void popFront() { hasChar = false; }
private:
char ch;
bool hasChar;
}
从stdin读,写至stdout:
for (auto r = StdinByChar();
!r.empty;
r.popFront())
{//注意原语.
auto c = r.front; //这里
fputc(c, stdout);
}
加点语言神奇:
foreach (c; StdinByChar())
fputc(c, stdout);
//注意,这里无类型
前向区间
加了个保存属性:
@property R save;
(R为前向区间类型)
返回位置副本,而非数据.原与副本都可独立遍历区间.单链表是典型例子.归并排序用前向区间.
双向区间
加了个属性与方法:
@property E back;
void popBack();
类似front和popFront,但反向工作.双链表是典型,UTF-8和UTF-16也是双向编码.
随机访问区间
加上:
E opIndex(size_t I);
用[]来索引数据,并加了2个选项:
1,BidirectionalRange(双向区间)的@property size_t length;长度属性,可为无穷.
2,无穷的ForwardRange(前向区间).对无穷区间,空的总为假.
汇(输出区间)
有个(put)放方法:
void put(E e);
放E类型的e进区间.
输出区间写入标准输出
struct StdoutByChar {
void put(char c) {
if (fputc(c, stdout) == EOF)
throw new Exception("标出错误");
}
}
回忆早先:
foreach (c; StdinByChar())
fputc(c, stdout);
用输出区间,变成:
StdoutByChar r;
foreach (c; StdinByChar())
r.put(c);
甚至可正确处理错误!它从输入复制到输出.复制:
void copy(ref StdinByChar source,
ref StdoutByChar sink) {
foreach (c; source)
sink.put(c);
}
只要有StdinByChar和StdoutByChar类型,可复制/粘贴至其他类型:
用模板
void copy(Source, Sink)(ref Source source,
ref Sink sink) {
foreach (c; source)
sink.put(c);
}
解决了通用问题,但接受任意输入类型,而导致灾难,加上约束:
Sink copy(Source, Sink)(ref Source source,
ref Sink sink
if (isInputRange!Source &&
isOutputRange!(Sink, ElementType!Source
{
foreach (c; source)
sink.put(c);
return sink;
}
这是我们第一个算法!(返回是为了可组合.)
当前状态
StdinByChar source;
StdoutByChar sink;
copy(source, sink);
加上统调(UFCS):func(a,b,c),可写成:a.func(b,c),现在这样:
StdinByChar source;
StdoutByChar sink;
source.copy(sink);
过滤:
int[] arr = [1,2,3,4,5];
auto r = arr.filter!(a => a < 3);
writeln(r);
输出:
[1, 2]
映射:
int[] arr = [1,2,3,4,5];
auto r = arr.map!(a => a * a);
writeln(r);
输出:
[1, 4, 9, 16, 25]
化简:
int[] arr = [1,2,3,4,5];
auto r = arr.reduce!((a,b) => a + b);
writeln(r);
输出:
15
连在一起:
import std.stdio;
import std.array;
import std.algorithm;
void main() {
stdin.byLine(KeepTerminator.yes)
map!(a => a.idup).
array.
sort.
copy(stdout.lockingTextWriter());
}
还需要特征:
● 处理错误的异常
● 模板函数
● 模板约束
● UFCS(统调)
● 区间是概念,而不是类型
● 内联,自定义,优化
● 限定
● 推导类型
● 元组
结论
● 组件是可重用代码的一种方式
● 组件是需要传统和语言支持
● D的许多高级特征可结合来支持组件
● 在文件和过滤,流及stl的早期成功基础上构建.
浙公网安备 33010602011771号