(三) 优先使用聚合,而不是继承

有一段时间,养猪场的老板雇用了清洁工人来打扫猪舍。但有一天,老板忽然对自己说"不对啊,既然我有机器人,为什么还要雇人来做这件事情?应该让机器人来打扫宿舍!"
于是,这个需求被提交到了机器人的研发小组。看到这个需求,我们敏感地意识到,这是一个潜藏了更多变化的需求,未来机器人的功能还可能会不断增加,于是,我们提取出了一个抽象的机器人接口,并实现了两个具体的机器人类一-喂猪机器人和清洁机器人。系统的结构如图V8-1所示。

                              图V8-1

这样一来,老板希望机器人工作时,可以调用机器人接口的"工作"方法。由于这也是针对接口编程,当老板需要新的机器人时,只要添加具体的机器人类即可,其他代码无需变化。这似乎己经解决了我们遇到的问题。
但这里还存在另外一个问题:图v8-1 中的继承结构是在编译期间就确定了的,在运行期不能发生任何变化。因此,如果养猪场需要一个喂猪机器人和→个清洁机器人,那么我们必须在养猪场中放进这两个具体的机器人。依此类推,如果未来养猪场还需要兽医机器人、屠宰机器人等等,养猪场中不就挤满了机器人吗?更为重要的是,每添加一种机器人的类型,主是们就必须改动代码中的某一个地方,以便把这个机器人放进养猪场中,这就又会违反开闭原则了。在这种情况下,使用聚合的机制能很好地解决问题,因为基于聚合的结构可以在运行期间发生变化。
使用聚合机制的养猪场如图v10-1 所示。我们把机器人接口改成了功能接口,而清洁功能和喂猪功能实现了这个功能接口。真正的机器人类中聚合了一个功能接口的引用,这样,我们只需要在养猪场中放进一个机器人,该机器人中聚合了一个喂猪功能,这时它是一个喂猪机器人。当我们需要打扫养猪场时,老板只需要调用机器人中的"变形"方法,并传递一个"清洁功能"对象给机器人,机器人就会像《变形金刚》中的"擎天柱"一样,大吼一声"汽车人,变形"就变成了-个清洁机器人了。

                                    图v10-1

