【设计模式】面向对象设计

来源:互联网 发布:抄袭调色盘软件 编辑:程序博客网 时间:2024/05/07 21:20

转载请注明出处:http://blog.csdn.net/u012250875/article/details/78839459

一、什么是面向对象设计

  面向对象设计简称OOD(Object–Oriented Design),在使用面向对象的思想进行编程的过程中,我们需要将事物抽象成接口,类。那么该而如何去抽象,如何去组织这些类(接口)之间的关系,如何让类之间良好协作,让整个结构具有好的维护性呢?这就要面向对象设计要解决的问题。

二、OOD常见概念

OOD概念有很多,下面对这些概念分别作出解释

抽象与具体

鸟类与一只鸟:当我们说到鸟类这个概念的时候,就有了一个大致印象:有翅膀,有羽毛,卵生,脊椎动物。这就就是一个抽象概念,鸟类代表所有具有以上这些特点的动物的总称,并不特指某一只鸟。而身边飞过某一只鸟时,这只鸟则是鸟类的一个具体存在。鸟类与一只鸟的关系就是抽象与具体

面向对象中的抽象与具体:面向对象中类与实例,元类与类的关系也是抽象与具体的关系

耦合与内聚

  两个事物之间存在联系则称为耦合;一个事物内部各块间联系称为内聚。面向对象编程中,我们都希望模块自己高内聚,模块之间低耦合,即每个模块装好自己的东西(数据),做好自己的事(功能);各模块间依赖尽量要少。这就是编程中常说的高内聚,低耦合。

  高内聚,低耦合的好处是什么呢?最好的例子是电脑与收音机的例子。电脑要比收音机高端的多,功能强大的多,原理也要复杂的多,但是我们能很容易的组装出电脑,却不能组装出收音机。电脑是很典型的高内聚,低耦合,每个模块内部内聚性强,单个模块内部很复杂,但被很好的封装起来,我们全然不知。而模块之间连接则十分简单,通过插槽(接口)很轻松关联起来。因此电脑出问题的时候,大多数人自己就能修理,更换相应的模块即可。

常见的耦合关系有哪些呢?主要有以下四个:

依赖:表示一个事物依赖另一个事物,如开车这个动作依赖于一辆车,这个车可以是宝马,也可以是奔驰,只要是辆车即可;一般在代码中体现为以局部变量,方法参数;

关联:表示两个事物间存在联系,可能是单向的,也可能是双向的联系。如商品和订单的关系,订单和配送地址,省市县的关系。一般在代码中体现为成员变量;

聚合:一个事物由其他事物聚合而成,但其他事物可以单独存在。如雁群和大雁的关系。一般在代码中体现为成员变量,一般使用set注入;

组合:一个事物由其他事物组合而成,但其他事物不能单独存在。如人和心脏,胳膊的关系,人由这些器官构成,不能离开这些器官,同时这些器官不能单独存在,离开人体没有任何意义;一般在代码中体现为成员变量,使用构造器注入或在构造器中实例化;

耦合度:依赖<关联<聚合<组合
依赖,关联,聚合,组合都是关联关系,依赖是弱关联,聚合是较强关联,组合是强关联

面向对象三大特性

面向对象的基础就是三大特性,封装,继承,多态

封装:面向对象编程中的封装就是隐藏。那到底隐藏了什么?程序=数据+算法,这里封装的就是数据(属性)和算法(功能实现细节)。因此通过封装能屏蔽类中方法的复杂的细节,通过封装来控制属性(数据)的访问权限,通过封装可以使类内部自成一体,而外部调用者无法与内部不相关的类相耦合,以此来达到高内聚,低耦合。封装是实现高内聚,低耦合的基本手段。

继承:继承是面向对象中基本的复用手段。通过继承,子类可以获得父类的行为;通过继承,子类可以修改已有方法或新增方法来达到扩展。

多态:多态可以使我们以相同的代码逻辑处理不同的类(这些不同的类具有共同的接口,这里的接口可以是interface或abstractClass,只要具有相同行为即可),这样保证了我们在替换实现类的时候并不需要修改业务代码。多态是减少重复代码的基本手段之一,没有多态会怎样?,看看下面

public static void drive(BMWCar car){    car.fire();    car.run();}public static void drive(BenzCar car){    car.fire();    car.run();}public static void drive(QQCar car){    car.fire();    car.run();}...省略n多个代码类似的方法...

