面向对象设计与面向对象编程

我发现,面向对象设计,被忽视的太严重了。使用OOP语言,却不是面向对象来设计。无非就是用类写C程序而已,而且这种情况还很常见。另一种情况是过度重视类,而忽视了对象,但是对象才是根本。不过本文会以类型为主,来介绍面向对象设计。


前言

面向对象是一个完全不同于传统软件设计方式的理念,传统软件设计是以功能为主体,以功能为模块,以功能为目标。而面向对象不同,是以软件运行中的组件为主体,实现某个功能,只是恰好这些组件的交互可以间接做到。这些组件,就是对象。用面向对象来设计软件,实际上就是设计一系列的对象,使这些对象在运行中恰好能间接完成软件要做的事。


一 类型

为什么,面向对象出现的时候,同时也会有类这种东西。因为类就是设计对象的一个方式,将对象划分为不同的类型,使用类来描述各种类型的对象。就像一个建筑蓝图,有个人设计了一个大楼,画成蓝图,于是,他可以随时根据这个蓝图去建大楼。

这是反向运用。

因为我们是先认识到一个个的对象,才开始有类型的概念。四条腿,一个长长的头,和尾巴,非常能跑,我们发现很多个这样的对象,于是建立了一个类型:马。后来又发现了狗、猪、牛、羊等等,我们又发现了它们都有一个共同的特点:哺乳产子。于是我们建立一个了类型:会哺乳的,这个类型包含所有是哺乳产子的对象。然后我们又发现,还有另一些东西:鸡、鸭、鸟、鱼等等,和会哺乳的不同,它们是下蛋产子、下卵产子。它们和哺乳产子的都有一个共同的特点:会动。于是我们建立了一个类型:动物,这个类型包含了所有会自己动的对象。经过慢慢的发现,最终就有了现在的体系。

我们还发现了,任何对象,都有两个东西:行为、属性。鸟常常唱歌,这是鸟的行为,鸟有各种大小、颜色、样子,这是鸟的属性。所以我们的OOP语言,建立类型描述对象时,都会提供方法和成员变量,来对应行为和属性。

你现在用的语言,可能就有一个root类型:object,这个类型其实就是『类型本身』,所以任何类型的对象,都可以视为一个object类型的对象。我们在认识了很多很多个对象,建立了很多很多个类型后,同时也开始了反向认识,就是类。从对象建立类型,是从下往上,重建类型划分对象,是从上到下。

我们用的语言,对于建立类型,有三大要素:封装、细化、多态。比如在C++,我们用class定义类型,就是封装,此类型对象的行为和属性,都在class定义中。你建立一个类型:书,然后再建立几个类型:简书、帛书、线装书、电子书等等,你一定会将这些类型归为书的子类型,这就是细化。你需要画一个形状,这个形状可以是矩形圆形乱七八糟型,那么你只需要针对形状类型来操作,比如在形状类型中加上行为:画。其细化的类型:矩形圆形乱七八糟型都各自定义自己的画行为。你只需要获得一个由形状细化的类型的对象就行了,调用它的行为:画,这就是多态。

在软件设计中运用面向对象,其实就是建立一个类型系统,每一个软件都有一个自己的类型体系。我们用类型来设计软件,软件在运行时,通过各种类型的对象来交互,从而恰好间接的完成要做的事。


二 可变 不可变

我们在建立类型时,需要仔细的考虑细化问题。比如一个类型:human,然后我们再细化出两个类型:man、woman。

class human {};
class man : public human {};
class woman : public human {};

但是,这样的细化,是有很大问题的。因为一个man对象,是会变成woman对象的,反之亦然。比如泰国的poy,你会把她当成男吗?为什么?因为她现在就是女的。这个是会变的。而会变的,是属性,而不是类型。

什么叫属性,一个物体的坐标、一个人的名字、你钱包的充实度。这些就是属性,是可变的。而类型不会变,马属于哺乳动物,钢铁属于金属,这些是不变的。

而性别,其实是属性,比如在动物界,很多类型的动物,会改变性别,有些还没性别。我们更应该,把性别作为属性,而不是类型。

class human
{
public:
    const string& getsex() {return this.sex;}
    void setsex(const string& newsex) {return this.sex;}
private:
    string sex;
};

