找到这里的多半是我的学弟学妹了,怎么说呢,希望能帮到你们吧,也更希望我留在这里的思路和代码是你们的下限,设计模式真的很精妙,祝你们有所增益。
写在开头
我在八周之前开始学这门课的时候看大黑书,觉得大黑书写的晦涩枯燥根本理解不了,老师讲了之后做作业也根本没思路没想法,然后上网找答案,结果网上答案也写得参差不齐,最难受的就是自己看不懂也不知道对不对,感觉自己陷入了一个恶性循环,很浮躁。后来机缘巧合看了程杰的《大话设计模式》,写的很有趣很生动,开始有了求知欲,我也不知道我能不能坚持,试试吧,程序狗肯定还是要回归敲代码啦。
题目1
小王为某管理信息系统的用户管理模块设计了如图所示接口。
1)请你从面向对象设计原则的角度详细分析小王的设计存在什么问题?
答案:
1)存在的问题有:违反了①接口隔离原则②单一职责原则
这道题要解答必须明晰设计模式的六大基本原则:
- 单一职责原则:一个类或者一个方法只负责一项职责,尽量做到类的只有一个行为原因引起变化;
- 里氏替换原则:能出现子类的地方都应该可以允许父类出现,也就是子类可以扩展父类的功能,而不能改变原有父类的功能使得父类不能用了;(本质其实就是c++的多态)
- 依赖倒置原则:通俗点就是说变量或者传参数,尽量使用抽象类,或者接口,也就是针对接口编程,而不要对实现编程。
- 接口隔离原则:建立单一接口,复杂的接口,根据业务拆分成多个简单接口。
- 迪米特原则:最少知道原则,尽量降低类与类之间的耦合。一个对象应该对其他对象有最少的了解。举个例子:要想实现一个类还需要知道其他100个类这是不是很离谱很不好。
- 开闭原则:对扩展开放,对更改封闭。这条原则呢是希望我们尽量不要改动旧代码,因为可能牵一发而动全身嘛,所以设计一个类的时候要想到它最好留有接口(可扩展),并且把自己的访问修改权限管的严一点不要让后来的代码随便改动它(闭更改)。
然后从这个接口类实现的图里我们可以看到,它的方法涉及到了用户名、密码和角色的修改,任何一个小变量的更改都会导致使用这个类相关的类发生修改,作用范围太大,所以违反了单一职责原则;同时我们还应该注意到,changPassword显然是用户的基本属性,addRole更应该是用户的行为,然而这两个都放在了这个接口类去实现,这合理吗?不合理!不同性质的调用操作必须分离,所以它违背了接口分离原则。
2)请你采用面向对象方法帮小王重新设计,使得新设计能够符合面向对象设计原则。简要说明你的设计思想,画出UML类图。
根据第一问的分析进行修改,也就是说:我们可以将基本信息与用户权限相关的方法单独设置一个接口IBasicInfo, 账户安全信息与管理员权限相关的单独设置一个接口ISecurityInfo(接口分离原则得到满足)。然后实例化后的类BasicUserInfo实现IBasicInfo,类UserInfo使用聚合来复用IBasicInfo(复用就使得单一原则得到满足,相当于各干各的,我需要的时候再调用你),并实现ISecurityInfo接口。
我们老师给的答案:
题目2
客户请小王编写一个从键盘读入字符并输出到打印机的程序。小王使用结构化的设计方法,编写了如下代码。
void Copy( ) {
int c;
while ((c=ReadKeyboard( )) != EOF)) //EOF为宏定义的终结字符
WritePrinter(c);
}
该程序包含3个子程序:Copy子程序从ReadKeyboard子程序中获取字符,并把字符传递给WritePrinter子程序。几个月后,客户说有时希望Copy程序能从纸带读入机中读入字符。并且,在未来可能还会从其它种类的输入设备中读入字符,也可能输出到其它种类的输出设备。小王希望他写出的代码既能满足上述要求,又不用每次都改写Copy的实现。
1)请你从面向对象设计原则的角度分析小王原来的设计存在什么问题?
这个很显然,违背了依赖倒置原则,c=ReadKeyboard( )) != EOF该语句只能去读键盘,如果变成纸带机等其他输入设备时,明明实现的功能类似,却要重写代码,这就是过度依赖实现细节了。
2)请你采用面向对象方法帮小王重新设计Copy函数,添加必要的类,使得新设计能够满足小王的愿望。简要说明你的设计思想,给出实现代码。
那么分析一下,公共部分就是读入字符嘛,那ReadKeyboard太过具体了就不好。
那我们就想:“可不可以搞一个父类Device喃?要细化的时候就继承重写嘛。”
- 但这个也不是很好,继承是强耦合的,同时注意题目说不仅输入设备会换,之后方法的具体实现也可能变,如果是继承的话,子类只要改写,这一过程很可能引入父类无法实现的新行为,是很危险的(继承必须满足里氏替换原则,假如说我们就用继承,父类里面有一个方法copy,从输入设备读入数据,然后某一天我们想弄一个收音机,把收音机里面的磁带数据读入,但是这意味着我们又要找一个能读磁带数据的机器,此时父类的方法还能用吗,显然不能,而子类能出现的地方父类却不能出现了,这就不合理了)所以这里我觉得可以暂不要使用继承。
那么就剩下接口实现了,这个就很好!因为接口特性决定的耦合性不高,接口还可以实现多继承效果,为适应日后增添的输入输出设备,想要什么类就实现device接口重写read()就好了。
所以可以改成:
public interface Device {
String read();
}
//接口类以适应日后增添的输入输出设备,想要什么类就实现device接口重写read()就好了
class Keyboard implements Device{
@Override
//重写父类方法,这个时候需要返回一个KeyboardRead对象
public String read() {
return "KeyboardRead";
}
}
class Printer implements Device {
@Override
public String read() {
return "PrinterRead";
}
}
public class Copy{
void copy(Device device){
String c;
while(c=device.read()!=EOF){
WitePrinter(c)
}
}
}
老师给的参考答案:
题目3
一个开宝箱游戏的基本描述为:
游戏中有多种类型的人物(Role),如战士(Solider)、魔法师(Mage)等,主角的类型只能选择其中一种,且游戏中不再更改。游戏中还有各种宝箱(Box),如装有不同数目金钱的宝箱、装有毒物的宝箱等。当任一种类型的主角打开装有金钱的宝箱时,宝箱中的金钱会增加给主角,同时宝箱的金钱数目变成0;当战士打开装有毒物的宝箱时,战士的生命值(HP)会减少10%,但金钱(Money)增加20%;当魔法师打开装有毒物的宝箱时,魔法师的生命值(HP)会减少30%,但金钱(Money)增加40% 。
请根据上述描述,给出相应类的设计并完整实现,要求你的设计应具有良好的扩展性,如增加新角色类型及箱子种类时,不需要修改已有的设计及实现。
首先分析这个案例,我们抽象一下就是
- aaa打开bbb,aaa对应的生命值属性会变化,同时aaa的金钱属性会更新
- aaa是角色,目前有战士、魔法师之分,日后会新增
- bbb是宝箱,现有金钱、毒药之分,日后会新增类型
显然我们其实已经理出来:
- 要有一个角色Role类
- 属性:生命值HP、金钱Money
- 方法:打开宝箱Open(Box* aBox)、获得金钱GetMoney()、生命值受损 virtual void PoisonHurt() = 0; - 要有一个Box类
- 属性:Role* role
- 方法:更改Role的属性值virtual void BeOpened(Role* role) = 0;
这里复习了一下c++里virtual关键字的用法:
实现多态时,用在父类和子类的签名相同(方法同名、参数相同(个数相同、类型相同)、返回值相同)的方法中。virtual用在父类方法中,表示该方法可以被覆盖。
游戏流程就是根据相应的要求实例化角色对象,相应的具体角色继承实现Role类,然后覆盖重写对应的角色行为成为魔法师和战士这些,相应的宝箱继承实现Box以实现毒药、金钱的功能。
在这道题里,我没有画uml图理它们的关系,但可以提一嘴,父类Role和Box都在某个方法实现中使用了对方类指针,这是一种双向关联,但因为指针的生命周期只存在一个方法中,所以耦合性不强,属于关联关系。
#include<iostream>
using namespace std;
class Role;
class Box {
public:
virtual ~Box() {}
virtual void BeOpened(Role* role) = 0;
};
class Role {
public:
Role(int theMoney, int theHP) :money(theMoney), hp(theHP) { }
virtual ~Role() {}
int GetMoney() const { return money; }
void SetMoney(int m) { money = m; }
void Open(Box* aBox) { aBox->BeOpened(this); }
virtual void PoisonHurt() = 0;
protected:
int money;
int hp;
};
class Solider :public Role {
public:
Solider(int theMoney, int theHP) :Role(theMoney, theHP) { }
virtual void PoisonHurt() {
hp *= 0.9;
money *= 1.2;
}
};
class Mage :public Role {
public:
Mage(int theMoney, int theHP) :Role(theMoney, theHP) { }
virtual void PoisonHurt() {
hp *= 0.7;
money *= 1.4;
}
};
class MoneyBox :public Box {
public:
MoneyBox(int m) :boxMoney(m) { }
virtual void BeOpened(Role* role) {
role->SetMoney(role->GetMoney() + boxMoney);
boxMoney = 0;
}
protected:
int boxMoney;
};
class PoisonBox :public Box {
public:
virtual void BeOpened(Role* role) { role->PoisonHurt(); }
};
int main() {
Role* solider = new Solider(10, 10);
Role* mage = new Mage(10, 10);
Box* moneyBox = new MoneyBox(10);
Box* moneyBox2 = new MoneyBox(2);
Box* poisonBox = new PoisonBox;
solider->Open(moneyBox);
//cout << solider->GetMoney() << endl;
solider->Open(poisonBox);
//cout << solider->GetMoney() << endl;
mage->Open(moneyBox2);
mage->Open(poisonBox);
return 0;
}
老师给的参考答案:
题目4
给出适当的类设计和相应的代码:
有一个只能放进不能取出的盒子, 最多可放8个水果, 不一定一天放入。
水果只是苹果和桔子两种, 它们放入盒子前的原始重量分别为50和30。
放入盒子后, 由于丢失水分, 它们的重量减轻, 苹果和桔子每天分别减轻4和3, 直到达到各自原始重量的3/5后, 不再减轻重量。
盒子的功能有: 输出盒子中苹果的数量; 输出盒子中桔子的数量; 输出一天来盒子中水果减轻的总重量; 输出当前水果的总重量。
首先分析这个案例,万事万物先抽象——
- 盒子里放水果,水果会缩水,盒子需要告诉主人内部的水果情况
继续分析:
盒子只有一个,目前已知的水果种类是苹果和橘子,但每一个实例化的苹果和橘子会因为呆在盒子中的天数不同属性(水分)也变得不同。
那么仔细想想苹果和橘子的联系,他们的公共部分都放到虚基父类Fruit里,然后多态实现的时候各自覆盖父类方法,他们的公共部分有:
- 属性:初始状态重量int mMaxWeight、终止状态重量int mMinWeight、已经损失的重量int mLoseWeight、当前的重量int mWeight;
- 方法:减重 virtual int ReduceWeight( );
而关于8个水果限制的解决办法:可以把实例化的每个苹果放入LIst容器,放入时检查List.size()是否到达8的限制,同时盒子也可以从这个容器里去遍历求重量。
class Fruit
{
public:
Fruit(int mMax =0,int mMin=0,int mLose = 0,int mW = 0)
:mMaxWeight(mMax),mMinWeight(mMin),mLoseWeight(mLose),mWeigth(mW) {}
virtual ~Fruit() {}
virtual Fruit * Clone( ) const = 0;
virtual int ReduceWeight( );
virtual int Weight( )const;
protected:
int mMaxWeight;
int mMinWeight;
int mLoseWeight;
int mWeight;
};
int Fruit::ReduceWeight( )
{
int newWeight = mWeight - mLoseWeight;
if (newWeight < mMinWeight )
newWeight = mMinWeight;
int reduce = mWeight - newWeight;
mWeight = newWeight;
return reduce;
}
int Fruit::Weight( ) const
{
return mWeight;
}
class Apple:public Fruit
{
public:
Apple():Fruit(50,50*3/5,4,50) { }
virtual ~Apple( ) {}
virtual Apple * Clone() const
{ return new Apple(*this); }
// other
};
class Orange:public Fruit
{
public:
Orange():Fruit(30,30*3/5,3,30) { }
virtual ~Orange( ) {}
virtual Orange * Clone() const
{ return new Orange(*this); }
// other
};
class Box
{
public:
Box():count(0) {for(int i=0;i<8;i++) fruit[i]=0;}
~Box() {for(int i=0;i<8;i++) delete fruit[i];}
void AddFruit(Fruit& one) //放入一个水果
{
if (count<8)
fruit[count++]=one.Clone();
}
int ApplesNum() const
{
int num=0;
for(int i=0;i<8;i++) {
Apple * p=dynamic_cast<Apple *>(fruit[i]);
if (p)
++num;
}
return num;
}
int OrangesNum() const
{
int num=0;
for(int i=0;i<8;i++) {
Orange * p=dynamic_cast<Orange *>(fruit[i]);
if(p)
++num;
}
return num;
}
int PassOneDay() //一天来失去的总重量
{
int num=0;
for(int i=0;i<8;i++)
if(fruit[i])
num+=fruit[i]->ReduceWeight();
return num;
}
老师给的参考答案:
题目5
当把下面程序交给用户试用时, 针对函数f, 用户提出了一项新的要求:
当condition为100时, 依次执行b的成员函数g1( )和g2( )。
经过进一步了解, 小王获悉: 以后可能还要增加处理condition的值是其它数值时的情况, 但这些需要分别处理的不同条件值的个数肯定不多。小王希望他写出的代码既能满足上述要求, 又不用每次都改写f的代码。请你帮小王重新设计, 使得新设计能够满足小王的愿望。简要说明你的设计思想, 给出实现代码。
class B {
public:
B(int n):data(n) { }
int Data( ) const { return data; }
void g1( );
void g2( );
void g3( );
private:
const int data;
};
void f( B& b ) {
int condition = b.Data( );
if(condition == 1) { b.g1( ); }
else if(condition == 5) { b.g2( ); }
else if(condition == 9) { b.g3( ); }
}
这道题要求里意思就是说当有不同的condition我们会有不同的g1( )g2( )g3( )的排列组合,如果按照原来的代码,每增加一个condition都要增加else if,如果要修改,还得在茫茫的else if语句中找到目标值对应的condition,现在condition只有三个分支看不出来,但如果每个自然数都有一个呢,源代码的问题就出在这里,源代码中g1( )g2( )g3( )的耦合性太高。
所以我们可以想到,我们申明一个虚基类c,c中安排一个方法,该方法参数是传入一个类B的对象,那么c的子类们重写该方法,就可以访问g1( )g2( )g3( )任意一个函数了,同时c可以有很多子类,子类与子类之间互不影响,这就变相解除了g1( )g2( )g3( )的耦合,实现了真正的排列组合。
然后根据condition的不同生成不同的c的子类,满足了开闭原则,修改也是只修改condition对应的c的子类。
class B {
public:
B(int n):data(n){}
int Data()const { return data; }
void g1();
void g2();
void g3();
private:
const int data;
};
class c {
public:
virtual void f(B& b) = 0;
protected:
int data;
};
class c1 :public c{
virtual void f(B& b) {
b.g1();
}
};
class c5 :public c {
virtual void f(B& b) {
b.g2();
}
};
class c9 :public c {
virtual void f(B& b) {
b.g3();
}
};
class c100:public c {
virtual void f(B& b) {
b.g1();
b.g2();
}
};
void func(B& bb, c* cc) {
cc->f(bb);
}