面向对象设计原则

面向对象设计原则

大家好,又见面了,我是全栈君。


开放封闭原则

基本描述

一个设计良好的应用程序应该做到对扩展开放,对修改封闭。换言之:当系统需要添加一个新的模块时,尽可能少地修改已有的代码(对修改封闭),通过添加新的类型(class)以增加新的功能(对扩展开放)。

举例说明

假设要开发一个二目运算类Calculater,其不考虑扩展性的设计如下:

/*
 * Created by SharpDevelop.
 * User: Joey
 * Date: 2015/5/26
 * Time: 23:55
 */
using System;

namespace OCP
{
    class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            
            int result = new Calculater().Calculate(1,1,"+");
            Console.WriteLine(result);
            
            Console.Write("Press any key to continue . . . ");
            Console.ReadKey(true);
        }
    }
    
    class Calculater
    {
        public int Calculate(int p1, int p2, String calculateType)
        {
            if (calculateType == "+") {
                return p1 + p2;
            }
            if (calculateType == "-") {
                return p1 - p2;
            }
            
            throw new Exception("未知的计算");
        }
    }
}

上述设计中,如果要增加新的运算符号,那么就要对Calculater进行修改,添加更多的if语句。下面的设计是经过重构后满足开闭原则的代码:

/*
 * Created by SharpDevelop.
 * User: Joey
 * Date: 2015/5/26
 * Time: 23:55
 */
using System;

namespace OCP
{
    class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            
            AbsCalculater calculater = GetCalculater("+");
            int result = calculater.Calculate(1,1);
            Console.WriteLine(result);
            
            Console.Write("Press any key to continue . . . ");
            Console.ReadKey(true);
        }
        
        public static AbsCalculater GetCalculater(String calculateType)
        {
            if (calculateType == "+") 
            {
                return new AddCalculater();
            }
            if (calculateType == "-") 
            {
                return new SubCalculater();
            }
            return null;
        }
    }
    
    abstract class AbsCalculater
    {
        abstract public int Calculate(int p1, int p2);
    }
    
    class AddCalculater : AbsCalculater
    {
        public override int Calculate(int p1, int p2)
        {
            return p1 + p2;
        }
    }
    
    class SubCalculater : AbsCalculater
    {
        public override int Calculate(int p1, int p2)
        {
            return p1 - p2;
        }
    }
    
}

这样重构以后,如果要添加新的运算方法,只需添加新的继承至类AbsCalculate即可。当然GetCalculater这个方法需要修改,但是可以通过依赖注入反射代替之,而先前的设计是无法实现的。

目的

满足开闭原则与否决定了代码是否优雅,符合开闭原则有如下好处:

  • 添加新功能(模块)时不会影响旧有代码,减少出错可能性,易扩展
  • 使应用程序表现出多态特性,灵活应对变化

单一职责原则

基本描述

一个类应该尽量被设计为仅有一个职责,即只干一件事情。衡量一个类是否具有多个职责的依据是:如果引起类变化(此处的变化可以理解对类中代码进行修改)的原因仅有一个,那么可以说这个类仅有一个职责;反之,引起类变化的原因有多个,那么,可以说这个类具有多个职责。

举例说明

举例说明,比如存在一个实体类User,一个操作User的UserOperator(点击链接查看类图)类,该类明显违反单一职责模式,其存在两个职责:一个是对用户的操作,一个是对数据库的操作。经过优化后的类图(点击链接查看类图)增加了一个类DbAcess专门负责操作数据库,UserOperator通过组合的关系调用DbAcess。

目的

  • 代码更清晰可读
  • 有利于复用(该例中DbAcess显示可以在很多地方复用)
  • 维护代码更容易(职责太多的话难免有时会因为修改一个职责而对另一个职责产生影响)

扩充

不仅是类,接口的职责也要做到单一,也就是后面要说到的“接口隔离原则”。方法的职责更应该单一,尽量避免长篇大论的代码段,努力控制代码行数,将长方法按照职责的不同分为数个小方法。比如,有一个从Txt文件,Excel文件导入数据的方法LoadDataFromFile(),也许可以将该方法拆分为两个方法:LoadDataFromTxt()LoadDataFromExcel():

class DataLoder
{
    pirvate void LoadDataFromFile()
    {
        //60行代码
    }
}

修改为:

class DataLoader
{
    pirvate void LoadDataFromFile()
    {
        LoadDataFromTxt();
        LoadDataFromExcel();
    }
    
    private void LoadDataFromTxt()
    {
        //30行代码
    }
    
    private void LoadDataFromExcel()
    {
        //30行代码
    }
}

这样处理的好处也是显而易见的,想象一下60行代码中如果有4个嵌套循环8个if,找对称的左右大括号就很让人郁闷了……

另外,在一个应用系统中,每个层次都有自己明确的一个职责,每个模块都有自己明确的一个职责,不可以有除了这个明确职责之外的职责,比如数据访问层就只管读写数据(而不在数据库访层中添加和业务相关的代码),业务逻辑层就只管业务逻辑处理(而不在业务逻辑层中拼SQL)。

