• 认真地记录技术中遇到的坑!

面向对象的特征和设计原则

设计模式 Moxun 1年前 (2018-03-26) 377次浏览 0个评论

面向对象三大特征

封装

封装在程序中的具体变现为类。利用抽象数据类型(ADT)对数据信息和操作进行打包,使其变成一个不可分割的整体,封装的特点体现在对外提供接口(方法调用),隐藏实现细节。

封装的好处:
1. 隐藏数据信息,避免恶意修改带来的安全性问题(依赖于访问权限)
1. 封装之后,可供多处调用,减少程序的耦合度
1. 类内部的结构可以自由更改,而不会影响其它代码

在C++中封装依赖于抽象,所谓抽象即指对具体问题(对象)进行概括,抽象出一类公共性质并加以描述的过程。抽象包含数据抽象和行为抽象两方面的内容。封装就是把经过抽象得来的数据和行为组合成一个有机整体。

继承

从程序设计的角度来讲,继承是指让某个类型的对象可以具有另一个类型的功能(属性和方法)的方法。从实现上来说,我们需要找到目标类共有的特性和行为,然后将这些公共特性和行为抽象成一个新的ADT,供其它类去继承它,以便获取这个公共类的属性和方法,我们把这个公共类称为基类,而通过继承基类得来的类,我们称其为子类或者派生类。派生类继承了基类除构造函数和析构函数之外的所有属性和方法。

例如:人是一个具体的对象,我们对人进行抽象后发现人可以分为两类:男人和女人,男人和女人都有姓名、年龄、出生年月,行为上都有吃喝拉撒的公共行为,那么我们把这些公共特性和行为抽象出来构成一个新的类:人类,男人类和女人类通过继承人类,就可以获得这些人类的功能。

继承这个概念从根本上来说为表达类之间的从属关系而生的,继承的优点是:
1. 在局部上实现了代码的高复用性
1. 使代码层次机构更清晰

注:关于C++继承的基本知识我们在其它内容中讨论

多态

多态是基于继承体系的。运行时多态的三大必要条件:
1. 要有继承体系结构
1. 要有方法的重写(继承中的一些类必须具有和基类中的虚函数同名的成员函数)
1. 父类引用指向子类对象(对父类中定义的方法,如果子类重写了该方法那么指向子类对象父类类型引用将调用子类中的该方法)

运行时多态的实现依赖于虚函数机制,当某个类声明了虚函数时,编译器将位该类对象安插一个虚函数表指针,并为该类设置一张唯一的虚函数表,虚函数表中存放的是该类虚函数地址,在运行时通过虚函数表指针和虚函数表去确定该类虚函数的真正实现。运行时多态是发生在运行期间的。

编译期多态:

对模板参数而言,多态是通过模板实例化和函数重载解析实现的,以不同的模板参数实例化导致调用不同的函数,这就是编译期多态,实现编译期多态的类之间并不需要有继承体系,它们之间可以没有任何关系,但约束是它们必须具有相同的隐式接口。代码示例:

class Animal
{
public :
    void shout() { cout << "发出动物的叫声" << endl; };
};
class Dog
{
public:
     void shout(){ cout << "汪汪!"<<endl; }
};
class Cat
{
public:
     void shout(){ cout << "喵喵~"<<endl; }
};
class Bird
{
public:
     void shout(){ cout << "叽喳!"<<endl; }
};
template <typename T>
void  animalShout(T & t)
{
    t.shout();
}
int main()
{
    Animal anim;
    Dog dog;
    Cat cat;
    Bird bird;

    animalShout(anim);
    animalShout(dog);
    animalShout(cat);
    animalShout(bird);

    getchar();
}

在编译之前,函数模板t.shout()调用的是哪个接口并不确定,在编译期间,编译器推断出模板参数,因此确定调用shout()的是哪个具体类型的接口,不同的推断结果调用不同的函数,这就是编译时多态。

