软件设计与体系——面向对象设计的原则

Posted _瞳孔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件设计与体系——面向对象设计的原则相关的知识,希望对你有一定的参考价值。

一:前言

用一道题熟悉OO设计与编程:给定一个4位数(数字不完全相同),这4个数字可以得到最大4位数和最小4位数,算它们的差,不是6174则重复上述过程,最终会得到6174,验证这个过程。

import java.util.Arrays;
import java.util.Scanner;

public class Test 
    private static int[] x2Arr(int x) 
        int[] xArr = new int[4];
        for (int i = 0; i < 4; i++) 
            xArr[i] = (int)(x / Math.pow(10, i)) % 10;
        
        Arrays.sort(xArr);
        return xArr;
    

    public static void main(String[] args) 
        Scanner sc = new Scanner(System.in);
        System.out.print("请输入各位数字不完全相同的4位正整数:");
        int x = sc.nextInt();
        if (x < 1000 || x > 9999) return;

        int round = 0;
        int start = x;
        int end;
        while(true) 
            int[] xArr = x2Arr(start);
            int low = 0, high = 0;
            for (int i = 0; i < 4; i++) 
                low += xArr[i] * Math.pow(10, 3 - i);
                high += xArr[i] * Math.pow(10, i);
            
            end = high - low;

            // 结束条件
            if (end == 0) 
                System.out.println("输入错误,各位数字不完全相同");
                return;
            
            if (start == end) 
                System.out.println("最终结果稳定为:" + start + ",重复轮数为:" + round);
                return;
            

            System.out.println("第" + (++round) + "轮差值为:" + end);
            start = end;
        
    

Robert C. Martin指出,导致软件难维护的有原因四个:

  • 过于僵硬(Rigidity):难加入新性能
  • 过于脆弱(Fragility):很难改动,与过于僵硬同时存在,牵一发动全身
  • 复用率低(Immobility):类似的算法、函数、模块,由于已有的代码依赖于很多其他东西,很难分离。最好的服用方法是直接CV
  • 黏度过高(Viscosity):一个改动有以下两种方式,一个系统的设计,如果总是后一种方法容易,则称为黏度过高
    • 保存原始设计意图和原始设计框架
    • 权益之计,解决短期问题,牺牲中长期利益

二:面向对象设计原则

面向对象设计模式七大原则:

  • 单一职责原则(Single Responsibility Principle, SRP):类的职责要单一,不能将太多的职责放在一个类中
  • 接口隔离原则(The Interface Segregation Principle):客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口
  • 依赖倒转原则(Dependency Inversion Principle, DIP):要针对抽象层编程,而不要针对具体类编程。
  • 里氏代换原则(Liskov Substitution Principle, LSP):在软件系统中,一个可以接受基类对象的地方必然可以接受一个子类对象。
  • 开闭原则(Open-Closed Principle, OCP):软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能。
  • 迪米特法则(Law of Demeter, LoD):一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互。
  • 合成复用原则(组合优先原则)(Composite Reuse Principle, CRP):在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系。

使用设计模式的目的:编写软件过程中,程序员面临着来自耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式是为了让程序(软件)具有更好的:

  1. 代码重用性
  2. 可读性(编程规范性)
  3. 可扩展性(便于添加新功能)
  4. 可靠性(增加新功能后,对原功能不影响)
  5. 使程序呈现高内聚,低耦合的特性

其中单个类的设计原则

  • 抽象、封装和信息隐藏
  • 关注点分离和单一职责原则
  • 接口隔离原则

多个合作类的设计原则:

  • 松耦合
  • 开闭原则
  • 里氏代换原则
  • 依赖倒置原则
  • 契约式设计

程序的可读性:

  • 封装、抽象和信息隐藏
  • 关注点分离和单一职责原则

程序的正确性:

  • 里氏代换原则
  • 契约式设计原则
  • 多态程序的正确性

可扩展性

  • 接口隔离原则
  • 松耦合
  • 里氏替换原则
  • 开闭原则
  • 依赖倒置原则

一些概念:

  • 封装
    • 方法和数据封装成一个对象
    • 实现信息隐藏、接口对外
  • 抽象
    • 方法的接口使对象功能抽象
    • 对象实现接口定义方法
  • 信息隐藏
    • 封装内部属性
    • 通过接口访问(外部属性)
    • 保密原则

2.1:单一职责原则

基本介绍:对类来说的,即一个类应该只负责一项职责。如类A负责两个不同职责:职责1,职责2。当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为A1,A2。

单一职责原则的核心含意是:

  • 一个类应该有且仅有一个职责
  • 一个类的职责是指引起该类变化的原因
  • 并非极端为一个类只设定一个方法,而是一组方法只能有一个变化轴线
  • 最好有一个概念统构该组方法

一个类(或者大到模块,小到方法)承担的职责越多,它被复用的可能性越小。而且如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。