面向对象养猪厂V10版本实现代码如下:

 using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms; namespace PigFactoryV10
{
/*
* 猪悟能的博客
* http://www.cnblogs.com/hackpig/
*
有一段时间,老板雇用清洁工人打扫猪厂,但有一天,老板对自己说“我有了机器人,为什么还要雇用人打扫猪场?”
于是,软件团队必须开发新的清洁机器人品种
这里,机器人的类型成了变化点 下面的代码使用了聚合方式,把机器人功能变成接口,只要操作员调用“变形”功能,并传入一个“清洁功能”,机器人就会
由喂猪机器人变身为清洁机器人。
如果未来要增加屠宰机器人,原有代码也不用修改。 这里反映出设计模式的第三个核心设计原则: 优先使用聚合而不是继承。
*/ public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
btn_clean.Click += new EventHandler(btn_clean_Click);
btn_feed.Click += new EventHandler(btn_feed_Click);
} void btn_feed_Click(object sender, EventArgs e)
{
adminMan man1 = new adminMan(, "大润发养猪场");
this.rtbMsgInfo.Text = man1.Msg;
} void btn_clean_Click(object sender, EventArgs e)
{
adminMan man1 = new adminMan(, "大润发养猪场");
this.rtbMsgInfo.Text = man1.Msg;
}
} public class adminMan:feedPigFactory
{
private string _msg; public string Msg
{
get { return _msg; }
set { _msg = value; }
} public adminMan(int funId,string factoryname)
{
base.FactoryName = factoryname;
robot robot1 = null;
switch (funId)
{
case : //喂食
robot1= new robot();
IList<Ipig> list1=new List<Ipig>();
list1.Add(new dbPig());
list1.Add(new dbPig());
list1.Add(new dbPig());
robot1.transformation(new feedPig(list1));
this._msg = robot1.work();
break;
case : //清洁
robot1= new robot();
robot1.transformation(new clean());
this._msg= robot1.work();
break;
default:
break;
}
} } public interface IfunInterface
{
string work();
} public class robot
{
private IfunInterface _robotFun; public IfunInterface RobotFun
{
get { return _robotFun; }
set { _robotFun = value; }
} public void transformation(IfunInterface robotFun)
{
this._robotFun = robotFun;
}
public string work()
{
return this._robotFun.work();
}
} public class clean : IfunInterface
{
public string work()
{
return "正在打扫清洁..."+Environment.NewLine;
}
} public class feedPig : IfunInterface
{
IList<Ipig> pigList = new List<Ipig>(); public feedPig(IList<Ipig> plist)
{
foreach (Ipig m in plist)
pigList.Add(m);
} public feedPig()
{
} public void Attack(Ipig pig)
{
pigList.Add(pig);
} public string work()
{
string msgstr = string.Empty;
foreach (Ipig m in pigList)
{
msgstr += m.eat() + Environment.NewLine;
} return string.Format("{0}{1}{2}",
"大润发养猪场" + Environment.NewLine,
"喂猪机器人开始工作...." + Environment.NewLine + Environment.NewLine,
msgstr);
}
} public abstract class feedPigFactory
{
private string _factoryName; public string FactoryName
{
get { return _factoryName; }
set { _factoryName = value; }
} } public interface Ipig
{ int PigIndex
{
get;
set;
} string eat(); } public class cbPig : Ipig
{
private int _pigIndex; public int PigIndex
{
get { return _pigIndex; }
set { _pigIndex = value; }
}
public cbPig(int pignum)
{
this._pigIndex = pignum;
} public string eat()
{
return string.Format("{0}[{1}]开始吃.", "长白猪", _pigIndex);
}
} public class dbPig : Ipig
{
private int _pigIndex; public int PigIndex
{
get { return _pigIndex; }
set { _pigIndex = value; }
}
public dbPig(int pignum)
{
this._pigIndex = pignum;
} public string eat()
{
return string.Format("{0}[{1}]开始吃.", "大白猪", _pigIndex);
}
} }

运行结果如上图所示, 老板可以下达指令在喂食和清洁机器人之间切换了.

代码说明:

在这里,喂猪机器人类是把原来直接调用Work() 喂食方法, 变成了先由transformation()指定功能类型, 再来执行Work().

public class robot
{
private IfunInterface _robotFun; public IfunInterface RobotFun
{
get { return _robotFun; }
set { _robotFun = value; }
} public void transformation(IfunInterface robotFun)
{
this._robotFun = robotFun;
}
public string work()
{
return this._robotFun.work();
}
}

而功能类型就是个IfunInterface接口, 而clearn(打扫清洁功能), feedPig(喂猪功能), 都是承继这个接口的.

public interface IfunInterface
{
string work();
}
public class clean : IfunInterface
public class feedPig : IfunInterface

最后利用工厂方法, 决定了机器人在喂食,还是清洁两种功能之间切换.

 public adminMan(int funId,string factoryname)
{
base.FactoryName = factoryname;
robot robot1 = null;
switch (funId)
{
case : //喂食
robot1= new robot();
IList<Ipig> list1=new List<Ipig>();
list1.Add(new dbPig());
list1.Add(new dbPig());
list1.Add(new dbPig());
robot1.transformation(new feedPig(list1));
this._msg = robot1.work();
break;
case : //清洁
robot1= new robot();
robot1.transformation(new clean());
this._msg= robot1.work();
break;
default:
break;
}
}

实际上我们是聚合IfunInterface这个抽象接口,即通过指向接口类的引用来访问对象, 这种实现方法其实是综合了聚合与继承两种机制的方式

此后,当我们添加一个新的机器人种类(如兽医机器人)时,只需要添加一个兽医功能的派生类,老板就可以根据自己的需要,在任何时刻命令机器人在三个种类之间随意变形。可以看出,添加一个机器人类型时,需要改动的代码都在系统外部,系统内已有的代码不需要发生变化。这里的聚合机制使我们很好地满足了开闭原则。

