Browsed by
分类:系统架构

API设计原则

API设计原则

原文链接:API Design Principles – QT Wiki
基于Gary的影响力上 Gary Gao 的译文稿:C++API设计指导

译序

Qt的设计水准在业界很有口碑,一致、易于掌握和强大的APIQt最著名的优点之一。此文既是Qt官网上的API设计指导准则,也是QtAPI设计上的实践总结。虽然Qt用的是C++,但其中设计原则和思考是具有普适性的(如果你对C++还不精通,可以忽略与C++强相关或是过于细节的部分,仍然可以学习或梳理关于API设计最有价值的内容)。整个篇幅中有很多示例,是关于API设计一篇难得的好文章。

api design

需要注意的是,这篇Wiki有一些内容并不完整,所以,可能会有一些阅读上的问题,我们对此做了一些相关的注释。

感谢酷壳博主、C++大拿 左耳朵耗子的全文审校,并对难点和注意的地方给出贴心的说明和译注。

PS:翻译中肯定会有不足和不对之处,欢迎评论&交流;另译文源码在GitHub的这个仓库中,可以提交Issue/Fork后提交代码来建议/指正。

API设计原则

一致、易于掌握和强大的APIQt最著名的优点之一。此文总结了我们在设计Qt风格API的过程中所积累的诀窍(know-how)。其中许多是通用准则;而其他的则更偏向于约定,遵循这些约定主要是为了与已有的API保持一致。

虽然这些准则主要用于对外的APIpublic API),但在设计对内的APIprivate API)时也推荐遵循相同的技巧(techniques),作为开发者之间协作的礼仪(courtesy)。

如有兴趣也可以读一下 Jasmin Blanchette 的Little Manual of API Design (PDF) 或是本文的前身 Matthias Ettrich 的Designing Qt-Style C++ APIs

1. 好API的6个特质

API之于程序员就如同图形界面之于普通用户(end-user)。API中的『P』实际上指的是『程序员』(Programmer),而不是『程序』(Program),强调的是API是给程序员使用的这一事实。

在第13期Qt季刊Matthias 的关于API设计的文章中提出了观点:API应该极简(minimal)且完备(complete)、语义清晰简单(have clear and simple semantics)、符合直觉(be intuitive)、易于记忆(be easy to memorize)和引导API使用者写出可读代码(lead to readable code)。

1.1 极简

极简的API是指每个classpublic成员尽可能少,publicclass也尽可能少。这样的API更易理解、记忆、调试和变更。

1.2 完备

完备的API是指期望有的功能都包含了。这点会和保持API极简有些冲突。如果一个成员函数放在错误的类中,那么这个函数的潜在用户就会找不到,这也是违反完备性的。

1.3 语义清晰简单

就像其他的设计一样,我们应该遵守最少意外原则(the principle of least surprise)。好的API应该可以让常见的事完成的更简单,并有可以完成不常见的事的可能性,但是却不会关注于那些不常见的事。解决的是具体问题;当没有需求时不要过度通用化解决方案。(举个例子,在Qt 3中,QMimeSourceFactory不应命名成QImageLoader并有不一样的API。)

1.4 符合直觉

就像计算机里的其他事物一样,API应该符合直观。对于什么是符合直觉的什么不符合,不同经验和背景的人对是否符合直觉有不同的看法。API符合直观的测试方法:经验不很丰富的用户不用阅读API文档就能搞懂API,而且程序员不用了解API就能看明白使用API的代码。

1.5 易于记忆

为使API易于记忆,API的命名约定应该具有一致性和精确性。使用易于识别的模式和概念,并且避免用缩写。

1.6 引导API使用者写出可读代码

代码只写一次,却要多次的阅读(还有调试和修改)。写出可读性好的代码有时候要花费更多的时间,但对于产品的整个生命周期来说是节省了时间的。

最后,要记住的是,不同的用户会使用API的不同部分。尽管简单使用单个Qt类的实例应该符合直觉,但如果是要继承一个类,让用户事先看好文档是个合理的要求。

2. 静态多态

相似的类应该有相似的API。在继承(inheritance)合适时可以用继承达到这个效果,即运行时多态。然而多态也发生在设计阶段。例如,如果你用QProgressBar替换QSlider,或是用QString替换QByteArray,你会发现API的相似性使的替换很容易。这即是所谓的『静态多态』(static polymorphism)。

静态多态也使记忆API和编程模式更加容易。因此,一组相关的类有相似的API有时候比每个类都有各自的一套API更好。

一般来说,在Qt中,如果没有足够的理由要使用继承,我们更倾向于用静态多态。这样可以减少Qt public类的个数,也使刚学习Qt的用户在翻看文档时更有方向感。

2.1 好的案例

QDialogButtonBoxQMessageBox,在处理按钮(addButton()setStandardButtons()等等)上有相似的API,不需要继承某个QAbstractButtonBox类。

2.2 差的案例

QTcpSocketQUdpSocket都继承了QAbstractSocket,这两个类的交互行为的模式(mode of interaction)非常不同。似乎没有什么人以通用和有意义的方式用过QAbstractSocket指针(或者  以通用和有意义的方式使用QAbstractSocket指针)。

2.3 值得斟酌的案例

QBoxLayoutQHBoxLayoutQVBoxLayout的父类。好处:可以在工具栏上使用QBoxLayout,调用setOrientation()使其变为水平/垂直。坏处:要多一个类,并且有可能导致用户写出这样没什么意义的代码,((QBoxLayout *)hbox)->setOrientation(Qt::Vertical)

3. 基于属性的API

新的Qt类倾向于用『基于属性(property)的API』,例如:

QTimer timer;
timer.setInterval(1000);
timer.setSingleShot(true);
timer.start();

这里的 属性 是指任何的概念特征(conceptual attribute),是对象状态的一部分 —— 无论它是不是Q_PROPERTY。在说得通的情况下,用户应该可以以任何顺序设置属性,也就是说,属性之间应该是正交的(orthogonal)。例如,上面的代码可以写成:

QTimer timer;
timer.setSingleShot(true);
timer.setInterval(1000);
timer.start();

【译注】:正交性是指改变某个特性而不会影响到其他的特性。《程序员修炼之道》中讲了关于正交性的一个直升飞机坠毁的例子,讲得深入浅出很有画面感。

为了方便,也写成:

timer.start(1000);

类似地,对于QRegExp会是这样的代码:

QRegExp regExp;
regExp.setCaseSensitive(Qt::CaseInsensitive);
regExp.setPattern(".");
regExp.setPatternSyntax(Qt::WildcardSyntax);

为实现这种类型的API,需要借助底层对象的懒创建。例如,对于QRegExp的例子,在不知道模式语法(pattern syntax)的情况下,在setPattern()中去解释"."就为时过早了。

属性之间常常有关联的;在这种情况下,我们必须小心处理。思考下面的问题:当前的风格(style)提供了『默认的图标尺寸』属性 vs. QToolButton的『iconSize』属性:

toolButton->setStyle(otherStyle);
toolButton->iconSize();    // returns the default for otherStyle
toolButton->setIconSize(QSize(52, 52));
toolButton->iconSize();    // returns (52, 52)
toolButton->setStyle(yetAnotherStyle);
toolButton->iconSize();    // returns (52, 52)

提醒一下,一旦设置了iconSize,设置就会一直保持,即使改变当前的风格。这 很好。但有的时候需要能重置属性。有两种方法:

  1. 传入一个特殊值(如QSize()-1或者Qt::Alignment(0))来表示『重置』
  2. 提供一个明确的重置方法,如resetFoo()unsetFoo()

对于iconSize,使用QSize()(比如 QSize(–1, -1))来表示『重置』就够用了。

在某些情况下,getter方法返回的结果与所设置的值不同。例如,虽然调用了widget->setEnabled(true),但如果它的父widget处于disabled状态,那么widget->isEnabled()仍然返回的是false。这样是OK的,因为一般来说就是我们想要的检查结果(父widget处于disabled状态,里面的子widget也应该变为灰的不响应用户操作,就好像子widget自身处于disabled状态一样;与此同时,因为子widget记得在自己的内心深处是enabled状态的,只是一直等待着它的父widget变为enabled)。当然诸如这些都必须在文档中妥善地说明清楚。

4. C++相关

4.1 值 vs. 对象

4.1.1 指针 vs. 引用

指针(pointer)还是引用(reference)哪个是最好的输出参数(out-parameters)?

void getHsv(int *h, int *s, int *v) const;
void getHsv(int &h, int &s, int &v) const;

大多数C++书籍推荐尽可能使用引用,基于一个普遍的观点:引用比指针『更加安全和优雅』。与此相反,我们在开发Qt时倾向于指针,因为指针让用户代码可读性更好。比较下面例子:

color.getHsv(&h, &s, &v);
color.getHsv(h, s, v);

只有第一行代码清楚表达出hsv参数在函数调用中非常有可能会被修改。

这也就是说,编译器并不喜欢『出参』,所你应该在新的API中避免使用『出参』,而是返回一个结构体,如下所示:

struct Hsv { int hue, saturation, value };
Hsv getHsv() const;

【译注】:函数的『入参』和『出参』的混用会导致 API 接口语义的混乱,所以,使用指针,在调用的时候,实参需要加上“&”,这样在代码阅读的时候,可以看到是一个『出参』,有利于代码阅读。(但是这样做,在函数内就需要判断指针是否为空的情况,因为引用是不需要判断的,所以,这是一种 trade-off)

另外,如果这样的参数过多的话,最好使用一个结构体来把数据打包,一方面,为一组返回值取个名字,另一方面,这样有利用接口的简单。

4.1.2 按常量引用传参 vs. 按值传参

如果类型大于16字节,按常量引用传参。

如果类型有重型的(non-trivial)拷贝构造函数(copy-constructor)或是重型的析构函数(destructor),按常量引用传参以避免执行这些函数。

对于其它的类型通常应该按值传参。

示例:

void setAge(int age);
void setCategory(QChar cat);
void setName(QLatin1String name);

// const-ref is much faster than running copy-constructor and destructor
void setAlarm(const QSharedPointer<Alarm> &alarm);

// QDate, QTime, QPoint, QPointF, QSize, QSizeF, QRect
// are good examples of other classes you should pass by value.

【译注】:这是传引用和传值的差别了,因为传值会有对像拷贝,传引用则不会。所以,如果对像的构造比较重的话(换句话说,就是对像里的成员变量需要的内存比较大),这就会影响很多性能。所以,为了提高性能,最好是传引用。但是如果传入引用的话,会导致这个对象可能会被改变。所以传入const reference

4.2 虚函数

C++中,当类的成员函数声明为virtual,主要是为了通过在子类重载此函数能够定制函数的行为。将函数声明为virtual的目的是为了让对这个函数已有的调用变成执行实际实例的代码路径。对于没有在类外部调用的函数声明成virtual,你应该事先非常慎重地思考过。

// QTextEdit in Qt 3: member functions that have no reason for being virtual
virtual void resetFormat();
virtual void setUndoDepth( int d );
virtual void setFormat( QTextFormat *f, int flags );
virtual void ensureCursorVisible();
virtual void placeCursor( const QPoint &pos;, QTextCursor **c = 0 );
virtual void moveCursor( CursorAction action, bool select );
virtual void doKeyboardAction( KeyboardAction action );
virtual void removeSelectedText( int selNum = 0 );
virtual void removeSelection( int selNum = 0 );
virtual void setCurrentFont( const QFont &f );
virtual void setOverwriteMode( bool b ) { overWrite = b; }

QTextEditQt 3移植到Qt 4的时候,几乎所有的虚函数都被移除了。有趣的是(但在预料之中),并没有人对此有大的抱怨,为什么?因为Qt 3没用到QTextEdit的多态行为 —— 只有你会;简单地说,没有理由去继承QTextEdit并重写这些函数,除非你自己调用了这些方法。如果在Qt在外部你的应用程序你需要多态,你可以自己添加多态。

【译注】:『多态』的目的只不过是为了实践 —— 『依赖于接口而不是实现』,也就是说,接口是代码抽像的一个非常重要的方式(在Java/Go中都有专门的接口声明语法)。所以,如果没有接口抽像,使用『多态』的意义也就不大了,因为也就没有必要使用『虚函数』了。

4.2.1 避免虚函数

Qt中,我们有很多理由尽量减少虚函数的数量。每一次对虚函数的调用会在函数调用链路中插入一个未掌控的节点(某种程度上使结果更无法预测),使得bug修复变得更复杂。用户在重写的虚函数中可以做很多疯狂的事:

  • 发送事件
  • 发送信号
  • 重新进入事件循环(例如,通过打开一个模态文件对话框)
  • 删除对象(即触发『delete this』)

