返回信息流最近整个人都不好了。吐槽一下:
桥接模式是什么样的?
比如我们要实现一个“人"类。这里用C++语言。
// Person.h
class Person {
char name[100];
int age;
public:
Person();
char *getName();
int getAge();
void setName(char *n);
void setAge(int a);
};
// person.cpp
#include "person.h"
#include <cstring>
Person::Person() {
std::memset(name, 0, sizeof(name));
age = 0;
}
char *Person::getName() {
return name;
}
int Person::getAge() {
return age;
}
void Person::setName(char *n) {
std::strcpy(name, n);
}
void Person::setAge(int a) {
age = a;
}
// main.cpp
#include "person.h"
#include <iostream>
int main() {
Person p;
p.setName("John");
p.setAge(42);
std::cout<<p.getName()<<" "<<p.getAge()<<std::endl;
return 0;
}
编译方法:g++ -o main main.cpp person.cpp
运行:
$ ./main
John 42
上述代码有什么问题?
二进制兼容性。注意到上述编译方法,是把person和main编译到一个可执行文件里的。
但是,如果Person类的开发人员把person.h和person.cpp编译成动态库,而用户使用main.cpp,那么情况就不太一样了。
编译方法:
g++ -fPIC -shared -o libperson.so person.cpp
g++ -o main -L . -lperson main.cpp
运行:
$ LD_LIBRARY_PATH=. ./main # 这里设置LD_LIBRARY_PATH环境变量,让装载器从当前目录找libperson.so。
John 42
但是,万一Person类的作者突发奇想,给这个对象添加了一个域“title”(头衔)。于是他改了person.h和person.cpp
// 新的Person.h
class Person {
char title[100];
char name[100];
int age;
public:
Person();
char *getName();
int getAge();
void setName(char *n);
void setAge(int a);
};
#include "person.h"
#include <cstring>
Person::Person() {
std::memset(title, 0, sizeof(title)); // 多了一行
std::memset(name, 0, sizeof(name));
age = 0;
}
char *Person::getName() {
return name;
}
int Person::getAge() {
return age;
}
void Person::setName(char *n) {
std::strcpy(name, n);
}
void Person::setAge(int a) {
age = a;
}
然后,他重新编译了libperson.so。
编译方法:g++ -fPIC -shared -o libperson.so person.cpp
然后,不要重新编译main,立即运行,试试看。
$ LD_LIBRARY_PATH=. ./main
John 42
[1] 5023 segmentation fault (core dumped) LD_LIBRARY_PATH=. ./main
尼玛,发生了什么事!!!!
注意到,main函数里,Person是分配在栈上的。编译main.cpp的时候,编译器对person所有的知识只有那个person.h。所以,在分配内存的时候,很不幸,以为对象还只有100+4=104个字节。
但是,自从person.h和person.cpp变了以后,构造函数在初始化清零的时候,认为Person类占用的内存大小已经增加到100+100+4=204个字节了。于是,往栈上多写了100个0。于是,冲掉了栈上很多有用的信息(比如main函数的返回地址),然后就造成各种诡异的错误了。
所以,二进制兼容性是C++的软肋。C++程序员为了避免这种问题,一般使用“桥接模式”。
桥接模式把对象分为接口和实现两个类。接口类包含一个指向实现类的指针。所有对接口类的函数调用,都会转交给实现类。
新的代码:
// person.h
class PersonImpl; // 只声明一下。不知道它内容是什么。
class Person {
PersonImpl *impl; // 唯一的数据是一个指向实现的!!指针!!。不是包含实现本身。
public:
Person();
~Person(); // 需要析构函数。因为没打算加子类,就不virtual了。
char *getName();
int getAge();
void setName(char *n);
void setAge(int a);
};
#include "person.h"
#include <cstring>
// 在.cpp文件里定义一个实现类。
class PersonImpl {
//char title[100];
char name[100];
int age;
public:
PersonImpl();
char *getName();
int getAge();
void setName(char *n);
void setAge(int a);
};
// 原来Person的方法,现在变成PersonImpl的方法。
PersonImpl::PersonImpl() {
//std::memset(title, 0, sizeof(title));
std::memset(name, 0, sizeof(name));
age = 0;
}
char *PersonImpl::getName() {
return name;
}
int PersonImpl::getAge() {
return age;
}
void PersonImpl::setName(char *n) {
std::strcpy(name, n);
}
void PersonImpl::setAge(int a) {
age = a;
}
// Person的构造函数中,需要创建一个PersonImpl实例。
Person::Person() {
impl = new PersonImpl();
}
Person::~Person() {
delete impl; // 不要忘了回收内存。
}
// 其余的方法将控制转交给实现。
char *Person::getName() {
return impl->getName();
}
int Person::getAge() {
return impl->getAge();
}
void Person::setName(char *n) {
impl->setName(n);
}
void Person::setAge(int a) {
impl->setAge(a);
}
还用老方法编译:
g++ -fPIC -shared -o libperson.so person.cpp
g++ -o main -L . -lperson main.cpp
老方法运行:
LD_LIBRARY_PATH=. ./main
然后,把person.cpp里的title和memset的注释去掉,重新编译它
g++ -fPIC -shared -o libperson.so person.cpp
然后运行:
LD_LIBRARY_PATH=. ./main
这回运行就正常了。
感觉怎么样?
我的感觉:烦。太麻烦了。明明都使用getter和setter了……还是二进制不兼容。
但问题到底在哪里?
问题是C++把对象的布局和代码绑定得太早:看到头文件,就确定了对象的布局。甚至连有哪些private成员都被用户知道了。这违背了.h文件只放接口的原则。所以C++需要bridge模式。
Java呢?
Java其实不需要。Java是要维护一定的二进制兼容性的。
Java Language Specification专门有一章规定二进制兼容性。第13章专门提到了:向已有的类添加新的域、方法、构造方法,以及调换域、方法、构造方法的顺序,都是兼容的。
可以试试:
// Person.java
public class Person {
//private String title;
private String name;
private int age;
// Java自动把域初始化为null, 0, false等零值,我就不加构造方法了。
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
// Main.java
public class Main {
public static void main(String[] args) {
Person p = new Person();
p.setName("John");
p.setAge(42);
System.out.format("%s %d\n", p.getName(), p.getAge());
}
}
编译:
javac Person.java
javac Main.java
运行:
java Main
然后去掉title的注释,重新编译Person.java
rm Main.java # 如果你实在是怀疑javac可能会重新编译Main,干脆先删了它。
javac Person.java
再运行:
java Main
一切正常。
所以,结论是Java如果只是为了二进制兼容性的话,其实并不需要Bridge模式。
当然,Java鼓励分离接口和实现。如果想抽象出一个不含任何实现的接口,可以用interface:
public interface IPerson {
// String getTitle(); // 向接口里添加新方法
// void setTitle(String title); // 也不会破坏二进制兼容性。
String getName();
void setName(String name);
int getAge();
void setAge(int age);
}
这是一条镜像帖。来源:北邮人论坛 / java / #36941同步于 2014/12/10
该镜像源已超过 30 天没有更新,可能在源站已被删除。
Java机器人发帖
[黑设计模式系列]01:Bridge:桥接模式
nuanyangyang
2014/12/10镜像同步54 回复
订阅后,新回复会通过你的通知中心匿名送达。
9 条回复
Python呢?Python没有“二进制”兼容性问题。这种脚本语言,程序基本上都是以源代码发布的。Python文档明确说了.pyc文件的格式会随着Python本身的版本而变化,不鼓励用.pyc格式发布。
另一方面,Python没有object layout问题。所有的对象本质上都是字典,所有的属性访问都是运行时查字典。就算类突然加了__slot__,那么用不用slot也是运行时查询的,毕竟Python没有C++意义上的“编译”的步骤。
Haskell呢?很遗憾,Haskell除了osx上以外,不能用动态库发布,所以,所有的程序都是一下子编译成的一个大可执行文件,所以就没有“二进制兼容性”的问题了。
Scala呢?Scala跑在JVM上。但是Scala库的二进制兼容性一直让人头疼。Scala本身的标准库,每发布一个新版本,二进制就和前一个版本不兼容。所以很多其他的库编译的时候都要标明Scala本身的版本。
bd 学习
【 在 nuanyangyang (暖羊羊) 的大作中提到: 】
: 最近整个人都不好了。吐槽一下:
: 桥接模式是什么样的?
: 比如我们要实现一个“人"类。这里用C++语言。
: ...................