总之,继承和聚合是两种各不相同也各有优缺点的机制:

  • 继承反映的是类之间"……是一个……"这样的关系,它在编译期间静态定义。继承的优点是使用起来比较简单(因为面向对象的语言直接支持继承机制),对设计

人员来说比较容易理解。但继承也有缺点:

首先,你不能在运行期间改变继承树的结构,因为继承是在编译期间定义的:

其次,基类中往往定义了部分的实现,基类的实现暴露给派生类后,继承机制就会破坏数据和操作的封装,使派生类对基类产生较强的依赖O

  • 聚合反映的是类之间"有-个……"或"……包含一个……"的关系,它是在运行期间动态定义的,因此,被聚合对象的类型可以很容易地在运行期间发生变化,只要我们保证它们的接口相同,满足完全替换原则即可。而且,使用聚合可以更好地封装对象,使每一个类集中在单个职能上,类的继承层次也会保持较小的规模,不会造成类数量的爆炸。聚合的缺点是它并不是面向对象语言直接支持的一个特性,用户必须编写一些代码来完成聚合功能。例如,上面机器人类中的"工作"方法就必须把消息转发给内部聚合的功能对象,即调用功能对象的"工作"方法。被聚合对象的接口必须遵从聚合类的要求,这种消息转发的方式又被称为"委托( Delegation ) "。一般来说,聚合的结构比继承更难理解一些。

从上面的分析可以看出,聚合在某些方面比继承更为优越。但我们强调聚合的作用绝不是否定继承的优点。使用聚合时,我们必须遵循针对接口编程的设计原则,不能聚合某一个具体的派生类对象,而应该聚合该类的抽象接口,即通过指向接口类的引用或指针来访问对象----这种实现方法其实是综合了聚合与继承两种机制的方式。

由此,我们可以总结出设计模式的第三个核心设计原则
继承反映的是类之间的"……是一个…"的关系,聚合反映的是类之间"…有一个……"或包含一个……"的关系。在不违反这个关系前提下,应该
优先使用聚合而不是继承, 同时,聚合也必须和接口及相关的继承结构协同使用。

全文完.

本文源代码下载