还有其他很多原因要避免过度使用虚函数:

  • 添加、移动或是删除虚函数都带来二进制兼容问题(binary compatibility/BC
  • 重载虚函数并不容易
  • 编译器几乎不能优化或内联(inline)对虚函数的调用
  • 虚函数调用需要查找虚函数表(v-table),这比普通函数调用慢了2到3倍
  • 虚函数使得类很难按值拷贝(尽管也可以按值拷贝,但是非常混乱并且不建议这样做)

经验告诉我们,没有虚函数的类一般bug更少、维护成本也更低。

一般的经验法则是,除非我们以这个类作为工具集提供而且有很多用户来调用某个类的虚函数,否则这个函数九成不应该设计成虚函数。

【译注】:

  1. 使用虚函数时,你需要对编译器的内部行为非常清楚,否则,你会在使用虚函数时,觉得有好些『古怪』的问题发生。比如在创建数组对象的时候。
  2. C++中,会有一个基础类,这个基础类中已经实现好了很多功能,然后把其中的一些函数放给子类去修改和实现。这种方法在父类和子类都是一组开发人员维护时没有什么问题,但是如果这是两组开发人员,这就会带来很多问题了,就像Qt这样,子类完全无法控制,全世界的开发人员想干什么就干什么。所以,子类的代码和父类的代码在兼容上就会出现很多很多问题。所以,还是上面所说,其实,虚函数应该声明在接口的语义里(这就是设计模式的两个宗旨——依赖于接口,而不是实现;钟爱于组合,而不是继承。也是为什么Java和Go语言使用interface关键字的原因,C++在多态的语义上非常容易滥用)

4.2.2 虚函数 vs. 拷贝

多态对象(polymorphic objects)和值类型的类(value-type classes)两者很难协作好。

包含虚函数的类必须把析构函数声明为虚函数,以防止父类析构时没有清理子类的数据,导致内存泄漏。

如果要使一个类能够拷贝、赋值或按值比较,往往需要拷贝构造函数、赋值操作符(operator =)和相等操作符(operator ==)。

class CopyClass {
public:
    CopyClass();
    CopyClass(const CopyClass &other);
    ~CopyClass();
    CopyClass &operator =(const CopyClass &other);
    bool operator ==(const CopyClass &other) const;
    bool operator !=(const CopyClass &other) const;

    virtual void setValue(int v);
};

如果继承CopyClass这个类,预料之外的事就已经在代码时酝酿了。一般情况下,如果没有虚成员函数和虚析构函数,就不能创建出可以多态的子类。然而,如果存在虚成员函数和虚析构函数,这突然变成了要有子类去继承的理由,而且开始变得复杂了。起初认为只要简单声明上虚操作符重载函数(virtual operators)。 但其实是走上了一条混乱和毁灭之路(破坏了代码的可读性)。看看下面的这个例子:

class OtherClass {
public:
    const CopyClass &instance() const; // 这个方法返回的是什么?可以赋值什么?
};

(这部份还未完成)

【译注】:因为原文上说,这部份并没有完成,所以,我也没有搞懂原文具体也是想表达什么。不过,就标题而言,原文是想说,在多态的情况下拷贝对象所带来的问题??

4.3 关于const

C++的关键词const表明了内容不会改变或是没有副作用。可以应用于简单的值、指针及指针所指的内容,也可以作为一个特别的属性应用于类的成员函数上,表示成员函数不能修改对象的状态。

然而,const本身并没有提供太大的价值 —— 很多编程语言甚至没有类似const的关键词,但是却并没有因此产生问题。实际上,如果你不用函数重载,并在C++源代码用搜索并删除所有的const,几乎总能编译通过并且正常运行。尽量让使用的const保持实用有效,这点很重要。

让我们看一下在QtAPI设计中与const相关的场景。

4.3.1 输入参数:const指针

有输入指针参数的const成员函数,几乎总是const指针参数。

如果函数声明为const,意味着既没有副作用,也不会改变对象的可见状态。那为什么它需要一个没有const限定的输入参数呢?记住const类型的函数通常被其他const类型的函数调用,接收到的一般都是const指针(只要不主动const_cast,我们推荐尽量避免使用const_cast

以前:

bool QWidget::isVisibleTo(QWidget *ancestor) const;
bool QWidget::isEnabledTo(QWidget *ancestor) const;
QPoint QWidget::mapFrom(QWidget *ancestor, const QPoint &pos) const;

QWidget声明了许多非const指针输入参数的const成员函数。注意,这些函数可以修改传入的参数,不能修改对象自己。使用这样的函数常常要借助const_cast转换。如果是const指针输入参数,就可以避免这样的转换了。

之后:

bool QWidget::isVisibleTo(const QWidget *ancestor) const;
bool QWidget::isEnabledTo(const QWidget *ancestor) const;
QPoint QWidget::mapFrom(const QWidget *ancestor, const QPoint &pos) const;

注意,我们在QGraphicsItem中对此做了修正,但是QWidget要等到Qt 5:

bool isVisibleTo(const QGraphicsItem *parent) const;
QPointF mapFromItem (const QGraphicsItem *item, const QPointF &point) const;

4.3.2 返回值:const

调用函数返回的非引用类型的结果,称之为右值(R-value)。

非类(non-class)的右值总是无cv限定类型(cv-unqualified type)。虽然从语法上讲,加上const也可以,但是没什么意义,因为鉴于访问权限这些值是不能改变的。多数现代编译器在编译这样的代码时会提示警告信息。

【译注】:cv-qualified的类型(与cv-unqualified相反)是由const或者volatile或者volatile const限定的类型。详见cv (const and volatile) type qualifiers – C++语言参考

当在类类型(class type)右值上添加const关键字,则禁止访问非const成员函数以及对成员的直接操作。

不加const则没有以上的限制,但几乎没有必要加上const,因为右值对象生存时间(life time)的结束一般在C++清理的时候(通俗的说,下一个分号地方),而对右值对象的修改随着右值对象的生存时间也一起结束了(也就是本条语句的执行完成的时候)。

示例:

struct Foo {
    void setValue(int v) { value = v; }
    int value;
};

Foo foo() {
    return Foo();
}

const Foo cfoo() {
    return Foo();
}

int main() {
    // The following does compile, foo() is non-const R-value which
    // can't be assigned to (this generally requires an L-value)
    // but member access leads to a L-value:
    foo().value = 1; // Ok, but temporary will be thrown away at the end of the full-expression.

    // The following does compile, foo() is non-const R-value which
    // can't be assigned to, but calling (even non-const) member
    // function is fine:
    foo().setValue(1); // Ok, but temporary will be thrown away at the end of the full-expression.

    // The following does _not_compile, foo() is ''const'' R-value
    // with const member which member access can't be assigned to:
    cfoo().value = 1; // Not ok.

    // The following does _not_compile, foo() is ''const'' R-value,
    // one cannot call non-const member functions:
    cfoo().setValue(1); // Not ok
}

【译注】:上述的代码说明,如果返回值不是const的,代码可以顺利编译通过,然而并没有什么卵用,因为那个临时对像马上就被抛弃了。所以,这样的无用的代码最好还是在编译时报个错,以免当时头脑发热想错了,写了一段没用但还以为有用的代码。

4.3.3 返回值:非const的指针还是有const的指针

谈到const函数应该返回非const的指针还是const指针这个话题时,多数人发现在C++中关于『const正确性』(const correctness)在概念上产生了分歧。 问题起源是:const函数本身不能修改对象自身的状态,却可以返回成员的非const指针。返回指针这个简单动作本身既不会影响整个对象的可见状态,当然也不会改变这个函数职责范围内涉及的状态。但是,这却使得程序员可以间接访问并修改对象的状态。

下面的例子演示了通过返回非const指针的const函数绕开const约定(constness)的诸多方式中的一种:

QVariant CustomWidget::inputMethodQuery(Qt::InputMethodQuery query) const {
    moveBy(10, 10); // doesn't compile!
    window()->childAt(mapTo(window(), rect().center()))->moveBy(10, 10); // compiles!
}

返回const指针的函数正是保护以避免这些(可能是不期望的/没有预料到的)副作用,至少是在一定程度上。但哪个函数你会觉得更想返回const指针,或是不止一个函数?

若采用const正确(const-correct)的方法,每个返回某个成员的指针(或多个指向成员的指针)的const函数必须返回const指针。在实践中,很不幸这样的做法将导致无法使用的API

QGraphicsScene scene;
// … populate scene

foreach (const QGraphicsItem *item, scene.items()) {
    item->setPos(qrand() % 500, qrand() % 500); // doesn't compile! item is a const pointer
}

QGraphicsScene::items()是一个const函数,顺着思考看起来这个函数只应该返回const指针。

Qt中,我们几乎只有非const的使用模式。我们选择的是实用路子: 相比滥用非const指针返回类型带来的问题,返回const指针更可能招致过分使用const_cast的问题。

4.3.4 返回值:按值返回 还是 按const引用返回?

若返回的是对象的拷贝,那么返回const引用是更直接的方案; 然而,这样的做法限制了后面想要对这个类的重构(refactor)。 (以d-point的典型做法(idiom)为例,我们可以在任何时候改变Qt类在内存表示(memory representation);但却不能在不破坏二进制兼容性的情况下把改变函数的签名,返回值从const QFoo &变为QFoo。) 基于这个原因,除去对运行速度敏感(speed is critical)而重构不是问题的个别情形(例如,QList::at()),我们一般返回QFoo而不是const QFoo &

【译注】:参看《Effective C++》中条款23:Don’t try to return a reference when you must return an object

4.4.5 const vs. 对象的状态

const正确性(Const correctness)的问题就像C圈子中viemacs的讨论,因为这个话题在很多地方都存在分歧(比如基于指针的函数)。

但通用准则是const函数不能改变类的可见状态。『状态』的意思是『自身以及涉及的职责』。这并不是指非const函数能够改变自身的私有成员,也不是指const函数改变不了。而是指函数是活跃的并存在可见的副作用(visible side effects)。const函数一般没有任何可见的副作用,比如:

QSize size = widget->sizeHint(); // const
widget->move(10, 10); // not const

代理(delegate)负责在其它对象上绘制内容。 它的状态包括它的职责,因此包括在哪个对象做绘制这样的状态。 调用它的绘画行为必然会有副作用; 它改变了它绘制所在设备的外观(及其所关联的状态)。鉴于这些,paint()作为const函数并不合理。 进一步说,任何paint()QIconpaint()的视图函数是const函数也不合理。 没有人会从内部的const函数去调用QIcon::paint(),除非他想显式地绕开const这个特性。 如果是这种情况,使用const_cast会更好。

// QAbstractItemDelegate::paint is const
void QAbstractItemDelegate::paint(QPainter **painter, const QStyleOptionViewItem &option, const QModelIndex &index) const

// QGraphicsItem::paint is not const
void QGraphicsItem::paint(QPainter *painter, const QStyleOptionGraphicsItem option, QWidget *widget)

const关键字并不能按你期望的样子起作用。应该考虑将其移除而不是去重载const/非const函数。

5. API的语义和文档

当传值为-1的参数给函数,函数会是什么行为?有很多类似的问题……

是警告、致命错误还是其它?

API需要的是质量保证。API第一个版本一定是不对的;必须对其进行测试。 以阅读使用API的代码的方式编写用例,且验证这样代码是可读的。

还有其他的验证方法,比如

  • 让别人使用API(看了文档或是先不看文档都可以)
  • 给类写文档(包含类的概述和每个函数)

6. 命名的艺术

命名很可能是API设计中最重要的一个问题。类应该叫什么名字?成员函数应该叫什么名字?

6.1 通用的命名规则

有几个规则对于所有类型的命名都等同适用。第一个,之前已经提到过,不要使用缩写。即使是明显的缩写,比如把previous缩写成prev,从长远来看是回报是负的,因为用户必须要记住缩写词的含义。

如果API本身没有一致性,之后事情自然就会越来越糟;例如,Qt 3 中同时存在activatePreviousWindow()fetchPrev()。恪守『不缩写』规则更容易地创建一致性的API

另一个时重要但更微妙的准则是在设计类时应该保持子类名称空间的干净。在Qt 3中,此项准则并没有一直遵循。以QToolButton为例对此进行说明。如果调用QToolButton的 name()caption()text()或者textLabel(),你觉得会返回什么?用Qt设计器在QToolButton上自己先试试吧:

  • name属性是继承自QObject,返回内部的对象名称,用于调试和测试。
  • caption属性继承自QWidget,返回窗口标题,对QToolButton来说毫无意义,因为它在创建的时候parent就存在了。
  • text函数继承自QButton,一般用于按钮。当useTextLabel不为true,才用这个属性。
  • textLabel属性在QToolButton内声明,当useTextLabeltrue时显示在按钮上。

为了可读性,在Qt 4QToolButtonname属性改成了objectNamecaption改成了windowTitle,删除了textLabel属性因为和text属性相同。

当你找不到好的命名时,写文档也是个很好方法:要做的就是尝试为各个条目(item)(如类、方法、枚举值等等)写文档,并用写下的第一句话作为启发。如果找不到一个确切的命名,往往说明这个条目是不该有的。如果所有尝试都失败了,并且你坚信这个概念是合理的,那么就发明一个新名字。像widgeteventfocusbuddy这些命名就是在这一步诞生的。

【译注】:写文档是一个非常好的习惯。写文档的过程其实就是在帮你梳理你的编程思路。很多时候,文档写着写着你就会发现要去改代码去了。除了上述的好处多,写文档还有更多的好处。比如,在写文档的过程中,你发现文字描述过于复杂了,这表明着你的代码或逻辑是复杂的,这就倒逼你去重构你的代码。所以 —— 写文档其实就是写代码

6.2 类的命名

识别出类所在的分组,而不是为每个类都去找个完美的命名。例如,所有Qt 4的能感知模型(model-aware)的item view,类后缀都是ViewQListViewQTableViewQTreeView),而相应的基于itemitem-based)的类后缀是WidgetQListWidgetQTableWidgetQTreeWidget)。

