我发现,面向对象设计,被忽视的太严重了。使用OOP语言,却不是面向对象来设计。无非就是用类写C程序而已,并且这种状况还很常见。另外一种状况是过分重视类,而忽视了对象,可是对象才是根本。不过本文会以类型为主,来介绍面向对象设计。
编程
面向对象是一个彻底不一样于传统软件设计方式的理念,传统软件设计是以功能为主体,以功能为模块,以功能为目标。而面向对象不一样,是以软件运行中的组件为主体,实现某个功能,只是刚好这些组件的交互能够间接作到。这些组件,就是对象。用面向对象来设计软件,实际上就是设计一系列的对象,使这些对象在运行中刚好能间接完成软件要作的事。
设计模式
为何,面向对象出现的时候,同时也会有类这种东西。由于类就是设计对象的一个方式,将对象划分为不一样的类型,使用类来描述各类类型的对象。就像一个建筑蓝图,有我的设计了一个大楼,画成蓝图,因而,他能够随时根据这个蓝图去建大楼。数组
这是反向运用。编程语言
由于咱们是先认识到一个个的对象,才开始有类型的概念。四条腿,一个长长的头,和尾巴,很是能跑,咱们发现不少个这样的对象,因而创建了一个类型:马。后来又发现了狗、猪、牛、羊等等,咱们又发现了它们都有一个共同的特色:哺乳产子。因而咱们创建一个了类型:会哺乳的,这个类型包含全部是哺乳产子的对象。而后咱们又发现,还有另外一些东西:鸡、鸭、鸟、鱼等等,和会哺乳的不一样,它们是下蛋产子、下卵产子。它们和哺乳产子的都有一个共同的特色:会动。因而咱们创建了一个类型:动物,这个类型包含了全部会本身动的对象。通过慢慢的发现,最终就有了如今的体系。函数
咱们还发现了,任何对象,都有两个东西:行为、属性。鸟经常唱歌,这是鸟的行为,鸟有各类大小、颜色、样子,这是鸟的属性。因此咱们的OOP语言,创建类型描述对象时,都会提供方法和成员变量,来对应行为和属性。this
你如今用的语言,可能就有一个root类型:object,这个类型其实就是『类型自己』,因此任何类型的对象,均可以视为一个object类型的对象。咱们在认识了不少不少个对象,创建了不少不少个类型后,同时也开始了反向认识,就是类。从对象创建类型,是从下往上,重建类型划分对象,是从上到下。spa
咱们用的语言,对于创建类型,有三大要素:封装、细化、多态。好比在C++,咱们用class定义类型,就是封装,此类型对象的行为和属性,都在class定义中。你创建一个类型:书,而后再创建几个类型:简书、帛书、线装书、电子书等等,你必定会将这些类型归为书的子类型,这就是细化。你须要画一个形状,这个形状能够是矩形圆形乱七八糟型,那么你只须要针对形状类型来操做,好比在形状类型中加上行为:画。其细化的类型:矩形圆形乱七八糟型都各自定义本身的画行为。你只须要得到一个由形状细化的类型的对象就好了,调用它的行为:画,这就是多态。设计
在软件设计中运用面向对象,其实就是创建一个类型系统,每个软件都有一个本身的类型体系。咱们用类型来设计软件,软件在运行时,经过各类类型的对象来交互,从而刚好间接的完成要作的事。code
咱们在创建类型时,须要仔细的考虑细化问题。好比一个类型: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做为这个对象的行为,并没什么问题。
面向对象,面向的,是对象。
那么什么是面向对象设计?在面向对象的领域中,有几个方面: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的。为何,由于在这种状况下,类型都不是必须的,不少语言都着叹为观止的类型设计手段,而没有一个设计单对象的方式。只有一个对象,意味着什么封装、细化、多态都不须要了,必定要创建一个类型,而后用各类诡异、莫名其妙的手段来限制只能创建一个实体对象,是自寻烦恼。面向对象,核心是对象,而不是类型。