05/06/2020
05/08/2020 :增加总结
C++模拟鸭子
父子继承关系1
- 假设父类所有的鸭子特点
- 不抽象:相同的行为
- 所有鸭子都会叫,叫的声音都一样 (quack)
- 所有鸭子都会游泳,游泳姿势都一样 (swim)
- 抽象:没有相同的行为(virtual)
- 所有鸭子都有外观这个特点,但每一个鸭子外观不同(diaplay)
- 不抽象:相同的行为
- 子类需要重写外观这个特点
//Duck 基类
class Duck
{
public:
void quack(); //声音
void swim();
virtual void display();
};
//子类鸭子类
class GreenHeadDuck:public Duck
{
public:
void display()override(); //显示头的颜色是绿色的
};
class RedHeadDuck:public Duck
{
public:
void display()override(); //红色的头
};
//...其他头部颜色的鸭子
简单完成,如何扩展
如何扩展飞行行为。思考:
- 每一只鸭子都会飞吗?
- 如果都会飞,它们飞行的姿态都一样吗?
问题一:飞行作为父类的抽象类
//Duck 基类
class Duck
{
public:
//... 之前所有的其他行为不变
void fly(); // 新增飞行类
};
缺点
- 如果父类加入fly,所有子类都会飞行,但是无生命的鸭子并不会飞行。对代码所做的局部修改,影响层面可不只是局部的。-- 开放-封闭原则。
- 对于一个设计好的父子结构,在父类增加新的行为,一定要小心,最好禁止这个行为。
问题二:使用抽象的飞行(virtual)
//父类
class Duck
{
public:
//... 之前所有的其他行为不变
virtual void fly(); // 新增飞行类
};
//子类鸭子类
class GreenHeadDuck:public Duck
{
public:
void display()override(); //显示头的颜色是绿色的
};
class RedHeadDuck:public Duck
{
public:
void display()override(); //红色的头
};
//重点:抽象的飞行--覆盖原有父类会有什么问题
class GreenHeadToyDuck:public Duck //玩具鸭
{
public:
void display()override(); //红色的头
void fly()overdie{
//空的行为,什么都不做,作为不会飞的鸭子
}
};
- 覆盖掉父类的方法将会导致程序员无法记住所有的实现,有些需要写空,有些需要实现,总归会有遗漏的。
- 突然发现quack行为也需要改变,因为有些鸭子也不会叫。
缺点:
利用继承来提供鸭子的行为,会导致什么缺点:
- 很难知道所有鸭子的全部行为
- 改变会牵一发动全身,造成其他鸭子不想要的改变
- 代码在多个子类中重复
- 运行时的行为不容易改变
继承复用:
只有当子类可以替换掉父类,软件单位的功能不受到影响时,父类才能真正被复用,而子类也能够在父类的基础上增加新的行为。2
里氏代换原则:
子类型必须能够替换掉它们的父类型。
例子1:
现实生活中,企鹅是一个特殊的掉,尽管不会飞。但是在面向对象设计时候,一个是鸟类,一个企鹅类,如果鸟是可以飞的,企鹅不会飞,企鹅可以继承鸟这个类吗? – 不能。有时候脱离现实,也源于现实。
问题三:接口使用
- C++使用抽象类,里面有纯虚函数。抽象类无法生成对象,只能被继承。
- 继承过来的纯虚函数必须被定义,否则继续声明为纯虚函数。
//Duck 基类
class Duck
{
public:
//void quack(); //叫声不一样,分离它
void swim();
virtual void display();
};
//抽象类,行为用来被继承
class Flyable
{
public:
virtual void fly() = 0;
};
//突然发现,鸭子的叫声也是不一样,不是所有鸭子都会叫
class Quackable
{
public:
virtual void quack() = 0;
};
//多继承完成接口使用
class RedHeadDuck:public Duck,public Flyable,public Quackable
{
public:
//...
void fly()override{/*完成细节*/} //继承过来的接口,必须定义!
void Quack()override{/*完成细节*/}
};
//不需要继承接口的玩具鸭
class GreenHeadToyDuck:public Duck
{
public:
void display()override{/*完成细节*/}
};
缺点:
虽然完成了扩展,但是子类依然需要去实现它们的细节,但是一旦有很多鸭子子类,需要覆盖的工程量很大。同时代码无法复用。(即重复使用)
设计原则1
- 找出应用中可能需要的变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
- 如果每次新的需求一来,都会使某方面的代码发生变化,那么你就可以确定,这部分的代码需要被抽出来,和其他稳定的代码有所区分。就如上述的quack的行为一样。
- 系统中的某部分改变不会影响其他部分。
分开变化和不会变化部分
变化:
- fly和quack 随着鸭子的不同而改变
- 对变化的行为设立新类
设计原则2
针对接口(即抽象类)编程,而不是针对实现编程。
- 行为类,区别于鸭子类,鸭子不需要去实现fly和quack的接口
例子:飞行行为
//fly 行为接口
class FlyBehavior
{
public:
virtual void fly() = 0;
};
//继承飞行接口
class FlyWithWings:public FlyBehavior
{
public:
void fly()override{} //实现鸭子飞行行为
};
class FlyNoWay:public FlyBehavior
{
public:
void fly()override{} //不会飞
};
优点:
- 针对接口编程:关键就在多态,利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为,不会被绑死在超类型的行为上。
例子1:针对实现编程
GreenHeadDuck* gD = new GreenHeadDuck;
gD->display();
例子2:针对接口/超类型编程:不需要知道子类的类型,关心如何正确进行display的动作就够了。
Duck* d = new GreenHeadDuck; //多态的体现
d->display();
- 完成复用,即其他类也能使用fly和quack的行为类。与鸭子类无关了。完成里氏代换原则。
如何整合鸭子的行为
- 继承还是拥有
- 加入两个实例变量,flyBehavior 和 quackBehavior
class FlyBehavior;
class QuackBehavior;
class Duck
{
public:
void swim();
virtual void display();
//取代原有的fly和quack
void performFly()
{
mFly->fly();
}
void performQuack()
{
mQuack->quack();
}
protected:
//多态体现
FlyBehavior* mFly;
QuackBehavior* mQuack;
};
- 并不在乎quackBehavior接口对象到底是什么,我们只关心该对象如何进行fly和quack。
子类的使用
class GreenHeadDuck:public Duck
{
public:
GreenHeadDuck()
{
mFly = new FlyWithWings; //初始化你想要的具体类型
mQuack = new Quack;
}
};
缺点
- 我们不能对具体实现编程,在构造器中我们实现了具体的类。
- 初始化实例变量的做法还不够弹性。
动态设定行为
如何实现在运行时,通过多态的魔力动态地给它指定不同QuackBehavior实现类。
- 使用set方法,动态设置
```cpp
class FlyBehavior;
class QuackBehavior;
class Duck
{
public:
//...
//建议使用智能指针,不然需要delete
void setFlyBehavior(FlyBehavior* fb)
{
mFly = fb;
}
void setQuackBehavior(QuackBehavior* q)
{
mQuack = q;
}
protected:
//多态体现
FlyBehavior* mFly;
QuackBehavior* mQuack;
};
//main
Duck* d= new GreenHeadDuck;
d->performFly();
d->setFlyBehavior(new FlyNoWay);
d->performFly();
总结
合成(组合)关系(Composition)3
鸭子拥有飞行或者呱呱叫这个行为,说明了一种合成关系。是一种强拥有的关系。体现了严格的部分和整体关系,部分和整体的生命周期一样。当我们初始化鸭子的时候,同时也初始化了飞行和叫声。这体现了相同的生命周期。
其他例子
鸟和翅膀的关系,武器和子弹的关系
聚合关系
聚合表示一种弱的拥有关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。
例子
- 大雁和大燕群的关系,使用到了一组容器。
- 商场促销策略4
策略模式(Strategy)4
策略模式是一种定义一系列算法,在这里鸭子飞行类就定义了一套算法,它们都是为了完成飞行这个相同的任务,只是实现不同,但可以以相同的方式调用所有算法,减少了鸭子和飞行之间的耦合。
优点
- 简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试
应用
不同时间应用不同的业务规则,可以考虑策略模式处理变化的可能性。
如何减少对if语句的依赖
当不同的行为堆砌在一个类中,就很难避免使用条件语句选择合适的行为。将这些行为封装在一个个独立的strategy类中,可以在使用这些行为的类中消除条件语句。所以策略封装了变化。
问题
但是客户端对象面临了选择的压力。