6.3 枚举类型及其值的命名

声明枚举类型时,需要记住在C++中枚举值在使用时不会带上类型(与JavaC#不同)。下面的例子演示了枚举值命名得过于通用的危害:

namespace Qt
{
    enum Corner { TopLeft, BottomRight, ... };
    enum CaseSensitivity { Insensitive, Sensitive };
    ...
};

tabWidget->setCornerWidget(widget, Qt::TopLeft);
str.indexOf("$(QTDIR)", Qt::Insensitive);

在最后一行,Insensitive是什么意思?命名枚举类型的一个准则是在枚举值中至少重复此枚举类型名中的一个元素:

namespace Qt
{
    enum Corner { TopLeftCorner, BottomRightCorner, ... };
    enum CaseSensitivity { CaseInsensitive, CaseSensitive };
    ...
};

tabWidget->setCornerWidget(widget, Qt::TopLeftCorner);
str.indexOf("$(QTDIR)", Qt::CaseInsensitive);

当对枚举值进行或运算并作为某种标志(flag)时,传统的做法是把或运算的结果保存在int型的值中,但这不是类型安全的。Qt 4提供了一个模板类QFlags<T>,其中的T是枚举类型。为了方便使用,Qttypedef重新定义了QFlag类型,所以可以用Qt::Alignment代替QFlags<Qt::AlignmentFlag>

习惯上,枚举类型命名用单数形式(因为它一次只能『持有』一个flag),而持有多个『flag』的类型用复数形式,例如:

enum RectangleEdge { LeftEdge, RightEdge, ... };
typedef QFlags<RectangleEdge> RectangleEdges;

在某些情形下,持有多个『flag』的类型命名用单数形式。对于这种情况,持有的枚举类型名称要求是以Flag为后缀:

enum AlignmentFlag { AlignLeft, AlignTop, ... };
typedef QFlags<AlignmentFlag> Alignment;

6.4 函数和参数的命名

函数命名的第一准则是可以从函数名看出来此函数是否有副作用。在Qt 3中,const函数QString::simplifyWhiteSpace()违反了此准则,因为它返回了一个QString而不是按名称暗示的那样,改变调用它的QString对象。在Qt 4中,此函数重命名为QString::simplified()

虽然参数名不会出现在使用API的代码中,但是它们给程序员提供了重要信息。因为现代的IDE都会在写代码时显示参数名称,所以值得在头文件中给参数起一个恰当的名字并在文档中使用相同的名字。

6.5 布尔类型的gettersetter方法的命名

bool属性的gettersetter方法命名总是很痛苦。getter应该叫做checked()还是isChecked()scrollBarsEnabled()还是areScrollBarEnabled()

Qt 4中,我们套用以下准则为getter命名:

  • 形容词以is为前缀,例子:
    • isChecked()
    • isDown()
    • isEmpty()
    • isMovingEnabled()
  • 然而,修饰名词的形容词没有前缀:
    • scrollBarsEnabled(),而不是areScrollBarsEnabled()
  • 动词没有前缀,也不使用第三人称(-s):
    • acceptDrops(),而不是acceptsDrops()
    • allColumnsShowFocus()
  • 名词一般没有前缀:
    • autoCompletion(),而不是isAutoCompletion()
    • boundaryChecking()
  • 有的时候,没有前缀容易产生误导,这种情况下会加上is前缀:
    • isOpenGLAvailable(),而不是openGL()
    • isDialog(),而不是dialog()
      (一个叫做dialog()的函数,一般会被认为是返回QDialog。)

setter的名字由getter衍生,去掉了前缀后在前面加上了set;例如,setDown()setScrollBarsEnabled()

7. 避免常见陷阱

7.1 简化的陷阱

一个常见的误解是:实现需要写的代码越少,API就设计得越好。应该记住:代码只会写上几次,却要被反复阅读并理解。例如:

QSlider *slider = new QSlider(12, 18, 3, 13, Qt::Vertical, 0, "volume");

这段代码比下面的读起来要难得多(甚至写起来也更难):

QSlider *slider = new QSlider(Qt::Vertical);
slider->setRange(12, 18);
slider->setPageStep(3);
slider->setValue(13);
slider->setObjectName("volume");

【译注】:在有IDE的自动提示的支持下,后者写起来非常方便,而前者还需要看相应的文档。

7.2 布尔参数的陷阱

布尔类型的参数总是带来无法阅读的代码。给现有的函数增加一个bool型的参数几乎永远是一种错误的行为。仍以Qt为例,repaint()有一个bool类型的可选参数用于指定背景是否被擦除。可以写出这样的代码:

widget->repaint(false);

初学者很可能是这样理解的,『不要重新绘制!』,能有多少Qt用户真心知道下面3行是什么意思:

widget->repaint();
widget->repaint(true);
widget->repaint(false);

更好的API设计应该是这样的:

widget->repaint();
widget->repaintWithoutErasing();

Qt 4中,我们通过移除了重新绘制(repaint)而不擦除widget的能力来解决了此问题。Qt 4的双缓冲使这种特性被废弃。

还有更多的例子:

widget->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding, true);
textEdit->insert("Where's Waldo?", true, true, false);
QRegExp rx("moc_***.c??", false, true);