没有多态,竟然要为每个不同的Car类写一个drive方法,虽然这些类具有相似的行为,没有多态,面向对象将是会是面向灾难,方法暴增,而使用多态后,只要所有car实现ICar接口,业务逻辑方法drive将只用写一个就够了。

//老司机开车逻辑public static void drive(ICar car){    car.fire();//点火    car.run();//跑起来}public interface ICar{    void fire();    void run();}

六大设计原则(SOLID)

设计原则就是几条在面向对象设计过程中具有很好指导意义的规则,遵循这些规则,你的设计将有良好的可维护性。而二十三条设计模式或多或少都遵循了这些设计原则。

S代表单一职责原则(Single responsibility principle);
O代表开闭原则(Open Closed Principle);
L代表里式替换原则(Liskov Substitution Principle)和迪米特法则(Law of Demeter);
I代表接口隔离原则(Interface Segregation Principle);
D代表依赖倒置原则(Dependence Inversion Principle);

1.单一职责原则

定义:不要存在多于一个导致类变更的原因,即一个类只负责一项职责。

说明:定义中目标对象是类,但是我认为单一职责主要有四层含义。
第一层含义,架构分层的单一职责。如mvc架构,将整个架构分为了模型层,视图层,控制层,每层做一类事;
第二层含义,模块职责单一。每个模块做一类事,比如一个app中,商品模块,订单模块,用户模块,只在需要衔接的地方交互;
第三层含义,类职责单一。一个类负责一类职责,Person类负责人相关的行为,Dog类负责狗相关的行为;
第四层含义,方法功能单一。一个方法负责一个功能;

优点:第一个优点是,由于各司其责,修改一个功能不会导致别的功能出现问题。第二个优点是,职责粒度划分小,因此各部分复用性好。
想想雕版印刷术和活字印刷术就是一个很好的例子

活字印刷术雕版印刷术

活字印刷术优于雕版印刷术的地方就是职责的单一性,活字印刷术每个模子的职责仅仅是印刷一个字,每个模子职责粒度小,因此复用性很好,只要重新排列组合这些单字,则可以组合出不同的语句;同时活字印刷术方便修改,一个字雕刻错了,只需要把雕刻错的字进行替换成正确的即可,如果两个字顺序颠倒了,只需要交换顺序即可,而雕版印刷中无论是刻错字还是字顺序出现问题,都需要重新刻制整个模板,牵一发而动全身。

缺点:职责粒度划分越细,则职责单元的个数越多。这样最明显的现象是类个数,或者方法个数会暴增。因此职责粒度划分要在层,模块,类和方法上权衡,比如一个模块类数量过多,是不是可以通过适当增加方法数来减少类的个数,又或者一个类方法数过多是否需要拆分成多个类呢?具体的转化需要根据具体的场景合理的处理。

2.开闭原则

定义:软件架构体系应该对扩展开放,对修改关闭。

说明:软件开发的周期中,需求会一直发生变化,整天抱怨需求改来改去不如去拥抱变化,用良好的设计去应对变化。开闭原则是面向对象设计的终极目标,新需求来临时通过扩展来满足新需求而不是通过修改已有的代码来满足新需求。开闭原则在我看来只是个宣言性质的原则,并没给你提供具有可操作性的建议,仅仅是告诉你好的软件体系应该是什么样的,并且开闭原则只能尽量达到,完全满足该原则是不太现实的。那怎么才能满足开闭原则呢?常用的技巧就是面向抽象编程,通过抽象搭建的框架具有很好的稳定性,通过增加实现类来达到扩展新功能。

开闭原则的一个典型例子就是智能手机与功能手机,智能手机面向扩展开放,要给智能手机增加一个新功能,只要下载一个app安装即可,你在不修改手机系统的情况下就完成了手机功能的扩展。而功能机则违反开闭原则,功能机要加一个功能,则要修改系统了,然后重刷系统了。

3.里式替换原则

定义
定义一.所有引用基类的地方,都能够使用子类进行透明无痛替换。
定义二.如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型

说明:定义一和定义二是等价的。里式替换第一层含义,面向抽象,面向接口,面向父类,这是能使用子类替换的前提;第二层含义,能用子类进行透明无痛替换,强调这个替换过程是透明的无痛的。
  什么叫透明无痛替换?透明无痛替换表示调用处的业务在替换后是无感知的,业务逻辑能够保持正常。最经典的一个例子就是“鸵鸟非鸟”,什么?鸵鸟不是鸟?这不是搞笑么!我们都知道鸵鸟是鸟啊,下面我们来这样一段代码。