这样,你的类型系统就正常了。为什么呢,比如你设计一个通讯录啊、人际管理啊之类的软件,你细化出man、woman的话,类型体系臃肿,不说,你光是新建一个人员时都得分析是建立man对象还是woman对象,作为属性就不需要了,你只需要human a("man");就可以了,他变性了,那么a.setsex("woman");就可以了。试想如果你不用属性,而是用类型来做,又会是怎样的场面?

所以,我们在建立类型系统时,在细化时,需要分清楚,到底是属性,还是子类型。

可变的,就是属性。

不可变的,就是类型。


三 可能 不可能

这里,我们分析的是,对象的行为。我们在设计一个类型时,如何合理的设计方法。

我常常看到一些,不正确的方法设计。方法没有放到正确的位置,或者是多余的方法,或者是不应该用方法而应该用函数。

比如,有人要设计一个看书软件,于是建立这书这个类型:

class book
{
protected:
    string name;
    string author;
};

然后,因为书是要翻的,于是他有了这样的设计:

class book
{
public:
    void before_page(){if (this.cur_page > 0) {this.cur_page -= 1;} }
    void after_page(){if (this.cur_page < this.page_num) {this.cur_page += 1;} }
protected:
    ...
    size_t cur_page;
    size_t page_num;
};

看上去,似乎没什么。再一看,好像确实没问题。

但是。

这是一个错误的设计。

面向对象,每一个对象都有着自己的行为、属性。我们好好的分析,往上翻页,往下翻页,是谁的行为。是书自己的行为?就比如现实中我们跟一本书讲:翻到下一页,然后这本书就特么的自己动了?自己翻了?这已经是个神话故事了。

翻书是谁的行为,是看书者的行为,看书者用手翻。而不是这本书自己翻。当然如果你真的看到了一本书自己翻,请一定要告诉我,我要膜拜神迹。

那么怎么样设计翻页这个行为呢?简单,非常简单。作为看书者的方法,就可以了。

class book
{
public:
    ...
    const string& page(size_t n);
private:
    ...
    vector<string> pages;
};

class reader
{
public:
    size_t page() {return this.cur_page;}
    void before_page(){if (this.cur_page > 0) {this.cur_page -= 1;} }
    void after_page() {if (this.cur_page < this.page_num) {this.cur_page += 1;} }
private:
    size_t cur_page;
};

class con
{
public:
    void render () {cout << cur_book.page(cur_reader.page()); << endl;}
private:
    reader cur_reader;
    book   cur_book;
};

// 上面这样的设计还带来了一个额外的好处:降低耦合。

书是什么对象,就是一个信息对象,只需要有属性,书名、作者、每个页的文本等等,就这些。当然我们需要用方法,来包装属性,这属于软件工程中很重要的一个手段。在这样的情况下,book.page(),并不是书的行为,而只是对属性的包装。

翻书,是看书者是行为,所以,放在看书者的类型里,最合适。

但是我为什么不我直接把cur_book放到reader里,作为属性呢?因为看书者和书,是两个不同的东西,我们拿起一本书,只是和这本书建立起一个暂时的联系。就像朋友问你,绝对不会问:“你的当前书是什么啊”,这是一种属性的问法,而只是会问:“你现在在看什么书啊”,这是联系的问法。看书者的属性,都是联系性的,比如书名啊,当前的页码啊等等。我们不需要放cur_book到里面,但是我们可以放一个string book_name到里面,作为reader的属性。



再看另一个例子,这个不同了,这是一个广泛被采用的错误例子!

class a : public object
{
public:
    a* clone() {return new a();}
};

class b : public object
{
public:
    b* clone() {return new b();}
};

...

object* src = ... // new a or b
object* k = src->clone();
...

对,克隆。一个非常广泛的类型方法设计。这是运用多态来克隆对象,我们不需要管被克隆的是什么对象,只要有clone方法,我就让你克隆,克死你。

我们仔细分析一下,问题在哪里,在哪里。

那就是不可能。

这个和上面的不同了,上面的book的page,只是属性的包装。而这个是真正的行为了,是纯动词了,连名词都没了。这就是对象的行为了,是一个类型的方法了。

但是,这不可能,这个对象不能够这样行动。

比如科学家克隆一只羊,如果用这个被广泛采用的方式,那就是这样的场面:”诶,那只羊,快点,克隆一个新的你,别墨迹啊。“