一个明显的解决方案是bool类型改成枚举类型。我们在Qt 4QString中就是这么做的。对比效果如下:

str.replace("%USER%", user, false);               // Qt 3
str.replace("%USER%", user, Qt::CaseInsensitive); // Qt 4

8. 案例研究

8.1 QProgressBar

为了展示上文各种准则的实际应用。我们来学习一下Qt 3QProgressBarAPI,并与Qt 4中对应的API作比较。

Qt 3中:

class QProgressBar : public QWidget
{
    ...
public:
    int totalSteps() const;
    int progress() const;

    const QString &progressString() const;
    bool percentageVisible() const;
    void setPercentageVisible(bool);

    void setCenterIndicator(bool on);
    bool centerIndicator() const;

    void setIndicatorFollowsStyle(bool);
    bool indicatorFollowsStyle() const;

public slots:
    void reset();
    virtual void setTotalSteps(int totalSteps);
    virtual void setProgress(int progress);
    void setProgress(int progress, int totalSteps);

protected:
    virtual bool setIndicator(QString &progressStr,
                              int progress,
                              int totalSteps);
    ...
};

API相当的复杂和不一致;例如,reset()setTotalSteps()setProgress()是紧密联系的但方法的命名并没明确地表达出来。

改善此API的关键是抓住QProgressBarQt 4QAbstractSpinBox及其子类QSpinBoxQSliderQDail之间的相似性。怎么做?把progresstotalSteps替换为minimummaximumvalue。增加一个valueChanged()消息,再增加一个setRange()函数。

进一步可以观察到progressStringpercentageindicator其实是一回事,即是显示在进度条上的文本。通常这个文本是个百分比,但是可通过setIndicator()设置为任何内容。以下是新的API

virtual QString text() const;
void setTextVisible(bool visible);
bool isTextVisible() const;

默认情况下,显示文本是百分比指示器(percentage indicator),重写text()方法来定制行为。

Qt 3setCenterIndicator()setIndicatorFollowsStyle()是两个影响对齐方式的函数。他们可被一个setAlignment()函数代替:

void setAlignment(Qt::Alignment alignment);

如果开发者未调用setAlignment(),那么对齐方式由风格决定。对于基于Motif的风格,文字内容在中间显示;对于其他风格,在右侧显示。

下面是改善后的QProgressBar API:

class QProgressBar : public QWidget
{
    ...
public:
    void setMinimum(int minimum);
    int minimum() const;
    void setMaximum(int maximum);
    int maximum() const;
    void setRange(int minimum, int maximum);
    int value() const;