适可而止

单一职责应是所以面向对象设计原则中最不易把握的一个,职责的划分粒度本来就没有什么标准可依,正如第一个例子,也可以说AddUser方法和DeleteUser方法也是两个不同的职责,可以再划分,但实际上大家肯定很少这么做,职责的粒度太细会引起类数量上的膨胀和工作量的增加。总之,单一职责需要适可而止,可以通过判断引起类变化的可能因素是否超过一个这条原则把控职责的粒度划分。

里氏替换原则

基本描述

设有程序P,父类型T1,子类型T2:

  • 程序P中,将任何T1类型的对象替换为T2类型的对象后,程序行为仍然正常
  • 程序P不能察觉出T1类型和T2类型的区别

举例说明

且看经典的矩形和正方形示例:

/*
 * Created by SharpDevelop.
 * User: Joey
 * Date: 2015/5/22
 * Time: 0:48
 */
using System;

namespace LSP
{
    class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("LSP test!");
            
            Rectangle rect = GetRectangle();
            rect.Width = 10;
            rect.Height = 5;
            int area = GetArea(rect);
            Console.WriteLine(area);//预想的结果是50,实际上是25
            
            Console.Write("Press any key to continue . . . ");
            Console.ReadKey(true);
        }
        
        private static int GetArea(Rectangle rectangle)
        {
            return rectangle.Width * rectangle.Height;
        }
        
        private static Rectangle GetRectangle()//假设这个是个工厂
        {
            //return new Rectangle();
            return new Square();
        }
    }
    
    class Rectangle
    {
        public virtual int Height { get; set; }
        
        public virtual int Width { get; set; }
    }
    
    class Square:Rectangle
    {
        int _width;
        
        public override int Width 
        {
            get { return _width; }
            set 
            {
                _width = value; 
                _height = value;
            }
        }
        
        int _height;
        
        public override int Height 
        {
            get { return _height; }
            set 
            {
                _height = value;
                _width = value;
            }
        }
    }
}

上述例子违反基本描述中的第一条:程序P中,将任何T1类型的对象替换为T2类型的对象后,程序行为仍然正常

再看一个例子:

/*
 * Created by SharpDevelop.
 * User: Joey
 * Date: 2015/5/23
 * Time: 8:33
 * 
 * To change this template use Tools | Options | Coding | Edit Standard Headers.
 */
using System;

namespace LSP2
{
    class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            
            Bird bird = GetBird();
            if (bird is Bird)
            {
                bird.FlyTo("home");
            }
            if (bird is Chicken)
            {
                (bird as Chicken).RunTo("home");
            }
            
            Console.Write("Press any key to continue . . . ");
            Console.ReadKey(true);
        }
        
        private static Bird GetBird()//工厂
        {
            return new Chicken();
        }
    }
    
    class Bird
    {
        public virtual void FlyTo(String position)
        {
            Console.WriteLine("the bird can fly to " + position);
        }
    }
    
    class Chicken : Bird
    {
        public override void FlyTo(String position)
        {
            ;
        }
        
        public void RunTo(String position)
        {
            Console.WriteLine("the chicken can run to " + position);
        }
    }
}    

该例中,Main方法调用Bird时必须要判断Bird的实例类型,这违反了基本描述中的第二条:程序P不能察觉出T1类型和T2类型的区别

目的

里氏替换原则是实现开闭原则的一个条件,如果违反里氏替换原则就无法做到”对修改封闭”。如上面的第二个例子,如果在程序P中将Bird对象替换为Chicken对象后,程序需要增加判断条件,没有做到“对修改封闭”。

依赖倒置原则

基本描述

很多时候,在一个应用设计方案中,高层模块调用低层模块,低层模块的变化会导致高层模块的修改,系统不是很稳定。使用依赖倒置原则可以缓解这种情况,该原则强调:高层模块不应该依赖于低层模块,两者都应该依赖于抽象;具体的实现细节应该依赖于抽象,而不是抽象依赖于具体。还有一种更容易理解的说法叫不要针对具体编程,而应该针对抽象编程

类型和类型之间的耦合按照类型是否为具体类型或抽象类型可划分为如下情况:

  • 具体类型和具体类型之间的依赖
  • 具体类型和抽象类型之间的依赖

其中,由于抽象类型较之具体类型是不容易改变、相对稳定的部分,所以引入对抽象类的依赖可以减小两个具体类之间的耦合程度,在该原则中则表现为:在高层模块和低层模块间引入抽象层模块以减少高层模块和低层模块之间的耦合度。

举例说明

假设某君TGL欲开发一款山寨星际2,该君主族是Terran,于是先定义了人类王牌防御工事——地堡类如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DIP
{
    class Bunker
    {
        public void Attack(Zealot enemy)
        {
            enemy.HP--;
        }

        public void Attack(Stalker enemy)
        {
            enemy.HP--;
        }
    }
}