//鸟抽象类public abstract class Bird {    public abstract void fly();    public void dosomething(){        //TODO    }}//老鹰继承鸟类public class Eagle extends Bird {    @Override    public void fly() {        System.out.println("老鹰扑着巨大的翅膀,飞了起来!");    }}//燕子继承鸟类public class Swallow extends Bird {    @Override    public void fly() {        System.out.println("燕子展开双翼,用似剪刀的尾巴控制方向,灵巧的飞起来了!");    }}//鸵鸟继承鸟类public class Ostrich extends Bird {    @Override    public void fly() {        System.out.println("鸵鸟扑腾退化的翅膀,并不会飞!");    }}//业务:鸟过悬崖public static void overTheCliff(Bird bird){    System.out.println("来到悬崖边");    bird.fly();    System.out.println("越过了悬崖");}

上面的这段代码中,最后的业务方法是overTheCliff(Bird bird),入参是Bird。按照里式替换原则我们一步一步来,首先overTheCliff方法符合面向抽象、面向父类的要求,入参是Bird,不是一个具体的鸟类,只要是Bird类的子类都能使用该方法实现过悬崖的业务,那我们下面使用子类来替换,来进行业务调用,看看业务是否无痛,业务执行是否正常。

public static void main(String[] args) {    overTheCliff(new Swallow());    overTheCliff(new Eagle());    overTheCliff(new Ostrich());}来到悬崖边燕子展开双翼,用似剪刀的尾巴控制方向,灵巧的飞起来了越过了悬崖来到悬崖边老鹰扑着巨大的翅膀,飞了起来!越过了悬崖来到悬崖边鸵鸟扑腾退化的翅膀,并不会飞!越过了悬崖

替换后,我们发现当子类是老鹰类和燕子类时,业务执行正常,通过飞翔越过悬崖,但当子类是鸵鸟类时出现问题了,鸵鸟不会飞,竟然扑腾着翅膀也越过了悬崖。编译期,这些代码一切正常。到了运行期时,发现业务出现了问题。

为什么会出现这个问题?这是因为在我们的业务里,抽象层次和继承层次是有问题的。解决该问题的方法有两种:第一种方法是修改继承层次,在这里的实际业务中,鸵鸟并不能被看成鸟,一旦被当成鸟则会导致业务失败,根据里氏替换原则的定义二中可以看出鸵鸟并不是鸟的子类(程序P的行为没有发生变化,那么类型S是类型T的子类型,这里行为发生变化了,不符合里氏替换原则中对子类的定义),在这里的业务中,会飞才能满足业务需求才能被认为是鸟,才能完成正常的业务逻辑。第二种方法是overTheCliff(IFly fly)方法中入参变为IFly接口,而Bird抽象类去掉fly()方法,让所有会飞的鸟自己去实现IFly接口即可。

怎么做到里氏替换原则呢?大概有下面几点技巧:
1.子类继承父类时,多扩展新方法,少重载,重写父类方法。
2.如果子类不得不重载父类方法时,方法的入参类型要比父类方法的入参类型宽松些。
3.如果子类重写父类方法或重载父类方法,返回类型窄于或等于父类方法。

遵循里氏替换原则将能有效的避免一些由于继承带来的负面影响。

4.迪米特法则

定义:一个类应该对其他类保持最少的了解,又称最少知道原则

说明:迪米特法则是典型的解耦法则,各类之间的耦合越小,业务发生变动时,需要修改的也越少。怎么来理解迪米特法则中的与其他类保持最少了解呢?
1.设计类时封装好自己的数据和行为,不该暴露的数据和方法坚决不暴露,只提供核心功能给外部,让其他类对你了解的少;
2.调用者调用其他类时,只调用public方法,不要通过反射等手段去使用内部的私有方法和私有属性;
3.类只与第一责任类打交道,举个例子:

    static class CPU{        public void cacu(){            System.out.println("cpu正在处理从内存中读取的数据并进行计算");        }    }    static class Memery{        public void read(){            System.out.println("内存正在读取数据");        }    }    //业务类    public static void main(String[] args) {        CPU cpu = new CPU();        Memery m = new Memery();        m.read();        cpu.cacu();    }    static class Computer{        CPU cpu;        Memery m;        public Computer(){            cpu = new CPU();            m = new Memery();        }        public void deal(){            m.read();            cpu.cacu();        }    }

