赞
踩
复用的天敌是改变
复用和复制之间的区别,要有更好的设计和封装的思想,对待问题的时候可以对业务进程分块。减少代码的重复,易于后期的维护
如果没有更细节的面向对象的思想,那么就会一改就改好多代码,如果进行整体上的划分,可以减少代码的修改量,易于维护
例如设计一个windows计算器
1、封装:面对问题需要具体划分业务情况:例如划分计算业务和显示业务等。这样可以更好地区分业务模块,维护不同的业务,前端客户端可以用简单的switch case来完成,后端的计算业务逻辑用try catch防止用户输入出错,增强程序鲁棒性
如果在计算器内部需要扩展一个开根号的业务
不能够直接增加一个case,因为除了其他业务都需要重新编译之外,万一有其他逻辑出错了,那么就会重新编译出错,这样不利于程序的二次开发
2、继承、多态:主要利用类的继承关系,对每个后端算法进行封装(有个operator的计算类,这个类很像hpp的头文件,后面一些继承他的类都会重写里面的函数,这样做到了单算法单类的设计方法)(主要通过虚函数的方法继承母类的一些方法)
之后配合上简单工厂模式,switch case中的case每个就实例化一个想要的类,一个父类指针指向子类对象即可
class C{
public:
void operation (string s){
string s = "";
Operator per = nullptr;
switch(s){
case "+":
per = new Add();
case "-":
per = new Sub();
}
}
}
这样后期要修改代码,只需要修改对应的类就可以了
设计原则有别于设计模式,设计模式是在设计原则的基础上衍生出来的产物
1、依赖倒置原则
高层的应用不应该依赖于底层的应用,而应该依赖于中间的抽象、接口(稳定,因为接口不会时常改变)相当于高层的应用依赖于接口,底层的应用也依赖于接口
抽象不应该依赖于实现细节,实现细节应该依赖于抽象(比如在抽象类中不应该反过来利用子类)
2、开放封闭原则
对扩展开放,对更改封闭。就是对不允许更改,但是可以扩展
增加部分代码,对于设计来说是增加到另外的文件中去的,所以是扩展的形式,如果对源文件进行了修改,那么就需要重写编译、部署等
3、单一职责原则
一个类应该只有一个引起他变化的原因
例如,一个类中有特别多的成员方法,这种情况一般是不正常的情况,因为这个类的变化已经不再是只有一个使他变化的原因了
4、里氏替换原则
子类必须要能够替换他们的父类(is a)
5、接口隔离原则
接口应该小而完备,不会强制要求客户依赖自己不需要的方法
比方说,小这个概念,将不必要公开的函数放在private中,也不必要放在public中让客户可以看见,一旦放在了public里面,接口函数需要被实现或者定义,这样就会膨胀出不需要的代码部分
6、优先使用对象组合而不是类继承
类继承关系也是破坏封装性的做法,主要用于描述类属关系的,不是所有类型都需要继承的关系
class A中放一个class B就是类的组合表达
7、封装变化点
也就是封装变化点,变化点是一侧可变一侧稳定
8、针对接口编程,不是针对实现编程
比如涉及SSD内存,很多品牌的SSD都可以用于插拔在相同的接口上,这样可以很灵活的选择不同厂家的SSD,这里就是对引脚和接口编程,不用多虑SSD里面的结构只要暴露出来的接口相同就可以
例如在类中写了固定的类型,vector那么只允许有int类型被访问,这就是面向实现编程
但是面向接口编程是定义,vector<shape *>这是一个泛型类型,这就是面向接口编程
面向接口编程可以适应很多不同的改动场景
主要描述不同的类对象之间的关系,以及类与接口之间的关系,以及类的一些内部的成员和成员函数
1、空心三角形+虚线标识,实现接口,有个接口类和该接口的接口实现
2、实线+箭头,实现关联关系,比如引用了一些成员变量
3、空心菱形+箭头,实现聚合关系,比如雁群和大雁的关系,A可以包含B,但是B对象不是A对象的一部分(弱拥有关系)
4、实心菱形+箭头,实现合成(组合)关系,比如鸟有翅膀,那么鸟和翅膀就是强连接关系,在线的两端会有两个数字,“1”,"2"主要代表组合关系,"1"代表拥有者,"2"代表被拥有者(强拥有关系)
5、虚线+箭头,实现类之间的依赖关系,例如鸟依赖于水和空气等
单例模式:一个类只有一个实例对象
单例模式设计初衷是为了保证程序的线程安全,以及一个实例在每次构造出来之后再销毁的成本很大的情况,就需要单例模式的设计方法,在线程池和数据连接池中,都有这种模式的映射,实例化一些连接对象和线程对象,不用频繁销毁,每次都可以复用该对象
线程安全:在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
保证线程安全的方法:给共享的资源加把锁,保证每个资源变量每时每刻至多被一个线程占用。
让线程也拥有资源,不用去共享进程中的资源。如:使用threadlocal可以为每个线程维护一个私有的本地变量。
单例模式是主要可以分为:懒汉模式和饿汉模式
懒汉模式:系统运行中,实例并不存在,只有当需要使用的时候才会去创建实例,需要考虑线程安全
饿汉模式:系统一旦运行,就创建实例,当需要的时候,直接调用,这种方式本身是线程安全的(不需要管线程安全的事情)
一般情况下懒汉模式还是用的最多的(如果在项目启动之后就构建,万一对象的构建需要很大的空间和复杂度,那么就会拖累项目的性能)就像开启一个浏览器,如果一开始就把所有的插件全部启动,那么开启的速度就会变慢很多
设计要点
1.构造函数和析构函数为非public类型,目的是禁止外部构造和析构
2.拷贝构造函数和赋值构造函数是私有类型,目的是禁止外部拷贝和赋值,确保实例的唯一性
3.在类中定义一个private静态成员,保存唯一的实例
4.实现一个public的静态方法,用于获取唯一的对象实例
class Singleton{ public: static Singleton getInstance(){ //这里先判断instance这个对象是否构造过,如果没有构造过,就当场构造 //如果构造过就直接返回这个对象 //这里不是线程安全的写法,可能会有AB两个线程同时需要对instance进行创建 if(instance == NULL){ instance = new Singleton(); } return instance; } private: Singleton(){} static Singleton* instance = NULL; //防止外部对其进行拷贝构造 SingleInstance(const SingleInstance &signal); const SingleInstance &operator=(const SingleInstance &signal); }
饿汉模式,是线程安全的,因为是编译的时候就对象进行了构建
class Singleton{
public:
static Singleton getInstance(){
return instance;
}
private:
Singleton(){}
static Singleton* instance = new Singleton();
}
可以使用双检锁的方法对提供访问的public中的函数进行加锁
class Singleton{ public: static Singleton getInstance(){ //如果已经构建了锁,那么就可以直接返回实例对象 if (instance == nullptr) { //这里使用了两个 if 判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁, //避免每次调用 GetInstance的方法都加锁,锁的开销毕竟还是有点大的 //总体上只有需要构建的时候同步,以构建好调用的时候不同步 std::unique_lock<std::mutex> lock(m_Mutex); // 加锁 //这里为什么要加个if,原因是如果A、B两个线程都进入了if条件体 //那么会出现A线程在直接获取锁之后创建了对象,B紧跟着就又创建了对象 //在这里再进行个判断,如果为空再去创建,可以避免上述情况 if (instance == nullptr) { instance = new (std::nothrow) SingleInstance(); } } return instance; } private: Singleton(){} //加入volatile防止指令重排的现象 volatile static Singleton* instance = NULL; //防止外部对其进行拷贝构造 SingleInstance(const SingleInstance &signal); const SingleInstance &operator=(const SingleInstance &signal); }
对于工厂模式的理解,可以认为是一些不同的参数输入进系统之后,会被整合成该类的输出,这样输出都是统一的格式方便处理。
这种思想可以量化为接口的思想,也就是可以重新写某个函数多次,只要传参返回值一致就可以。
// 鞋子抽象类 class Shoes { public: virtual ~Shoes() {} virtual void Show() = 0; }; // 耐克鞋子 class NiKeShoes : public Shoes { public: void Show() { std::cout << "我是耐克球鞋,我的广告语:Just do it" << std::endl; } }; // 阿迪达斯鞋子 class AdidasShoes : public Shoes { public: void Show() { std::cout << "我是阿迪达斯球鞋,我的广告语:Impossible is nothing" << std::endl; } }; // 李宁鞋子 class LiNingShoes : public Shoes { public: void Show() { std::cout << "我是李宁球鞋,我的广告语:Everything is possible" << std::endl; } };
可以通过实现工厂类的方法去实现函数的调用
定义一个总的类,类中的每个case是某种对象的实例化,只要输入了
enum SHOES_TYPE { NIKE, LINING, ADIDAS }; // 总鞋厂 class ShoesFactory { public: // 根据鞋子类型创建对应的鞋子对象 Shoes *CreateShoes(SHOES_TYPE type) { switch (type) { case NIKE: return new NiKeShoes(); break; case LINING: return new LiNingShoes(); break; case ADIDAS: return new AdidasShoes(); break; default: return NULL; break; } } };
int main() { // 构造工厂对象 ShoesFactory shoesFactory; // 从鞋工厂对象创建阿迪达斯鞋对象 Shoes *pNikeShoes = shoesFactory.CreateShoes(NIKE); if (pNikeShoes != NULL) { // 耐克球鞋广告喊起 pNikeShoes->Show(); // 释放资源 delete pNikeShoes; pNikeShoes = NULL; } // 从鞋工厂对象创建阿迪达斯鞋对象 Shoes *pLiNingShoes = shoesFactory.CreateShoes(LINING); if (pLiNingShoes != NULL) { // 李宁球鞋广告喊起 pLiNingShoes->Show(); // 释放资源 delete pLiNingShoes; pLiNingShoes = NULL; } // 从鞋工厂对象创建阿迪达斯鞋对象 Shoes *pAdidasShoes = shoesFactory.CreateShoes(ADIDAS); if (pAdidasShoes != NULL) { // 阿迪达斯球鞋广告喊起 pAdidasShoes->Show(); // 释放资源 delete pAdidasShoes; pAdidasShoes = NULL; } return 0; }
例子借鉴:https://www.cnblogs.com/xiaolincoding/p/11524376.html
最基础的收银软件:
1、商品价格 * 商品数量,再把所有商品价格全部加起来获得一个总和
如果遇到一些打折的情况
1、可以设计下拉选项,对商品的折扣进行选择,这样算到总数就乘上相应的系数就可以了
更好地改进方法是将之前设计的switch case用类的方法封装,再用继承的方法去写
但是如果这样又遇到了一些其他问题呢,比方说满300减100,满300减80这种,这样每次有个新活动,之前没有过,就要写一个类,也不方便智能
改进设计方法
1、现金收费抽象类(父类)
2、正常收费类(将入参直接返回即可)(子类)
3、打折收费类(参数需要输入打多少折,还有父类的收费金额是多少)(子类)
4、返利收费类(满足多少的返利金额就返回多少的值)(子类)
5、现金收费工厂类(用于switch case来判断输入的选项的,这样只要用父类指针指向子类对象,就可以调用好不同的类来收银了)
但是商场有一个折扣需要重写设置,就需要重新编译整个代码,明显不是最好的,这种简单工厂的模式会导致扩展的效率比较低,不适用于后端算法频繁变化的情况,新增一个类型的产品(方法)就会需要改变工厂类中的方法声明,这样破坏了设计原则中的开闭原则,就是对扩展开放,对修改封闭
策略模式,可以定义算法家族,分别封装,相互替换,让算法的变换,不会影响到使用算法的客户
改进简单工厂模式的收银服务
1、设计context类用于维护策略类中的方法和引用,就是相当于初始化传入的具体策略对象,根据策略对象调用不同的算法
2、设计一个策略类,里面定义了不同的策略方法
3、实现策略类中的一些方法接口
4、客户端:由于实例化的时候就会将策略作为入参,传入进context类中,new context(new strategyA())这种(带来的影响就是重复的代码很多)
using System; using System.Net.Configuration; namespace StrategyWithFactory { class Program { static void Main(string[] args) { Strategy strategyContent = null; //伪代码。获取输入算法类型 EStrategy inputType = RequestInput(); if (inputType == EStrategy.A) { new Content(new StrategyA()).ContentInterface(); } else if (inputType == EStrategy.B) { new Content(new StrategyB()).ContentInterface(); } else if (inputType == EStrategy.C) { new Content(new StrategyC()).ContentInterface(); } } } //算法抽象类 abstract class Strategy { public abstract void AlfoeirhmInterface(); } //A算法类 class StrategyA : Strategy { public override void AlfoeirhmInterface() { Console.WriteLine("this is the StrategyA"); } } //B算法类 class StrategyB : Strategy { public override void AlfoeirhmInterface() { Console.WriteLine("this is the StrategyB"); } } //B算法类 class StrategyC : Strategy { public override void AlfoeirhmInterface() { Console.WriteLine("this is the StrategyC"); } } //上下文类 class Content { private readonly Strategy _strategy; public Content(Strategy strategy) { _strategy = strategy; } public void ContentInterface() { _strategy.AlfoeirhmInterface(); } } //算法枚举 enum EStrategy { A = 1, B = 2, C = 3 } }
策略类的设计方法主要借鉴于简单工厂的继承与多态的方法,对一个方法进行多次的重写
简单工厂模式和策略模式的结合
目的:首先,上面的main函数如果相当于客户端来说的话,每次修改个算法就要对客户端重写修改,添加一个else if,这不符合设计规则
这时候如果将if else的部分代码下沉到服务端,那么就算修改也只要修改服务端的部分代码,涉及不到前端的修改
using System; namespace StrategyWithFactory { class Program { static void Main(string[] args) { //伪代码。获取输入算法类型 EStrategy inputType = RequestInput(); new Content(inputType).ContentInterface(); } } //上下文类 class Content { private readonly Strategy _strategy; public Content(EStrategy eStrategy) { _strategy = CreateFactory(eStrategy); } public Strategy CreateFactory(EStrategy eStrategy) { Strategy strategy = null; switch (eStrategy) { case EStrategy.A: strategy = new StrategyA(); break; case EStrategy.B: strategy = new StrategyB(); break; case EStrategy.C: strategy = new StrategyC(); break; } return strategy; } public void ContentInterface() { _strategy.AlfoeirhmInterface(); } } }
总体上来说,策略模式是用一个类来调用所有其他可调用的类,有点类似epoll的思路,但是epoll更加侧重于观察者模式。但是也是一个fd来管理所有监听的fd,减少了算法间的耦合
对于算法需求改动模式下,还是会存在成本的,但是后面的反射模式可以很好地解决此类问题
每个类只有一个能引起其变化的原因
在涉及代码的时候,需要考虑到不同的业务模块分离的部分。这样才能容易扩展,维护,复用,灵活多样
这里一般是通过多态的方法实现的,相当于接口相同,但是每个类对这个接口的实现是不同的,这样就可以各司其职,而且接口类不需要时常改变
对于软件实体(包括类,对象等)可扩展,但是不可修改
在计算机中,主板一般不会轻易更换,但是可以跟换上面的内存或硬盘什么的,这种是PC的易插拔的概念
在面向对象的编程思想中,我们也需要有类似的思想,就是高内聚,低耦合,能够将上面的类进行动态插拔
CPU的引脚就相当于接口,这些接口只要与主板上的接口相符合,就算CPU里面的设计再复杂,也不需要管,因为出来的接口都是统一的
所以面向对象最重要的还是能够将目标函数封装成接口的样式,满足生产的需要
所以换一个内存条可以涉及到很多设计模式
1、内存坏了只用换内存,不用换CPU:单一职责
2、内存能够添加,但是不能过多添加,因为接口就两个:开闭原则
所谓依赖倒转的意思:要面向接口编程,不要对实现编程。
意思就是比如计算机的一些硬件,他们都需要面向接口设计(或者说面向协议设计),如果单纯的面向功能设计,那么就只能intel用intel的主板,不能用其他厂商的主板了
主要对于高层和底层的代码,改变其中一个,不能影响到其他的模块,他们都依赖于中间层的接口函数,这样能够很好的避免其中一个层出现了问题,影响到其他层的设计
子类必须完全替换父类,非严格意义上就是子类必须完全替换父类的公共属性
例如,企鹅属于鸟这个说法在生物学上是对的,但是在面向对象的说法上,并非正确,因为对于企鹅来说,无法飞行,所以不能继承鸟这个类的飞行属性
也就是父类有的子类全都有,这样才能够让开闭原则成为可能,不需要修改,只要扩展就行
利用纯C语言开发的程序就是聚合度很高的,例如收音机这种机器,高耦合,每个部件之间都相互依赖,所以修起来就很困难
例如对于人这个类对象来说,需要一些成员函数,例如衣着,鞋子,帽子等
代码结构应该是服饰类对象,服饰类对象是个接口类,一些子类继承这个服饰类,其中包括对一些父类成员的实现。人这个类调用服饰类,实现可扩展的功能
但是这样的话在调用的时候,一般都会在client端直接调用服饰类对象里面的一些方法,不仅这个过程暴露给了前端,而且如果穿衣服需要一定的顺序的时候,就需要前端进行操作了,不符合设计逻辑
装饰器模式:动态的给一个对象增加一些额外的功能,但是总体上比增加一个子类的方法要更灵活
主要的结构是
1、一个父类,需要改写的父类(具体函数实现的抽象类)
2、子类,子类是对父类函数的实现
3、装饰类,这个类会实例化一个具体的类对象,然后需要重写的函数就相当于用类对象中的方法来引用了(a.get())这里的实例化的类对象可以作为参数传入,一般来说都是第2个那个那边的子类对象
4、装饰器的子类是对装饰器中的类对象的方法进行改写,相当于在外面套了一层壳子
client端:先通过子类对象实例化父类对象,然后用装饰器的子类对象来对这个类对象进行包装,一层层包装下来。
这样的好处,就是在包装的过程中,每个装饰器对象就只需要关心自己的功能,不需要关心如何被添加进来的
那么对于之前写的着装的类对象之间的包装来说,我们应该这样重新设计
1、人的类(person)人的类中设置一个装扮类作为虚函数
2、服装类(这里的服装类就是装饰类父类了)传入一个person类对象,然后通过他的子类对函数进行改写
3、子类中都是继承服装类,然后对其中的虚函数进行重写
client端调用的时候,需要套用起来调用
Person p = new Person("A");
S pqx = new S();
B kk = new B();
T dtx = new T();
pqx.Decorate(p);
kk.Decorate(pqx);
dtx.Decorate(kk);
dtx.show();
实际上都是用了p这一个实例,因为继承的时候都是继承于同一个实例化对象,所以都是对同一对象进行改动
再举个例子
奶茶里面有两种不同的口味,一个是草莓,一个是芒果,但是都可以给他们个性的定制是否要珍珠,是否要果冻,所以这时候我们需要尽量避免使用继承的方法,而是要使用组合的方法
原因:
继承是 是一个(is a) 关系,而组合是 有一个(has a) 关系
1、继承可能会导致类的无限膨胀。
2、继承关系是一种耦合度非常高的关系,因为一旦父类行为发生变化,子类也将受到影响。
3、而组合关系耦合度就没有那么高,尤其在组合“接口”时候,程序会变得更加灵活。
这时候不同口味的奶茶都是继承奶茶类,而其他的辅助材料,例如果冻等,是继承到一个装饰类上的,只要看客户定制的奶茶是否需要加入果冻等辅助材料来装饰就好了
意思是两个类本省应该相互没有对方的实例,或者说无法使用对方的实例,但是在经过某个类后,相互之间就可以访问相互的实例了
比如说有个类需要对方送一些礼物,那么在送的时候,类间相互不认识
结构:
1、送东西接口类
2、追求者类:实现送东西的一些方法,是送东西类的子类
3、代理类:其中包含有追求者类的实例对象,传参是追求的女生的类,这个类传入进来,实例化追求者类对象,然后可以调用一些追求者类中的函数,可能是一些给追求的女生赋值的操作
代理模式的应用:
1、远程代理:代理服务器
2、虚拟代理:可能访问一些文件需要很多时间,可能会存在CDN局部缓存服务器,来代替加载这些内容,或者存放在本地,下次打开的时候就会很快调用
3、安全代理
4、智能指引:代理会在调用真实对象的时候做一些其他的事情
和最开始设计的计算器功能一样,都是使用简单工厂类实现的,对于这样的设计,一般来说的结构是
1、工厂接口类作为父类
2、好多不同的工厂子类,例如加法类,减法类
3、客户端,通过父类指针指向子类对象的方法实现对子类工厂的调用
工厂方法模式的方法就是返回一个类对象,而不是像简单工厂一样用类的方法继承,而是直接返回类对象,这样的话所有的业务逻辑处理全部都在对应的子类里面了,上升不到客户端的层面上,所以,有利于避免客户端使用switch case的现象,能够区分度高。
只需要给一个符号,就可以动态的加载返回需要的类对象,直接调用
简单工厂和工厂模式之间的设计区别
在利用简单工厂模式的设计下,在调用的时候会存在很多的重复代码,所以客户端在调用的时候不符合正常的设计模式
但是工厂模式在使用的时候,分为
1、目标类:这个类是所有类的公共类,也就是后面工厂类的子类所返回的类对象
2、工厂类(这个类是所有后面需要的类的父类)其中有个成员函数,这个成员函数就是子类需要重写的函数,返回值是目标类
3、子类:工厂类的子类,他在重写工厂类中的成员函数的时候就会返回目标类的对象,这样就不需要在客户端再实例化很多重复的类对象,避免了很多重复的代码,将所有可能的代码都放到了后端上来区分
类似发布订阅的消息机制,通过某个被观察者发送出了信号,使得观察者做出反应。是一种一对多的形式,
例如老板不允许员工炒股,但是员工贿赂了前台小姐,当老板出门的时候,就炒股,回来的时候得到了小姐的通知就立马关闭页面
设计结构
1、前台类:挂一个链表,上面挂需要通知的同事名字;增加同事的函数(需要增加的同事就挂上链表),通知函数(用于通知所有的成员改变状态),前台状态函数
2、看股票的同事类:前台的状态,在得到了前台的通知之后,改变类内部的状态
3、客户端:实例化一个前台,实例化一些同事,将这些同事的实例加入前台类的链表中,当前台利用发布函数通知的时候,链表上的实例对象改变类内的状态
这么写有个问题,就是相互之间的耦合太大,前台类中需要同事的实例,同事类中需要前台的状态,相互之间的关联性太大了;比如说有的同事想看NBA呢,就需要重新写里面的函数了
总体上来说,类B不再关心是类A还是类C、类D等等需要监听,类B只知道有类需要被通知消息,他把这些类叫做观察者,于是类B持有一个“观察者队列”,并提供一个注册的方法。类A继承观察者接口,在需要的时候注册自己,并实现接口中被通知的方法。类B在自身变化时依次通知队列中的所有人
重新设计
1、增加抽象的观察者:新增抽象观察者类,这个类是由状态和成员函数组成,成员函数是一个抽象的函数,用于被其子类重写
2、不同的同事的具体类是抽象的观察者类的子类:是对抽象类的实现,不同的同事可以对抽象类有不同的实践
3、前台的抽象类:防止需要通知的人变化了,不再是前台,这样改写就需要重新写一下代码了,不符合对应的设计原则
4、前台类的具体实现:就是具体的实现,和之前的设计结构相似
当然观察者模式也有一定的设计缺点
1、抽象被观察者依旧依赖于抽象观察者
2、具体的观察者,通知方法被固定了,如果通知的手段需要进行变化的时候,就需要重写代码
3、需要注意观察者与被观察者之间不能有循环依赖,否则容易导致程序卡住
4、如果有太多的观察者,则通知每一个观察者需要耗费大量时间
委托:
delegate字段定义委托,相当于一个委托类型定义
事件:
event字段定义事件,相当于一个委托类型的回调函数对象,不使用event字段,依然可以定义delegate委托类型的对象,但这是不安全的,被event字段标记的属性对象,只能通过±来添加或移除注册函数,而不能被赋值
现在有个类作为一个模型,这个类是一个抽象类。他会派生一些子类,但是可能有一些子类只有在未来的某个时间才能创建出来,所以不可能在设计初期就能够想到很多类的方法,让所有未来可能出现的子类继承他。
解决方法:让所有的子类都自己创建出一个自己的类对象,然后让父类能够看见,这样就可以使用起来这些子类了(return 自己的类指针)子类中存在一个静态的类属于类的私有对象,然后需要将类的构造函数放入私有中(包括拷贝构造,赋值构造函数等)防止被类外调用,借用私有的构造函数,将自己的类对象挂到父类的内存空间中去,addprototype(this),将自己的类本身上传上去《父类中,addprototype是将子类的对象放入父类的一个容器中去,可以是链表也可以是vector》
这里有个小问题:就是为什么不直接将某个函数编程static的函数,这样就可以直接访问了,原因就是调用某个类的某个静态函数,必须要知道类的名字,之前的addprototype函数的步骤,就是将类的对象上传给父类,让父类可以通过类的名字调用里面的函数
利用重构代码的方法,将原本的代码转向更好的设计模式
1、静态转动态
2、早绑定转晚绑定
3、继承的关系转为组合的关系
4、编译时依赖转为运行时依赖
5、紧耦合转为松耦合,可改动空间大
主要通过晚绑定来实现松耦合
主要通过以下方法来实现
1、模板方法
2、策略模式
3、观察者模式
模板方法
library:框架开发人员会设计一些模板和函数,最终组成一个库,但是可能会存在一些虚函数,让应用开发人员实现
application:应用开发人员在继承了框架开发人员实现的库之后,会具体实现库中的虚函数,创建一个多态指针(父类指针指向子类对象)然后调用里面的函数
结构化设计的方法是
application的模块规定主流程,也就是函数的调用流程写在application之中(早绑定)
面向对象的设计方法是
library的模块规定主流程,也就是函数的调用顺序写在library里面(晚绑定)(这里是父类指针指向子类对象,也就是库调用应用程序)
library一般是写的最早的,application写的晚,一个晚的调用早的东西是早绑定;反之,是晚绑定,也就是library调用application
总体上:定义算法的骨架(就是函数的调用流程)将一些步骤延迟到子类中实现,也就是支持子类的变化
揭示了设计模式的原则:稳定中含有变化,也就是稳定要实现出来,变化的要写虚函数、
适用场景:有个稳定的流程骨架可以用(因为如果骨架需要变化的话,相当于流程需要变化,也就是封装在库中的东西需要变化,这不符合设计原则)
策略模式
通过设计一个抽象类(记得将类的析构函数变成虚函数)通过继承的方法,动态的改变、添加、修改一些策略
设计方法:
1、抽象类:用于作为不同方法的接口
2、一些子类:用于继承抽象类,能够重写接口函数
3、调用类:实例化一个抽象类对象,再实力话一个参数,用于传参,然后看需要哪个方法,就传递什么参数进去,就会自动转向对应的类实现
4、主函数:会调用switch case来调用相关的代码(这里从设计原则上说是不合理的,每次只要增加一个策略,那么就需要在main函数里面多写一个case。一般来说可以通过加switch case下沉到代码段来封装,可以将即将用到的策略传递给服务端,让服务端实例化并判断用哪个子类switch case(不便于修改代码,即要修改前端也需要修改后端))
关键的点在于,支持运行时的多态的调用并灵活变换,
也就是相互之间的依赖关系,一对多的一些问题
例如先抛出这样的一个问题,想要在程序中设置一个可以看进度的控件
class filespliter{
string stringPath;
progressBar *m_bar;//这里的m_bar是相当于通知
public:
filespliter(string str, progressBar *bar):stringPath(str),m_bar(bar){
}
void splite(){
for(int i = 0; i < stringPath.size(); ++i){
if(m_bar != nullptr){
m_bar->setvalue((i+1)/stringPath.size());
}
}
};
假如说不再需要进度条了而是要以百分比的形式改写前端的反馈,那么这个框架就不灵活了,是静态的了。
可以通过父子类的方法改写当前的进度条指针,以抽象类的方法改写相关的控件
class iprogress{ public: virtual void doprogress(float input){} = 0;//具体如何实现进度条 virtual ~iprogress(){}; }; class filespliter{ string stringPath; iprogress *m_bar;//这里的m_bar是相当于通知 public: filespliter(string str, iprogress *bar):stringPath(str),m_bar(bar){ } void splite(){ for(int i = 0; i < stringPath.size(); ++i){ if(m_bar != nullptr){ float input = (i+1)/stringPath.size(); m_bar->doprogress(input); } } };
涉及到多个观察者的话,或者是多个函数在某个信号到来的时候会响应某写函数的话,还需要继续扩展,可以以挂链表list或者列表vector存放的方式,统一进行发布
(绑定死就是紧耦合,可以动态改变就是松耦合)
单一职责原则可以防止子类急速膨胀,也就是会导致类中的成员函数或者成员类越来越多
1、装饰器模式
2、桥接模式
所谓装饰器模式,就是在原本的类上增加拓展一些业务,能够实现,一层实例上在实现一层实例,像是穿衣服的感觉
以设计一个输入输出流的类作为例子
1、父类,Stream,用于子类的抽象类
2、子类,filestream、memorystream等一系列输入输出流类的实现
3、在此基础上的业务的拓展,增加一些其他的类对象继承于子类
这里3的位置上使用了继承的方法,如果需要对3的类中做一些个性化的设计,比方说,加入一些加密方法或者验证方法,那么代码就会膨胀的比较快(这里的加密方法是每种2中的子类都可以复用的)明显,这样会存在很多的重复代码的现象。
这时候,就可以想到了以组合代替继承的方法,既然除了一些不同的方法的实现以外其他都相同,那么在设计上就不需要继承了,在3中的每个类里面定义个2中的子类,filestream * stream等,这样可以避免继承带来的麻烦,与此同时每个子类又继承于相同的父类,所以这里完全可以直接定义一个父类,Stream * stream以父类指针指向子类对象的方式调用不同的子类。(这里就实现了晚绑定,在编译后绑定,也就是编译的时候,大家都一样,绑定实现的时候不同)
client端,先实例化一个父类,再将不同的子类以拷贝构造函数的方法传递进去
设计中最重要的就是解耦和以组合的方式代替继承
这时候我们如果继续需要拓展,可能出现的情况是一部分子类需要父类,一部分子类不需要父类。
解决方法:为需要父类的子类设计一个中间类,这个中间类里Stream * stream,一个父类,只用来给后续的扩展进行调用,这就是装饰器类
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。