而单一职责原则是实现高内聚、低耦合的指导方针,在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责(单一职责的界定)需要设计人员具有较强的分析设计能力和相关重构经验。

单一原则注意事项和细节:

  • 降低类的复杂度,一个类只负责一项职责。提高类的可读性,可维护性
  • 降低变更引起的风险
  • 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,才可以在方法级别保持单一职责原则

代码示例:

package com.eyes.base.designMode.principle;

public class SingleResponsibility 
    public static void main(String[] args) 
        // 方案一测试
        Vehicle vehicle = new Vehicle();
        vehicle.run("摩托车");
        vehicle.run("飞机");
        vehicle.run("潜艇");

        // 方案二测试
        new RoadVehicle().run("汽车");
        new AirVehicle().run("飞机");
        new WaterVehicle().run("潜艇");

        // 方案三测试
        VehicleImproved vehicleImproved = new VehicleImproved();
        vehicleImproved.runRoad("汽车");
        vehicleImproved.runAir("飞机");
        vehicleImproved.runWater("潜艇");
    


/*
 **************
 * 方案一:违反了单一职责原则
 **************
 */
// 交通工具类
class Vehicle 
    // 飞机和潜艇不是在公路上跑,因此违反单一职责原则
    public void run(String vehicle) 
        System.out.println(vehicle + " 在公路上跑");
    


/*
 **************
 * 方案二:
 *      遵守单一职责原则
 *      这么做改动很大,将类分离的同时修改客户端
 **************
 */
class RoadVehicle 
    public void run(String vehicle) 
        System.out.println(vehicle + " 在公路上跑");
    

class AirVehicle 
    public void run(String vehicle) 
        System.out.println(vehicle + " 在天上飞");
    

class WaterVehicle 
    public void run(String vehicle) 
        System.out.println(vehicle + " 在水里行驶");
    


/*
 **************
 * 方案三:
 *      没有在类级别遵守单一职责原则,但是方法级别遵守
 *      对原本的类修改不大
 **************
 */
class VehicleImproved 
    public void runRoad(String vehicle) 
        System.out.println(vehicle + " 在公路上跑");
    
    public void runAir(String vehicle) 
        System.out.println(vehicle + " 在天上飞");
    
    public void runWater(String vehicle) 
        System.out.println(vehicle + " 在水里行驶");
    

2.2:接口隔离原则

基本介绍:客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口

"胖"接口

  • 接口不内聚,表示该类具有"胖"接口
  • 类的"胖"接口可以分解成多组方法
  • 每一组方法都服务于一组不同的客户程序
  • 一些客户程序使用一组方法,其他使用其他组方法

而LSP原则(接口隔离原则)可以用来处理"胖"接口所具有的缺点

接口污染:为接口添加了不必要的职责

  • 在接口中加一个新方法只是为了减少类的数目,持续这么做接口就会被不断污染,变胖
  • 实际上,类的数目不是问题
  • 接口污染会带来维护和重用方面的问题
  • 最常见的问题:为了重用被污染的接口,被迫实现并维护不必要的方法
/*
 **************
 * 传统方法:
 *      类C通过Interface1依赖类A,类D通过接口Interface1依赖类B
 *      如果接口Interface对于类A和类C来说不是最小接口,那么类C和类D就必须去实现他们不需要的方法
 **************
 */

interface Interface1 
    void operation1();
    void operation2();
    void operation3();
    void operation4();


class A implements Interface1 

    @Override
    public void operation1() 
        System.out.println("A实现了operation1");
    

    @Override
    public void operation2() 
        System.out.println("A实现了operation2");
    

    @Override
    public void operation3() 
        System.out.println("A实现了operation3");
    

    @Override
    public void operation4() 
        System.out.println("A实现了operation4");
    


class B implements Interface1 

    @Override
    public void operation1() 
        System.out.println("B实现了operation1");
    

    @Override
    public void operation2() 
        System.out.println("B实现了operation2");
    

    @Override
    public void operation3() 
        System.out.println("B实现了operation3");
    

    @Override
    public void operation4() 
        System.out.println("B实现了operation4");
    


// C类通过接口Interface1依赖A或B类,但只会用到1,2,3方法
class C 
    public void depend1(Interface1 i) 
        i.operation1();
    

    public void depend2(Interface1 i) 
        i.operation2();
    

    public void depend3(Interface1 i) 
        i.operation3();
    


// D类通过接口Interface1依赖A或B类,但只会用到1,3,4方法
class D 
    public void depend1(Interface1 i) 
        i.operation1();
    

    public void depend3(Interface1 i) 
        i.operation3();
    

    public void depend4(Interface1 i) 
        i.operation4();
    