滑稽吗,可笑吗?

实际上,科学家是怎么克隆的呢?是科学家用各种手段,来复制这只羊。也就是说,克隆,是科学家的行为,而不是那只羊,被克隆者的行为。哪怕这只羊精通天文地理物理数学,啊,这样的话或许它还真能自己克隆自己,当然前提是它有各种设备可以用,如果你发现了这样的一只羊,请一定告诉我,我将膜拜。

当然不要被上面这段话误导,这实际上一个执行者和目标对象的关系,克隆由科学家执行,执行者是科学家,目标对象是羊。跟这只羊精不精通天文地理物理数学没关系,如果他能自己克隆自己,那么执行者就是羊,目标对象是它本身,行为是克隆。

那么我们应该如何设计,克隆呢?

那就是分析出谁是执行者,放到执行者里面。

class scientist
{
public:
    sheep* clone(sheep* target);
    dog* clone(dog* target);
    pig* clone(pig* target);
    money* clone(money* target); // 其实money是一种动物 :)
};

...

secientist sei;
sheep s;
animal* k = sei.clone(&s);

有些时候,我们分析不出执行者,或者实在懒得分析,那么作为函数就行了

class string : public object {};
class vector : public object {};

// 你可能需要使用friend来连接这些函数
string* clone(string* target);
vector* clone(vector* target);

...

string s();
object* k = clone(&s);


我们有没有在现实中看到能自己克隆自己的东西呢?自己能克隆自己,简直是神迹啊。

但是,宇宙万物不可思议。

还真的有能自己克隆自己的东西:病毒。

这是题外话。

所以,虽然我说被广泛采用的clone方式是个错误的设计,但指的是语义上的错误,在软件设计中,其实那是正确的设计。病毒能自己克隆自己,那么计算机里的字符串、数组能自己克隆自己,有什么好稀奇的。不可思议,计算机本来就不可思议。既然能自己克隆自己,那么clone作为这个对象的行为,并没什么问题。


所以我们建立类型系统,是以『现在』为参照的,对于『现在』是不变的。但到了以后,或许人类不再被归为高级动物了,不是动物了,那么所有将human作为animal的子类型的设计,又要重新设计了。所以我们用面向对象来设计软件时,不要沉迷于类型,而是要沉迷于对象。我就是因为在上面的例子中沉迷于类型,才会有clone不能作为对象行为的看法。什么行为和属性,一个对象能克隆自己,那么给他加个clone方法,不能,就不需要有,根本不需要用类型来定义。为什么JavaScript比C++/Java更面向对象,因为JavaScript不沉迷于类型,而C++/Java沉迷于类型。

面向对象,面向的,是对象。



以上是对类型的介绍,下面,才是真正的对面向对象的介绍



四 面向对象设计

那么什么是面向对象设计?在面向对象的领域中,有几个方面:OOA(面向对象分析)、OOD(面向对象设计)、OOP(面向对象编程)。

首先是面向对象分析,我们需要弄清楚,软件中需要有哪些对象,这些对象是什么关系,要做的是什么。注意,是对象,而不是类型。就比如一个看书软件,有哪些对象?比如我们可以发现,有书、看书者,还有界面、用户设置等等对象。

然后是面向对象设计,我们就是分析出的对象为主体,围绕这些对象,来设计软件。而不是围绕功能,围绕功能去设计软件的,就不是面向对象。面向对象只能是以对象为主体,什么乱七八糟功能,都只是这些对象在交互中,恰好间接的完成了而已。模块的划分,大概就体现出了,你是不是面向对象。是面向对象的话,基本上每个模块都对应着一个对象。当然不是Java那种,Java是以类型划分模块,而不是以对象划分模块。


简单的说,只要你的设计是以对象主的,那么就是面向对象设计。

FILE* f = fopen("a.txt", "rb");
const char* str = "hello world!\n";
fwrite(str, strlen(str), 1, f);
fclose(f);

C语言不是OOP语言,但C语言是OOD语言,C语言的很多地方,都体现出了面向对象,比如fopen系列,数学运算系列,字符串系列等等。

