胡侃软件之设计原则

目录

1. 概述

2. 设计原则

2.1. 职责单一原则

2.2. 里氏替换原则

2.3. 依赖倒转原则

2.4. 接口隔离原则

2.5. 迪米特原则

2.6. 开闭原则

 

1 概述

        软件设计是从需求规格说明书开始,根据需求分析阶段确定的功能设计软件系统的整体结构、划分功能模块、确定每个模块的实现算法以及编写具体的代码,形成软件最终的软件产品.软件设计是把许多事物和问题抽象出来。将问题或事物分解并且模块化使得解决问题变得容易,同时分解的越细模块数量也就越多,这带来的副作用就是使得设计者需要考虑更多的模块之间耦合度的情况。

软件设计包括软件的结构设计,数据设计,接口设计和过程设计.软件是对真实世界的抽象,是对某种思想的总结和呈现.思想和对真实世界的看法是不断变化的,因此软件是会不断变化的.软件的设计为了让这些变化变得更加容易,成本更低而总结的一套方法.

2 设计原则

        软件设计的原则主要是解耦各个模块之间的关系,这里需要明确一点,软件设计出来是给人看的,解耦是指软件在开发阶段的时候对于其他模块的依赖,不是在运行阶段彻底分离.如果在运行阶段也彻底分离那就说明这个模块在此系统中没有任何作用可以去掉了.

一个优秀的设计可以应对软件的变化,可以方便的对现有软件进行扩展,可以很容易的对现有程序进行修改,可以很容易理解软件各个模块之间的关系.将程序修改成本降到最低.为此业界各个大神总结了一套软件设计的原则.

 2.1职责单一原则

      只因为一种需求变化引起本模块的修改.如果有多重需求的变化都会引起此模块的变化,那么就应该考虑将模块进行分离,重新设计.职责单一原则的好处1,降低类的复杂度,实现什么功能很明确.2,提高可读性.3,提高可维护性,因为复杂度降低和可读性提高可维护性自然就上来了.4,变更引起的风险降低,这是单一原则最大的好处.一个接口修改只对相应的实现类有影响,与其他的接口无影响,这个是对项目有非常大的帮助.

我们看一个例子,现在手机都有拍照和播放音乐的功能,我们程序模拟手机这两功能进行设计.类图如下:

这个设计看起来没有什么问题,功能能够很好的完成.那么问题来了拍照和播放音乐在系统中有直接关系吗明显没有.那就说明,Mobile类的修改会有两种不同的需求变化引起.这样设计就不符合职责单一原则.修改如下:

这样修改之后手机只关心功能的接口,不在关心具体的实现,接口实现只针对具体的摸一个功能来完成.而且只有一种变化会引起实现类的修改.

职责单一原则也是比较有争议的原则,职责的定义很难有一个明确的标准.因此在项目中很难看到单一原则的影子,在实际的项目环境中,由于开发工作量大,人员技术水平以及项目紧迫程度都会导致大量的设计违背这一原则.而且原本一个类可以实现的功能可能会被拆分到多个类中实现,然后在采用聚合等方式合并到一起.这样也增加了开发工作量.所以导致了很多人放弃了此原则.

职责单一原则不仅仅是在接口和类中,同样也实用于方法.一个方法应该尽可能只完成一件单一的事情.俗话说,我简单我快乐就是这样的.比如某些方法喜欢根据参数来进行switch case然后做不同的事情,这样代码其实是非常糟糕的代码.一般的做法是将具体的case中的内容单独提取成为一个方法,然后进行调用.有判断的地方可以考虑策略等模式进行处理.

 

2.2里氏替换原则

      基类可以出现的地方都可以用子类进行替换,而不会引起任何不适应的问题.里氏替换原则是继承复用的基石,只有当派生类可以替换掉基类,软件的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为.里氏代换原则是对“开-闭”原则的补充.实现“开-闭”原则的关键步骤就是抽象化.覆盖或实现父类的方法时,子类返回值类型可以是父类返回值类型的子类.

举个例子:现在流行的log4j日志系统,就可有很多种输出方式,可以发送邮件,可以写数据库,可以写文本.其简单设计模型如下:

客户端WriteLog类调用只依赖于抽象类Log4j.同时对于子类实例的创建,这里有很多种方式,比如工厂方法,IoC注入等等,当然也可以在Client类中直接创建,但这种方式不是很好.本例中采用的是属性注入的方式.具体代码如下:

public class Log4jDemo {

    public static void main(String[] args) {
        WriteLog writeLog =new WriteLog(new WriteTextLog());
        writeLog.writeLog(“日志”);
    }
}

class WriteLog
{
    private Log4j log4j;

    public WriteLog(Log4j log4j)
    {
        this.log4j=log4j;
    }

    public void writeLog(String log)
    {
        log4j.writeLog(log);
    }

}

abstract class Log4j
{
    public abstract void writeLog(String logInfo);
}

class WriteDatabaseLog extends Log4j
{
    private String connectString;

    public String getConnectString() {
        return connectString;
    }

    public WriteDatabaseLog setConnectString(String connectString) {
        this.connectString = connectString;
        return this;
    }

    @Override
    public void writeLog(String logInfo) {
        System.out.println(“写数据库日志”);
        System.out.println(logInfo);
    }
}

class WriteTextLog extends Log4j
{
    private String filePath;

    public String getFilePath() {
        return filePath;
    }

    public WriteTextLog setFilePath(String filePath) {
        this.filePath = filePath;
        return this;
    }

    @Override
    public void writeLog(String logInfo) {
        System.out.println(“写文本日志”);
        System.out.println(logInfo);
    }
}

class  SendToEmail extends Log4j
{
    private String emailAddr;

    public String getEmailAddr() {
        return emailAddr;
    }