/*
 **************
 * 接口隔离改进:
 *      将接口Interface1拆分为独立的几个接口,类C和类D分别与他们需要的接口建立依赖关系,也就是采用接口隔离原则
 *      接口Interface1中出现的方法可以根据实际情况分为三个接口
 **************
 */
interface Interface2 
    void operation1();
    void operation3();


interface Interface3 
    void operation2();


interface Interface4 
    void operation4();

2.3:依赖倒转原则

依赖倒转原则是Robert C. Martin在1996年为《C++ Reporter》所写的专栏Engineering Notebook的第三篇,后来加入到他在2002年出版的经典著作《Agile Software Development, Principles, Patterns, andPractices》中。

依赖倒转原则是指:

  • 高层模块不应该依赖低层模块,二者都应该依赖其抽象
  • 抽象不应该依赖细节,细节应该依赖抽象
  • 依赖倒转的中心思想是面向接口编程

倒置的含义:很多传统的软件开发方法,比如结构化分析和设计,总是倾向于创建高层模块依赖于低层模块、抽象则依赖于细节的软件结构。一个设计良好的面向对象的程序的依赖关系结构相对于传统过程式方法设计的通常的结构而言就是被“倒置”了

依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类

使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成

层次化概念: Booch曾经说过:“所有的结构良好的面向对象架构都具有非常清晰的层次,每一个层次通过一个被很好地定义和控制的接口向外提供了一系列相互内聚的服务。”这个陈述的简单理解可能会致使一个设计者设计出类似下图的结构,在图中高层的策略类调用了低层的机制层;而机制层又调用更具体的工具类。存在一个隐伏的特性,那就是:Policy Layer对于其下层次一直到Utility Layer的改动都是非常敏感的,这意味着依赖是可传递的

下图是一个更合适的模型:

代码示例:

package com.eyes.base.designMode.principle;

public class DependencyInversion 
    public static void main(String[] args) 
        // 方法一
        Person person = new Person();
        person.receive(new Email());

        // 方法二
        new PersonPlus().receive(new EmailPlus());
        new PersonPlus().receive(new WeChat());
        new PersonPlus().receive(new Phone());

        // 方法三
        new OpenAndClose().open(new ColorTV());

        // 方法四
        new OpenAndClose2(new ColorTV2()).open();

        // 方法五
        OpenAndClose3 openAndClose3 = new OpenAndClose3();
        openAndClose3.setTv(new ColorTV3());
        openAndClose3.open();
    


/*
 ******************
 * 方式一:
 *      实现简单,receive直接依赖Email类
 *      但如果获取的对象是微信、短信等,则需要新增类,Person也要增加相应接收方法
 ******************
 */

class Email 
    public String getInfo() 
        return "邮件信息:hello world";
    


class Person 
    public void receive(Email email) 
        System.out.println(email.getInfo());
    


/*
 ******************
 * 方式二:遵循依赖倒转原则
 ******************
 */

interface Info 
    String getInfo();


class EmailPlus implements Info 
    @Override
    public String getInfo() 
        return "邮件信息:hello world";
    

class WeChat implements Info 
    @Override
    public String getInfo() 
        return "微信信息:hello world";
    

class Phone implements Info 
    @Override
    public String getInfo() 
        return "短信信息:hello world";
    


class PersonPlus 
    public void receive(Info info) 
        System.out.println(info.getInfo());
    


/*
 ******************
 * 方式三:通过接口传递依赖关系
 ******************
 */
interface IOpenAndClose 
    void open(ITV tv);


interface ITV 
    void play();


class ColorTV implements ITV 
    @Override
    public void play() 
        System.out.println("彩色电视播放");
    


class OpenAndClose implements IOpenAndClose 
    @Override
    public void open(ITV tv) 
        tv.play();
    


/*
 ******************
 * 方式四:通过构造方法传递依赖关系
 ******************
 */
interface IOpenAndClose2 
    void open();


interface ITV2 
    void play();


class ColorTV2 implements ITV2 
    @Override
    public void play() 
        System.out.println("彩色电视2播放");
    


class OpenAndClose2 implements IOpenAndClose2 
    private final ITV2 tv;

    public OpenAndClose2(ITV2 tv) 
        this.tv = tv;
    

    @Override
    public void open() 
        tv.play();
    


/*
 ******************
 * 方式五:通过setter方法传递依赖关系
 ******************
 */
interface IOpenAndClose3 
    void setTv(ITV3 tv);
    void open();


interface ITV3 
    void play();


class ColorTV3 implements ITV3 
    @Override
    public void play() 
        以上是关于软件设计与体系——面向对象设计的原则的主要内容,如果未能解决你的问题,请参考以下文章

面向对象设计原则 面向对象设计原则都有哪些

面向对象设计原则 里氏替换原则(Liskov Substitution Principle)

23种设计模式之里氏替换原则

8面向对象设计模式六大原则总结

8面向对象设计模式六大原则总结

面向对象设计原则