    virtual QString text() const;
    void setTextVisible(bool visible);
    bool isTextVisible() const;
    Qt::Alignment alignment() const;
    void setAlignment(Qt::Alignment alignment);

public slots:
    void reset();
    void setValue(int value);

signals:
    void valueChanged(int value);
    ...
};

8.2 QAbstractPrintDialog & QAbstractPageSizeDialog

Qt 4.0有2个幽灵类QAbstractPrintDialogQAbstractPageSizeDialog,作为 QPrintDialogQPageSizeDialog类的父类。这2个类完全没有用,因为QTAPI没有是QAbstractPrint-或是-PageSizeDialog指针作为参数并执行操作。通过篡改qdocQT文档),我们虽然把这2个类隐藏起来了,却成了无用抽象类的典型案例。

这不是说, 的抽象是错的,QPrintDialog应该是需要有个工厂或是其它改变的机制 —— 证据就是它声明中的#ifdef QTOPIA_PRINTDIALOG

8.3 QAbstractItemModel

关于模型/视图(model/view)问题的细节在对应的文档中已经说明得很好了,但需要强调的一个重要的总结是:抽象类不应该仅是所有可能子类的并集(union)。这样『合并所有』的父类几乎不可能是一个好的方案。QAbstractItemModel就犯了这个错误 —— 它实际上就是个QTreeOfTablesModel,结果就导致了一个错综复杂(complicated)的API,而这样的API要让 所有本来设计还不错的子类 去继承。

仅仅增加抽象是不会自动就把API变得更好的。

8.4 QLayoutIterator & QGLayoutIterator

QT 3,创建自定义的布局类需要同时继承QLayoutQGLayoutIterator(命名中的G是指Generic(通用))。QGLayoutIterator子类的实例指针会包装成QLayoutIterator,这样用户可以像和其它的迭代器(iterator)类一样的方式来使用。通过QLayoutIterator可以写出下面这样的代码:

QLayoutIterator it = layout()->iterator();
QLayoutItem **child;
while ((child = it.current()) != 0) {
    if (child->widget() == myWidget) {
        it.takeCurrent();
        return;
    }
    ++it;
}

QT 4,我们干掉了QGLayoutIterator类(以及用于盒子布局和格子布局的内部子类),转而是让QLayout的子类重写itemAt()takeAt()count()

8.5 QImageSink

Qt 3有一整套类用来把完成增量加载的图片传递给一个动画 —— QImageSource/Sink/QASyncIO/QASyncImageIO。由于这些类之前只是用于启用动画的QLabel,完全过度设计了(overkill)。

从中得到的教训就是:对于那些未来可能的还不明朗的需求,不要过早地增加抽象设计。当需求真的出现时,比起一个复杂的系统,在简单的系统新增需求要容易得多。

好烂啊有点差凑合看看还不错很精彩 (没人打分)
Loading...
从Gitlab误删除数据库想到的

从Gitlab误删除数据库想到的

昨天,Gitlab.com发生了一个大事,某同学误删了数据库,这个事看似是个低级错误,不过,因为Gitlab把整个过程的细节都全部暴露出来了,所以,可以看到很多东西,而对于类似这样的事情,我自己以前也干过,而在最近的两公司中我也见过(Amazon中见过一次,阿里中见过至少四次),正好通过这个事来说说一下自己的一些感想和观点吧。我先放个观点:你觉得有备份系统就不会丢数据了吗?

事件回顾

整个事件的回顾Gitlab.com在第一时间就放到了Google Doc上,事后,又发了一篇Blog来说明这个事,在这里,我简单的回顾一下这个事件的过程。

首先,一个叫YP的同学在给gitlab的线上数据库做一些负载均衡的工作,在做这个工作时的时候突发了一个情况,Gitlab被DDoS攻击,数据库的使用飙高,在block完攻击者的IP后,发现有个staging的数据库(db2.staging)已经落后生产库4GB的数据,于是YP同学在Fix这个staging库的同步问题的时候,发现db2.staging有各种问题都和主库无法同步,在这个时候,YP同学已经工作的很晚了,在尝试过多个方法后,发现db2.staging都hang在那里,无法同步,于是他想把db2.staging的数据库删除了,这样全新启动一个新的复制,结果呢,删除数据库的命令错误的敲在了生产环境上(db1.cluster),结果导致整个生产数据库被误删除。(陈皓注:这个失败基本上就是 “工作时间过长” + “在多数终端窗口中切换中迷失掉了”

阅读全文 Read More

好烂啊有点差凑合看看还不错很精彩 (71 人打了分,平均分: 4.83 )
Loading...
关于高可用的系统

关于高可用的系统

HighAvailability-BK在《这多年来我一直在钻研的技术》这篇文章中,我讲述了一下,我这么多年来一直在关注的技术领域,其中我多次提到了工业级的软件,我还以为有很多人会问我怎么定义工业级?以及一个高可用性的软件系统应该要怎么干出来?这样我也可以顺理成章的写下这篇文章,但是没有人问,那么,我只好厚颜无耻的自己写下这篇文章了。哈哈。

另外,我在一些讨论高可用系统的地方看到大家只讨论各个公司的技术方案,其实,高可用的系统并不简单的是技术方案,一个高可用的系统其实还包括很多别的东西,所以,我觉得大家对高可用的系统了解的还不全面,为了让大家的认识更全面,所以,我写下这篇文章

理解高可用系统

首先,我们需要理解什么是高可用,英文叫High Availability(Wikipedia词条),基本上来说,就是要让我们的计算环境(包括软硬件)做到full-time的可用性。在设计上一般来说,需要做好如下的设计:

阅读全文 Read More

好烂啊有点差凑合看看还不错很精彩 (37 人打了分,平均分: 4.95 )
Loading...
分布式系统的事务处理

分布式系统的事务处理

当我们在生产线上用一台服务器来提供数据服务的时候,我会遇到如下的两个问题:

1)一台服务器的性能不足以提供足够的能力服务于所有的网络请求。

2)我们总是害怕我们的这台服务器停机,造成服务不可用或是数据丢失。

于是我们不得不对我们的服务器进行扩展,加入更多的机器来分担性能上的问题,以及来解决单点故障问题。 通常,我们会通过两种手段来扩展我们的数据服务:

1)数据分区:就是把数据分块放在不同的服务器上(如:uid % 16,一致性哈希等)。

2)数据镜像:让所有的服务器都有相同的数据,提供相当的服务。