    public SendToEmail setEmailAddr(String emailAddr) {
        this.emailAddr = emailAddr;
        return this;
    }

    @Override
    public void writeLog(String logInfo) {
        System.out.println(“发送邮件”);
        System.out.println(logInfo);
    }
}

在本例中WriteLog类中申明了Log4j抽象类,但具体依赖的对象是通过注入方式进来的.注意这里建议只使用抽象类或者接口来做.否则就回违背其他原则(比如依赖倒转原则),而且在WriteLog类中注入的类是Log4j的子类中的任意一个均可以.

里氏替换原则只能正向使用,在这里其实很好理解,子类可以有自己的独有的外观和行为.上面的例子中父类并没有定义自己的外观,仅仅定义了一个行为.每个子类中都有自己的一个独有外观.所以这里大家也应该能够看明白,子类与子类之间并没有直接联系,因此子类和子类直接无法直接替换.这也是在定义接口时必须使用父类或者接口的原因.

在里氏替换原则中还有一个问题需要考虑,那就是子类是否完整整覆盖了父类业务.如果我们子类是对日志进行分析统计,那么这个时候写日志其实就不合适了.

我们下面我们将上面的例子修改一下增加一个日志分析类,类图下:

增加的类用于分析日志中错误出现的次数,代码如下:

class AnalysisLog extends Log4j
{
    @Override
    public void writeLog(String logInfo) {
        int count = logInfo.indexOf(“错误”);
        System.out.println(“有” + count + “条错误”);
    }
}

首先可以肯定这个代码是可以正常运行的,也能够得到正确的结果,但是要明白抽象类定义writeLog的方法是用于记录日志,而不是分析统计日志.因此这里的实现放在writeLog中并不合适.那么这就说明AnalysisLog类继承的Log4j类在这里并没有将业务完整覆盖.对于这种问题最简单的处理方式就是直接脱离原来的继承关系,但是脱离之后AnalysisLog类分析日志的时候还是需要日志信息,那么怎么办呢以增加一个抽象类,来完成日志信息获取.修改后的类图如下:

修改后的类图是在Analysis与Log4j之间增加了一个抽象类AbsAnalysisLog,将接收日志信息的功能委托出来,然后AnalysisLog和其他的写日志的类各自发展.

继承关系有一个需要注意的是:子类在继承父类之后子类的前置条件应该比父类更加宽松,举个例子:

class Father
{
    public Log4j factory(Log4j log4j)
    {
        System.out.println(“Execute father”);
        return log4j;
    }

}
class Son extends Father
{
    public Log4j factory(SendToEmail sendToEmail)
    {
        System.out.println(“Execute son”);
        return sendToEmail;
    }
}
class  main
{
    public static void main(String[] args) {
        Father f=new Son();
        f.factory(new SendToEmail());
        System.out.println(“====”);
        Son s=new Son();
        s.factory(new SendToEmail());
    }
}

输出结果如下:

这里就说明子类在参数方面缩小了父类的参数范围,这种上面的代码在实际的项目中可能会引起很多不需要麻烦.在main函数中的申明是一个Father类指向一个Son的对象.正常情况应该代用Son的factory方法.但这里执行了Father的方法,因此可能引起意想不到的Bug.所以子类中方法的前置条件必须与父类中被覆盖的方法的前置条件相同或者更宽松.上述例子中的代码其实子类和父类的的factory方面没有关系了.按照大多数的设计应该是子类方法中的参数和父类中的方法参数一样或者是父类方法中参数类型的父类.

子类在覆盖父类方法的时候输出结果可以缩小.意思就是方法返回值可以是父类方法返回值的子类.例子:

class Son2 extends Father
{
    @Override
    public SendToEmail factory(Log4j log4j)
    {
        System.out.println(“execute son2”);
        return (SendToEmail)log4j;
    }

}

class  Main
{
    public static void main(String[] args) {
        Father f2=new Son2();
        f2.factory(new SendToEmail());
    }
}

 

执行结果如下:

 

里氏替换原则是为了提高程序的健壮性和兼容性.在做版本升级的时候可以有更多的选择考虑上下级兼容.增加了子类扩展了功能可以保证原来的功能不受影响.

2.3依赖倒转原则

      依赖倒转原则简单来说就是高层模块不应该依赖底层模块,抽象不应该依赖细节,细节应该依赖抽象.依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多.以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多.在实际的开发中抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成.我们任然用上一节中的例子来表示类图如下:

在这里对于写日志WriteLog类来说只需要依赖于Log4j这个接口即可.根本不关心具体怎么写日志.给Log4j的任何一种实现都可以完成WriteLog中所需要的功能.

依赖倒置原则的核心思想是面向接口编程,只对契约负责.我们在举一个例子.很多人找对象都有一堆要求.比如会做饭,声音甜美,皮肤白皙,身材苗条,懂的什么叫做爱.好吧这些其实就是一个契约.符合这个契约的女人很多,给他随便哪一个都可以(不允许一夫多妻,要不然可以来多个).下面是类图:

从上面的类图中可以看到这个Man其实很可怜的,最多只有两个符合他的要求(只有两个类实现契约接口),可选余地很少.对于具体是Girl1还是Girl2对于都符合Requirement的要求.给他Girl1还是Girl2都无所谓.具体实现代码如下:

class Main1
{
    public static void main(String[] args) {
        Man zhangsan=new Man();
        Requirement xiaohua=new Girl1();
        xiaohua.setGirlName(“小花”);
        zhangsan.setRequirementGril(xiaohua);
        zhangsan.marray();
    }
}

class Man
{
    private  Requirement requirementGril;

    public Man setRequirementGril(Requirement requirementGril) {
        this.requirementGril = requirementGril;
        return this;
    }

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2019年5月1日
下一篇 2019年5月1日

相关推荐