运行时多态的优缺点
优点:
1. OO设计中的重要特性,对客观世界的直观认识
1. 能够处理同一继承体系下的异质类集合(其实说的就是子类指针可以给父类指针赋值)
缺点:
1. 运行期间进行虚函数绑定,提高了程序运行开销
1. 庞大的类继承层次,对接口的修改易影响类继承层次
1. 虚函数在运行期间确定,编译器无法对虚函数进行优化
1. 虚函数表指针增大了对象的体积,类也多了一张虚函数表(当然这是必要的资源消耗)

编译期多态的优缺点:
优点:
1. 基于此,可以进行泛型编程
1. 在编译器完成多态,提高运行效率
1. 具有很强的适配性与松耦合性,对于特殊类型可由模板偏特化、全特化来处理
缺点:
1. 程序可读性低,给代码阅读带来困难
1. 无法实现模板的分离编译,当工程量很大时,编译花费的时间不能忽略
1. 无法处理异质类集合

面向对象的设计原则

个人认为,所谓五大设计原则,在理解其内涵的时候,我们应该从代码复用的角度去理解,应该站在软件设计、功能扩展、版本迭代等宏观角度去看待这个问题,而不是局限于工程本身(微观层次)去理解它们。

单一职责原则

单一职责原则(SRP:):就一个类而言,应该仅有一个引起它变化的原因。

就是尽可能使类的功能单一,如果一个类承担过多的职责,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其它职责的能力,这种耦合会导致脆弱的设计,当变化发生时,设计会遭到意想不到的破坏。就说在进行软件设计时,要发现职责,并把职责相互分离。如果你能想到多于一个的原因去改变一个类,那么这个类就具有多于一个的职责。

开放-封闭原则

开放-封闭原则(OCD:The Opened-Closeed Principle),是说软件实体(类、模块、函数等等)应该对扩展开放,对修改封闭。

开放-封闭原则可以站在代码的可维护性角度考虑,即软件需求是随时可能发生变化的,但是软件在这种情况下应该是相对容易修改并且能够保持相稳定的,从而使得软件可以在第一个版本的基础上不断推出新版本。面对需求的变化,对程序的改动应该是通过增加新代码进行的,而不是修改先有的代码。

具体做法是:在最初编写代码时,假设变化不会发生,当变化发生时,我们就创建抽象来隔离以后发生的同类变化,这肯恩需要用都继承、多态等具体措施来进行隔离。因此,最好是在开发工作展开不久后就可以知道可能发生的变化,查明可能发生的变化所等待的时间越长,要创建正确的抽象就越困难。

面向对象的好处:可维护、可扩展、可复用、灵活性好。开发人员应该仅对呈现出频繁变化的那些部分作出抽象。

依赖倒转原则

依赖倒转原则(DIP),简单说就是要面向接口编程而非面向实现编程。它包含两个方面的内容:
A.高层模块不应该依赖低层模块。两个都应该依赖抽象(接口或者抽象类)
B.抽象不应该依赖细节,细节应该依赖抽象。

只要接口是稳定的,那么任何一个更改都不用担心其它受到影响。

里氏代换原则

里氏代换原则(LSP):子类型必须能够替换掉它的父类型。就是说,一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别,也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化。

迪米特原则

迪米特原则(LOD):也叫最少知识原则。如果两个类不必直接通信,那么这两个类就不应该发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。它强调了类之间的松耦合,在类的结构设计上,每一个类都应当尽量降低成员的访问权限,也就是说,一个类包装好自己的private状态,不需要让别的类知道的字段或者行为就不要公开。类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。

接口分离原则

(注:很多博客上的五大原则,不包含迪米特原则,而包含接口分离原则)

接口分离原则(ISP):不要强迫客户依赖于他们不需要的方法,应该使用接口将两者隔离。

在具体的编程过程中一个类对另一个类的依赖性应该当是建立在最小的接口上的。如果客户端只需要某一些方法的话,那么就应该向客户端提供这些需要的方法,而不是提供不需要的方法。这个的解决方法主要是通过适配器来完成的。

对于五大设计原则的具体设计示例参见博客:
https://blog.csdn.net/cloud323/article/details/76170210

内容参考:
https://www.cnblogs.com/QG-whz/p/5132745.html


喜欢 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址