包括面向对象养猪厂的各种版本实现代码(C#示例), 和VS2010绘制的UML类图.

原创文章,出处 : http://www.cnblogs.com/hackpig/

设计模式之 面向对象的养猪厂的故事,C#演示(二)的更多相关文章

  1. 设计模式之 面向对象的养猪厂的故事,C#演示(一)

    对于设计模式, 从本质上说, 其最大的用途就是适应需求的变化. 因为有了设计模式,我们可以在设计阶段就为未来可能发生的变化留下足够的空间. 我们通过一个建造现代化养猪场的故事, 来讨论一下设计模式与需 ...

  2. javaScript设计模式之面向对象编程(object-oriented programming,OOP)(二)

    接上一篇 面向对象编程的理解? 答:面向对象编程,就是将你的需求抽象成一个对象,然后针对这个对象分析其特征(属性)与动作(方法).这个对象我们称之为类.面向对象编程思想其中一个特点就是封装,就是把你需 ...

  3. [.net 面向对象程序设计进阶] (23) 团队开发利器(二)优秀的版本控制工具SVN(上)

    [.net 面向对象程序设计进阶] (23) 团队开发利器(二)优秀的版本控制工具SVN(上) 本篇导读: 上篇介绍了常用的代码管理工具VSS,看了一下评论,很多同学深恶痛绝,有的甚至因为公司使用VS ...

  4. 201871010123-吴丽丽《面向对象程序设计(Java)》第十二周学习总结

    201871010123-吴丽丽<面向对象程序设计(Java)>第十二周学习总结 项目 内容 这个作业属于哪个课程  https://www.cnblogs.com/nwnu-daizh/ ...

  5. 201271050130-滕江南-《面向对象程序设计(java)》第十二周学习总结

    201271050130-滕江南-<面向对象程序设计(java)>第十二周学习总结 项       目 内      容 这个作业属于哪个课程 https://www.cnblogs.co ...

  6. 201871010111-刘佳华《面向对象程序设计(java)》第十二周学习总结

    201871010111-刘佳华<面向对象程序设计(java)>第十二周学习总结 实验十   集合与GUI初步 实验时间 2019-11-14 第一部分:基础知识总结 第九章知识总结 1. ...

  7. 201871010109-胡欢欢《面向对象程序设计(java)》第十二周学习总结

    201871010109-胡欢欢<面向对象程序设计(java)>第十二周学习总结   内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这 ...

  8. 201871010101-陈来弟《面向对象程序设计(Java)》第十二周学习总结

    201871010101-陈来弟<面向对象程序设计(Java)>第十二周学习总结 实验十   集合与GUI初步 实验时间 2019-11-14 第一部分   理论部分 1.(1) 用户界面 ...

  9. 201871010104-陈园园 《面向对象程序设计(java)》第十二周学习总结

    201871010104-陈园园 <面向对象程序设计(java)>第十二周学习总结 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ ...

随机推荐

  1. ListView优化为何ViewHolder用static类(转载)

    如果有人还不了解ViewHolder为什么可以起到优化作用,我这边再做下简单说明:Android的findViewById动作是比较耗时的,需要遍历布局的树形结构,才能找到相应的视图.所以如果想在这一 ...

  2. 前端工程师技能之photoshop巧用系列扩展篇——自动切图

    × 目录 [1]初始设置 [2]自动切图 前面的话 随着photoshop版本的不断升级,软件本身增加了很多新的功能,也为切图工作增加了很多的便利.photoshop最新的版本新增了自动切图功能,本文 ...

  3. 开发笔记:基于EntityFramework.Extended用EF实现指定字段的更新

    今天在将一个项目中使用存储过程的遗留代码迁移至新的架构时,遇到了一个问题——如何用EF实现数据库中指定字段的更新(根据UserId更新Users表中的FaceUrl与AvatarUrl字段)? 原先调 ...

  4. 百度地图JavaScript API覆盖物旋转时出现偏移

    在项目中,调用百度地图JavaScript API,做覆盖物的旋转再添加到地图上,结果出现偏移了. 调试过程中的效果图: 发现图片的旋转并不是按车子的中心来的,而是之外的一个点.最后发现犯了一个很细节 ...

  5. 使用 CSS3 实现 3D 图片滑块效果【附源码下载】

    使用 CSS3 的3D变换特性,我们可以通过让元素在三维空间中变换来实现一些新奇的效果. 这篇文章分享的这款 jQuery 立体图片滑块插件,利用了 3D transforms(变换)属性来实现多种不 ...

  6. javascript学习4

    JavaScript Date(日期)对象 日期对象用于处理日期和时间. JavaScript Date(日期)对象 实例 返回当日的日期和时间 如何使用 Date() 方法或者当日的日期. getT ...

  7. elasticsearch中的API

    elasticsearch中的API es中的API按照大类分为下面几种: 文档API: 提供对文档的增删改查操作 搜索API: 提供对文档进行某个字段的查询 索引API: 提供对索引进行操作 查看A ...

  8. Laravel5设计json api时候的一些道道

    对于返回数据格式没规整的问题 在开发api的时候,这个问题是和客户端交涉最多的问题,比如一个user结构,返回的字段原本是个user_name的,它应该是string类型.但是呢,由于数据库设计这个字 ...

  9. 七、L2CAP

    1.      L2CAP 在BR/EDR模式下,在connection procedure成功执行后,两台设备通过一条物理信道(physical channel)连接在一起,同时两者之间建立起了一条 ...

  10. winform控件

    公共控件:1.Button: Enabled - 开始不可用 Visible -不可视(用来设置权限,取用户看不见的值)2.CheckBox .CheckListBox -复选框,复选框组 3.Com ...