对于第一种情况,我们无法解决数据丢失的问题,单台服务器出问题时,会有部分数据丢失。所以,数据服务的高可用性只能通过第二种方法来完成——数据的冗余存储(一般工业界认为比较安全的备份数应该是3份,如:Hadoop和Dynamo)。 但是,加入更多的机器,会让我们的数据服务变得很复杂,尤其是跨服务器的事务处理,也就是跨服务器的数据一致性。这个是一个很难的问题。 让我们用最经典的Use Case:“A帐号向B帐号汇钱”来说明一下,熟悉RDBMS事务的都知道从帐号A到帐号B需要6个操作:

  1. 从A帐号中把余额读出来。
  2. 对A帐号做减法操作。
  3. 把结果写回A帐号中。
  4. 从B帐号中把余额读出来。
  5. 对B帐号做加法操作。
  6. 把结果写回B帐号中。

为了数据的一致性,这6件事,要么都成功做完,要么都不成功,而且这个操作的过程中,对A、B帐号的其它访问必需锁死,所谓锁死就是要排除其它的读写操作,不然会有脏数据的问题,这就是事务。那么,我们在加入了更多的机器后,这个事情会变得复杂起来:

阅读全文 Read More

好烂啊有点差凑合看看还不错很精彩 (62 人打了分,平均分: 4.79 )
Loading...
7个示例科普CPU Cache

7个示例科普CPU Cache

(感谢网友 @我的上铺叫路遥 翻译投稿)

CPU cache一直是理解计算机体系架构的重要知识点,也是并发编程设计中的技术难点,而且相关参考资料如同过江之鲫,浩瀚繁星,阅之如临深渊,味同嚼蜡,三言两语难以入门。正好网上有人推荐了微软大牛Igor Ostrovsky一篇博文《漫游处理器缓存效应》,文章不仅仅用7个最简单的源码示例就将CPU cache的原理娓娓道来,还附加图表量化分析做数学上的佐证,个人感觉这种案例教学的切入方式绝对是俺的菜,故而忍不住贸然译之,以飨列位看官。

原文地址:Gallery of Processor Cache Effects

大多数读者都知道cache是一种快速小型的内存,用以存储最近访问内存位置。这种描述合理而准确,但是更多地了解一些处理器缓存工作中的“烦人”细节对于理解程序运行性能有很大帮助。

在这篇博客中,我将运用代码示例来详解cache工作的方方面面,以及对现实世界中程序运行产生的影响。

下面的例子都是用C#写的,但语言的选择同程序运行状况以及得出的结论几乎没什么影响。

示例1:内存访问和运行

你认为相较于循环1,循环2会运行多快?

int[] arr = new int[64 * 1024 * 1024];

// Loop 1
for (int i = 0; i < arr.Length; i++) arr[i] *= 3;

// Loop 2
for (int i = 0; i < arr.Length; i += 16) arr[i] *= 3;

阅读全文 Read More

好烂啊有点差凑合看看还不错很精彩 (36 人打了分,平均分: 4.31 )
Loading...
IoC/DIP其实是一种管理思想

IoC/DIP其实是一种管理思想

关于IoC的的概念提出来已经很多年了,其被用于一种面象对像的设计。我在这里再简单的回顾一下这个概念。我先谈技术,再说管理。

话说,我们有一个开关要控制一个灯的开和关这两个动作,最常见也是最没有技术含量的实现会是这个样子:

然后,有一天,我们发现需要对灯泡扩展一下,于是我们做了个抽象类:

但是,如果有一天,我们发现这个开关可能还要控制别的不单单是灯泡的东西,我们就发现这个开关耦合了灯泡这种类别,非常不利于我们的扩展,于是反转控制出现了。

就像现实世界一样,造开关的工厂根本不关心要控制的东西是什么,它只做一个开关应该做好的事,就是把电接通,把电断开(不管是手动的,还是声控的,还是光控,还是遥控的),而我们的造各种各样的灯泡(不管是日关灯,白炽灯)的工厂也不关心你用什么样的开关,反正我只管把灯的电源接口给做出来,然后,开关厂和电灯厂依赖于一个标准的通电和断电的接口。于是产生了IoC控制反转,如下图:

阅读全文 Read More

好烂啊有点差凑合看看还不错很精彩 (22 人打了分,平均分: 4.59 )
Loading...
并发框架Disruptor译文

并发框架Disruptor译文

(感谢同事方腾飞投递本文)

Martin Fowler在自己网站上写了一篇LMAX架构的文章,在文章中他介绍了LMAX是一种新型零售金融交易平台,它能够以很低的延迟产生大量交易。这个系统是建立在JVM平台上,其核心是一个业务逻辑处理器,它能够在一个线程里每秒处理6百万订单。业务逻辑处理器完全是运行在内存中,使用事件源驱动方式。业务逻辑处理器的核心是Disruptor。

Disruptor它是一个开源的并发框架,并获得2011 Duke’s 程序框架创新奖,能够在无锁的情况下实现网络的Queue并发操作。本文是Disruptor官网中发布的文章的译文(现在被移到了GitHub)。

剖析Disruptor:为什么会这么快

Disruptor如何工作和使用

阅读全文 Read More

好烂啊有点差凑合看看还不错很精彩 (21 人打了分,平均分: 4.48 )
Loading...
性能调优攻略

性能调优攻略

关于性能优化这是一个比较大的话题,在《由12306.cn谈谈网站性能技术》中我从业务和设计上说过一些可用的技术以及那些技术的优缺点,今天,想从一些技术细节上谈谈性能优化,主要是一些代码级别的技术和方法。本文的东西是我的一些经验和知识,并不一定全对,希望大家指正和补充

在开始这篇文章之前,大家可以移步去看一下酷壳以前发表的《代码优化概要》,这篇文章基本上告诉你——要进行优化,先得找到性能瓶颈! 但是在讲如何定位系统性能瓶劲之前,请让我讲一下系统性能的定义和测试,因为没有这两件事,后面的定位和优化无从谈起。

一、系统性能定义

让我们先来说说如何什么是系统性能。这个定义非常关键,如果我们不清楚什么是系统性能,那么我们将无法定位之。我见过很多朋友会觉得这很容易,但是仔细一问,其实他们并没有一个比较系统的方法,所以,在这里我想告诉大家如何系统地来定位性能。 总体来说,系统性能就是两个事:

  1. Throughput ,吞吐量。也就是每秒钟可以处理的请求数,任务数。
  2. Latency, 系统延迟。也就是系统在处理一个请求或一个任务时的延迟。

一般来说,一个系统的性能受到这两个条件的约束,缺一不可。比如,我的系统可以顶得住一百万的并发,但是系统的延迟是2分钟以上,那么,这个一百万的负载毫无意义。系统延迟很短,但是吞吐量很低,同样没有意义。所以,一个好的系统的性能测试必然受到这两个条件的同时作用。 有经验的朋友一定知道,这两个东西的一些关系:

  • Throughput越大,Latency会越差。因为请求量过大,系统太繁忙,所以响应速度自然会低。
  • Latency越好,能支持的Throughput就会越高。因为Latency短说明处理速度快,于是就可以处理更多的请求。

二、系统性能测试

经过上述的说明,我们知道要测试系统的性能,需要我们收集系统的Throughput和Latency这两个值。

阅读全文 Read More