在这里,f就是一个文件对象,fopen建立这个对象,fwrite将数据写到一个文件对象里,fclose关闭一个文件对象。fwrite不需要管你打开是哪个文件,只要能写,它就给你写。fclose不管你打开的是什么文件,反正你传入一个文件对象,就给你关了。fopen系列,围绕着文件对象,也就是FILE*类型的对象,这就是面向对象设计。在这个,你打开a.txt文件,然后写入hello world!,都是通过文件对象间接完成的。fopen/fread/fwrite/fclose/···就是文件对象的方法,FILE*结构里的东西,就是文件对象的属性。

有一个更大更好的例子,就是Windows API的HANDLE,几乎所有的函数,都是围绕HANDLE做事的。一个HANDLE对象,要么是最上层的HANDLE类型,要么是HMENU、HMODULE等更细化的类型。你不用知道这些HANDLE都是什么鬼样子,反正你只要用HANDLE对象来做事就行了,Windows API,就是围绕着HANDLE对象做事的,这就是面向对象设计,而且是最纯粹的。

面向对象设计,和什么语言是无关的。因为这是设计,是体现出现的,而不是具体的样子。我们可以在任何语言中运用面向对象设计。

哪——怕——是——汇——编。

但面向对象编程和语言是有关的。


五 面向过程

区别到底在哪里?区别在于,面向过程以功能为主,以目标为主。比如上面的a.txt例子,用面向过程去做,就是这样。

OpenFile("a.txt");
const char* str = "hello world!\n";
Write(str, strlen(str), 1);
CloseFile();

在这里,就是纯粹的实现功能,打开a.txt文件,然后写那行字,然后关了。没有什么文件对象。在划分模块时也是以功能为主:OpenFile(打开文件)/Write(写东西到文件)/CloseFile(关闭文件)

用面向对象方式,就是以对象为主,这里有文件对象,所以我们建立一个文件对象,围绕这个对象,来间接完成要做的事。看起来是不是区别小?实际上,天壤之别。


六 面向对象编程

光有面向对象设计是不够的,我们还需要有在骨子里就支持面向对象设计的语言,就是我们现在常见的,OOP语言。这些语言,能使你更自然的设计对象,提供封装、细化、多态来让你能更好的运用对象。

面向对象不包括封装、细化、多态。

面向对象编程才有封装、细化、多态。

为什么?因为面向对象,主体是对象,其实是不管类型的。然而我们需要管类型,所以面向对象编程,才会提供封装、细化、多态给我们,就是让我们能有一套手段来管理不同的对象。没有这些,我们就难以实现面向对象。

为什么str.size()对比strlen(str)是一个巨大的进步,因为我们有了强力的手段,来实现面向对象。面向对象早就有了,但是没有提升软件设计领域的水平,而面向对象编程语言的出现,才提升了软件设计领域的水平。

但是,虽然我们有了OOP语言,却依然会不知不觉的偏离面向对象,导致用类写C程序,用对象来直接完成功能的出现。

这是因为没有弄清楚对象,没有设计好对象。

另一个偏离是,过度重视类型,重视class,而忽略了对象。比如有些编程者,差不多把面向对象变成了面向设计模式,代码中全是设计模式,而最重要的,最核心的对象,都被淹没在各种模式中。


所以,整个面向对象领域中,有三大元素:

面向对象分析

面向对象设计

面向对象编程


缺一不可

在C语言,我们都会运用面向对象设计,到了OOP语言,就更应该面向对象设计。不要想着功能,不要想着目标,而是对象的交互恰好间接的实现用户的需求,恰好、间接,这是两个很重要很重要很重要很重要的概念。主体是对象,用类型来设计对象。用这些对象的交互来恰好间接实现功能,不能直接实现功能,不能。



在结尾的最后,我给大家说一个目前大部分OOP语言的一个缺陷,就是过度重视类型。比如在某个对象只需要有一个实体时就出问题了,设计模式中介绍的单例模式是有极其严重的BUG的。为什么,因为在这种情况下,类型都不是必须的,很多语言都着叹为观止的类型设计手段,而没有一个设计单对象的方式。只有一个对象,意味着什么封装、细化、多态都不需要了,一定要建立一个类型,然后用各种诡异、莫名其妙的手段来限制只能建立一个实体对象,是自寻烦恼。面向对象,核心是对象,而不是类型。

转载于:https://my.oschina.net/u/1982890/blog/479844

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值