Bunker类(点击查看类图)依赖于具体类Zealot和Stalker。且说开局Protoss欲4BG速Rush,无奈TGL利用地形快起地堡,置4枪兵于内防守,外加两工程师维修,Protoss一波流终于被打退,然对手也非等闲之辈,双方运营后打后期,10分钟后,Protoss两不朽、两巨像、若干狂战士外加哨兵一顿突突,TGL放弃抵抗,打出GG。究其原因,原来是Bunker类太依赖于对方具体单位,而致其灵活性较差。虽然Bunker目前能很好得地打击狂战士和追猎者,但当新兵种来临时,Bunker类必须修改,增加正对不朽等新的Attack方法,由于此次攻击兵种较多,TGL君APM本来就低,还要调整较多的代码,于是GG。

痛定思痛后TGL决定遵循依赖倒置原则,重新设计代码如下(点击查类图):

using System;
using System.Collections.Generic;
using System.Data.OracleClient;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DIP
{
    class Bunker
    {
        public void Attack(AbsEnemy enemy)
        {
            enemy.HP--;
        }
    }

    abstract class AbsEnemy
    {
        public abstract int HP { get; set; }

        //其它方法...
    }

    class Zealot: AbsEnemy
    {
        public override int HP
        {
            get;//略
            set;//略
        }

        //其它方法...
    }

    class Stalker:AbsEnemy
    {
        public override int HP
        {
            get;//略
            set;//略
        }

        //其它方法...
    }
}

目的

化对易变的具体的依赖为对稳定的抽象的依赖,其中,“倒置”这个词有点难于理解,几年前,在伟大的博客园上和那些不以为我傻逼的园友进行了深入的交流,对“倒置”略有所得,连接在此:依赖倒置原则的“倒置”体现在哪里,”依赖倒置“为什么不叫”依赖转移“而叫”倒置“(高人勿入)

接口隔离原则

基本描述

不应该为客户提供用不着的接口

举例说明

假设为客户A和B提供了接口X,接口X中包括了方法f1,f2,和f3,其中A需要调用f1和f2,而B需要调用f2和f3。这个时候正确的做法应该是为A提供一个接口Y,只包含方法f1和f2,为B提供一个接口,只包含f2和f3。

目的

  • 单一职责原则在抽象层的体现
  • 避免庞大臃肿的接口给客户带来选择上的混乱

转载于:https://www.cnblogs.com/zzy0471/p/6920853.html

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/108463.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 用vc2010怎么编译运行C语言,怎么用vc++2010学c语言程序设计?如何像vc6.0一样运行cpp文件?…「建议收藏」

    用vc2010怎么编译运行C语言,怎么用vc++2010学c语言程序设计?如何像vc6.0一样运行cpp文件?…「建议收藏」我简单做了一下,基本实现你要的功能,事件响应代码在下面得到路径之后,然后对文件进行文件内容的读取,读取到一个缓冲区内,然后用setwindowtext函数将内容显示在下面的空间上即可voidctest2dlg::onbutton1(){//todo:addyourcontrolnotificationhandlercodeherecstringstrpath;/…

  • p6操作教程_pc6视频教学

    p6操作教程_pc6视频教学在开发的过程中,我们经常会遇到由于sql语句书写错误导致的bug,那么如何来解决这种困扰呢?如果方法执行完了可以打印出完整的sql语句,就可以方便我们判断执行的是否正确,所以我们希望有一个可以打印sql语句的插件。p6spy就是一款这样的工具,下面给大家介绍一下p6spy的使用。使用p6spy需要做以下三步:1. 导入jar包:将jar包复制到项目中去,记得要buildpath一下。我用…

  • java反射机制

    java反射机制学习spring提到spring框架中的setter方法是使用反射机制实现的,反射机制到底是什么呢?找了一篇文章Java基础与提高干货系列——Java反射机制Java基础与提高干货系列前言今天

  • 【《重构 改善既有代码的设计》学习笔记4】构筑测试体系

    本篇文章的内容来自《重构 改善既有代码的设计》一书学习笔记整理并且加上自己的浅显的思考总结!如果想要进行重构,首要前提是 拥有一个可靠的测试环境。1、 自测代码的价值完成一个功能:设计+开发+调试,认真分析,程序员最多花费的时间不是开发(编码),而是用来调试。调试可能花费无数个小时,甚至通宵达旦。修复错误是快速的,而找出错误却是恶梦一场。当修复好一个错误,总是会有另一个错误的出现。而引…

  • ffplay播放器移植VC的工程:ffplay for MFC[通俗易懂]

    ffplay播放器移植VC的工程:ffplay for MFC[通俗易懂]ffplay播放器移植VC的工程:ffplayforMFC本文介绍一个自己做的FFPLAY移植到VC下的开源工程:ffplayforMFC。本工程将ffmpeg项目中的ffplay播放器(ffplay.c)移植到了VC的环境下。并且使用MFC做了一套简单的界面。它可以完成一个播放器播放视频的基本流程:解协议,解封装,视频/音频解码,视音频同步,视音频输出。此外还包含一些控制功能:播放,暂停/继

  • KindEditor是一套很方便的html编译器插件

    KindEditor是一套很方便的html编译器插件

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号