上面这个实现违背了迪米特法则,业务类需要的是处理数据的能力,但业务类为了获得计算能力,创造了cpu和内存两个对象,并对cpu和内存对象直接进行操作以此来获取计算能力。实际上这样的能力可以由电脑类直接来提供(如果没有电脑类,而计算业务经常使用,则考虑是否需要抽象出这么一个中间类来供其他类使用),因此需要一个能实现计算能力的Computer类来处理业务即可。这样业务类只需要和电脑类打交道,而不需要直接和cpu类、内存类打交道。假如他日计算过程发生改变,内存和cpu品牌发生更改,都不会影响到业务方法(这里的业务方法是main方法)。

//业务类public static void main(String[] args) {    //CPU cpu = new CPU();    //Memery m = new Memery();    //m.read();    //cpu.cacu();    //业务类应该直接和电脑类打交道,使用电脑提供的计算能力,而不是直接操作CPU和内存,怎么操作CPU和内存是电脑的事情    Computer c = new Computer();    c.deal();}

5.接口隔离原则

定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

说明:这个原则较为好理解,过多的依赖不需要的接口方法只会使得接口臃肿,在实现接口时你不得不实现那些压根不用的方法。如:

//控件被点击回调接口public interface Click {    void onClick(View v);//单击    boolean onLongClick(View v);//长按}

这样的接口设计成这样,可能仅仅要为一个按钮添加一个单击事件,却不得不实现长按事件。改进这个接口只需要将这个接口拆分成两个个接口即可。

6.依赖倒置原则

定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。

说明:就定义来说,不管高层模块(一般是业务类),低层模块(一般是实现类),还是细节实现,都应该依赖抽象。依赖倒置原则告诉我们要面向抽象编程,还是之前强调过几次的,抽象的逻辑更具有普适性和稳定性。怎么理解抽象的逻辑具有稳定性呢?看看下面三个方法

public void drive(BMWCar car){    car.move();}public void drive(Car car){    car.move();}public void drive(Transportable t){    t.move();}

方法一的参数是个宝马车类,是个具体的类。如果业务中使用的是第一个方法,结果会怎么样?当业务变更时,由于不能提供一辆宝马车的实例,将导致老司机无法完成开车的动作。怎么办?方法二中,将依赖宝马车改为依赖于车这个抽象类,这样修改后,老司机不只能开宝马,还能开奔驰,奥迪等,只要给一辆是车的东西,老司机都能开(当然不能是玩具车,这样很容易出现违背里氏替换原则,导致业务失效),该业务方法都可以正常使用。有一天发现业务中不能提供车了,无法完成drive这个业务动作了,怎么办?方法三中,将车继续抽象,抽象为交通工具,不论是飞机还是自行车,只要你提供的是交通工具,对于老司机来说,都没有问题,虽然提供的东西在不断改变,但业务中的drive方法如此稳定。这就是抽象的好处。

设计模式

什么是设计模式?

1.面向对象设计中的设计模式借鉴于建筑设计,被gof最早总结在Design Patterns一书上;

2.设计模式是设计过程中可以反复使用,用以解决特定问题的方案;

3.设计模式告诉你怎么使用正确姿势拥抱软件需求的变化;

4.设计模式分为:
创建型:
单例模式、工厂方法(简单工厂+工厂方法)、抽象工厂、原型模式、建造者模式

结构型:
适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元(蝇量)模式

行为型:
策略、模板方法、观察者、中介者、责任链、备忘录、解释器、命令、状态、访问者、迭代器

三、OOD总结

我们会了面向对象的三大特性,为什么还要学设计模式?三大特性,六大原则,二十三种模式之间有什么关系?我们用用练功来打个比方。三大特性 是 基本功,二十三种设计模式 是 高阶招式,六大设计原则 是 内功心法。
掌握了面向对象的三大特性这只是有了基本功,有了一定的设计能力,但仅仅掌握了三大特性还不不足以应对软件设计中万千变化,需要修炼高阶招式去高效解决各种问题,最后,万变不离其中,这些高阶招式都或多或少遵循了六大设计原则,而六大原则的终极目标则是达到开放关闭原则,使得软件少修改,易扩展,解放程序员的劳动力。

注:参考文献
https://www.codeproject.com/Articles/93369/How-I-explained-OOD-to-my-wife
http://www.cnblogs.com/aoyeyuyan/p/5495219.html
设计模式之禅
大话设计模式

原创粉丝点击