好烂啊有点差凑合看看还不错很精彩 (19 人打了分,平均分: 5.00 )
Loading...
需求变化与IoC

需求变化与IoC

感谢 Todd投递本文 – 微博帐号:@weidagang

需求又变了,怎么办?

先上一个轻松的段子:

程序员XX遭遇车祸成植物人,医生说活下来的希望只有万分之一,唤醒更为渺茫。可他的Lead和亲人没有放弃,他们根据XX工作如命的作风,每天都在他身边念:“XX,需求又改了,该干活了,你快来呀!”,奇迹终于发生了,XX醒来了,第一句话:“需求又改了?”。

这个段子用幽默的方式反映了需求变化是每一个程序员、架构师或项目经理都会经常遇到的问题。面对这个问题,不同的人有不同的应对之道,最近微博上有一段关于需求变化的讨论:

@假装刺猬的猪:我们在软件开发过程中,会持续碰到客户需求变更的情况。如果没有领域建模,我们单纯将问题使用直觉将问题解决,那么等到客户需求变更或者有新的需求时,就会面临一个僵硬的前设计!无法在以前的设计上持续深入的优化模型,导致需求变更无法及时深化。设计实现均滞后与变更!

@高煥堂: <碰到客户需求变更的情况>是合理的;但<领域建模>不是美好的手段!!!

@weidagang: 要不被客户牵着鼻子走,需要自己有很强的设计能力,反过来让客户跟着你的设计来满足你的要求。能做到这点的公司很少,但这是软件行业唯一有希望的出路。

@高煥堂: <这是软件行业唯一有希望的出路>。 Great!!

如何应对需求变化? @假装刺猬的猪 的答案是领域建模,并持续优化模型,适应需求的变化。@高煥堂 则认为领域建模不是美好的手段。我进一步补充,应该“反过来”让自己在需求变化中处于主导地位,而不是被动地适应。

阅读全文 Read More

好烂啊有点差凑合看看还不错很精彩 (17 人打了分,平均分: 4.76 )
Loading...
多版本并发控制(MVCC)在分布式系统中的应用

多版本并发控制(MVCC)在分布式系统中的应用

感谢 Todd投递本文 – 微博帐号:weidagang

问题

最近项目中遇到了一个分布式系统的并发控制问题。该问题可以抽象为:某分布式系统由一个数据中心D和若干业务处理中心L1,L2 … Ln组成;D本质上是一个key-value存储,它对外提供基于HTTP协议的CRUD操作接口。L的业务逻辑可以抽象为下面3个步骤:

  1. read: 根据keySet {k1, … kn}从D获取keyValueSet {k1:v1, … kn:vn}
  2. do: 根据keyValueSet进行业务处理,得到需要更新的数据集keyValueSet’ {k1′:v1′, … km’:vm’} (:读取的keySet和更新的keySet’可能不同)
  3. update: 把keyValueSet’更新到D (:D保证在一次调用更新多个key的原子性)

在没有事务支持的情况下,多个L进行并发处理可能会导致数据一致性问题。比如,考虑L1和L2的如下执行顺序:

  1. L1从D读取key:123对应的值100
  2. L2从D读取key:123对应的100
  3. L1将key:123更新为100 + 1
  4. L2将key:123更新为100 + 2

如果L1和L2串行执行,key:123对应的值将为103,但上面并发执行中L1的执行效果完全被L2所覆盖,实际key:123所对应的值变成了102。

阅读全文 Read More

好烂啊有点差凑合看看还不错很精彩 (17 人打了分,平均分: 4.24 )
Loading...
由12306.cn谈谈网站性能技术

由12306.cn谈谈网站性能技术

12306.cn网站挂了,被全国人民骂了。我这两天也在思考这个事,我想以这个事来粗略地和大家讨论一下网站性能的问题。因为仓促,而且完全基于本人有限的经验和了解,所以,如果有什么问题还请大家一起讨论和指正。(这又是一篇长文,只讨论性能问题,不讨论那些UI,用户体验,或是是否把支付和购票下单环节分开的功能性的东西)

业务

任何技术都离不开业务需求,所以,要说明性能问题,首先还是想先说说业务问题。

  • 其一有人可能把这个东西和QQ或是网游相比。但我觉得这两者是不一样的,网游和QQ在线或是登录时访问的更多的是用户自己的数据,而订票系统访问的是中心的票量数据,这是不一样的。不要觉得网游或是QQ能行你就以为这是一样的。网游和QQ 的后端负载相对于电子商务的系统还是简单。
  • 其二有人说春节期间订火车的这个事好像网站的秒杀活动。的确很相似,但是如果你的思考不在表面的话,你会发现这也有些不一样。火车票这个事,一方面会伴随着大量的查询操作,更BT的是下单的时候需要对数据库很多的一致性的操作,一方面是从起点到终点各个分段票的一致性,另一方面,买的人路线、车次、时间选择有很多,会不停地改变下单方式。而秒杀,直接杀就好了,没有那么多查询和一致性的问题。另外,关于秒杀,完全可以做成只接受前N个用户的请求(完全不操作后端的任何数据, 仅仅只是对用户的下单操作log),这种业务,只需要在内存cache中放好可秒杀的数量,还可以把数据分布开来放,100商品,10台服务器一台放10个,无需在当时操作任何数据库。可以订单数够后,停止秒杀,然后批量写数据库。而且秒杀的商品不多。火车票这个不是像秒杀那么简单的,春运时间,几乎所有的票都是热门票,而且几乎是全国人民都来了,而且还有转车业务,多条线的库存都要做事务操作,你想想吧,这有多难。(淘宝的双十一也就3百万用户,而火车票瞬时有千万级别甚至是亿级别的)(更新:2014年1月11日:来了淘宝后,对淘宝的系统有了解,淘宝的秒杀活动,本质上是用输验证码并在CDN上把用户直接过滤掉了,比如:1千万个用户过滤了只剩2万个用户,这样数据库就顶得住了)
  • 其三有人拿这个系统和奥运会的票务系统比较。我觉得还是不一样。虽然奥运会的票务系统当年也一上线就废了。但是奥运会用的是抽奖的方式,也就是说不存在先来先得的抢的方式,而且,是事后抽奖,事前只需要收信息,事前不需要保证数据一致性,没有锁,很容易水平扩展。
  • 其四订票系统应该和电子商务的订单系统很相似,都是需要对库存进行:1)占住库存,2)支付(可选),3)扣除库存的操作。这个是需要有一致性的检查的,也就是在并发时需要对数据加锁的。B2C的电商基本上都会把这个事干成异步的,也就是说,你下的订单并不是马上处理的,而是延时处理的,只有成功处理了,系统才会给你一封确认邮件说是订单成功。我相信有很多朋友都收到认单不成功的邮件。这就是说,数据一致性在并发下是一个瓶颈

阅读全文 Read More

好烂啊有点差凑合看看还不错很精彩 (87 人打了分,平均分: 4.84 )
Loading...