设计模式一、简介及设计原则 什么是设计模式 设计模式是为了解决相同代码在项目中重复出现问题,在软件设计初期对功能需求进行规划勾勒蓝图。 设计模式与方法库不同,设计模式不是指某一段代码,而是指一种设计理念,用来解决特定的问题一种设计方式,化简为繁。 人们常常会将设计模式与算法混淆,因为两者都是在概念上都是解决特定的问题的方案。但算法明确的定义了解决方案的一系列步骤,而设计模式是对解决方案的更抽象的分析,在同一模式下两个不同的程序中具体实现的代码可能会不一样。 算法更像是一个菜谱:提供达成目标的明确步骤。 模式更像是蓝图:你可以看到最终的结果和模式的功能,但需要自己去确定实现步骤。 设计模式包含哪些内容 关于设计模式的描述通常包括以下几个部分意图:简要的描述问题和解决方案动机:进一步分析问题并说明是如果解决问题结构:展示设计模式的各个部分和他们之间的关系实现:提供流行的编程语言代码,能让人理解设计模式的思想 设计模式的分类 不同的设计模式的复杂程度,细节层次以及在整个系统的应用返回等方面各不相同。相同的问题可能用不同的模式都可以解决。比如我们要规划城市的道路,希望在通过十字路口时能够更加安全那么我们可以选择去安装红绿灯,也可以选择搭建天桥或则人行地下通道。 最基础的设计模式通常被称为惯用技巧,这类模式一般只能在一种编程语言中使用。通用的高程的设计模式是架构模式,可以使用任意的编程语言实现和使用,可以用于整个程序的架构设计。 设计模式可以根据意图和目的来进行分类,分别为:创建行模式:用来提供创建对象的机制,增加代码的灵活性及可复用性,共有五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式结构型模式:用于将对象和类组装成较大的结构,并同时保持结构的领过和高效,共有七种:适配器模式、装饰者模式、代理模式、门面模式(外观模式)、桥梁模式、组合模式、享模式行为模式:负责对象键的高效沟通和职责委派。共有10种:策略模式、模版方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式 软件设计原则 要减少开发的成本,最有效的就是代码的复用,相同的逻辑,相同的代码都可以抽象出来,写的越少错误的概率就越小,与其反复从头开始,不如在新对象中重用已有的代码,但是复用代码也会带来一些问题,可能会导致组件之间的耦合,会降低代码的灵活性,使得复用代码变得困难。始人之一的埃里希·伽玛1,在谈到代码复用中设计模式的角色时说:
变化是一个程序员生命中唯一不变的事情,比如你开发了一个Windows平台的游戏,但现在人们想要macOS版本的,当你创建了一个方形按钮的GUI框架,但过了几个月开始流行圆形按钮。相信每个开发人员都遇到过这种情况,会因为各种原因变更我们的代码。我们在解决问题后才能更好的理解问题,在第一版程序完成后,就可以开始做好重写代码的准备,因为你对这个问题已经有更深层次的理解了,可以在更多方面理解问题,所以回过头看第一版代码会不尽人意。或则当客户对之前的版本有新的要求提出,希望你能在原有的基础上增加或修改部分功能,所以要保证程序的可扩展性也是很重要的设计初衷 封装变化的内容 找到程序中经常变化的内容将其与不常修改的内容区分开。目的是将变更造成的影响最小化,加入程序是一首船,变更就是水下的水雷,如果船碰到水雷就会沉没。 这时我们可以将船体分割为过个独立的空间,进行安全的密封,使得即使损坏也不会影响到其他空间,这样当某一个空间被鱼雷击中,也不会导致船体沉没。 我们可以在编写代码时将经常变化的部分单独分离出来,保护其他不经常改动的代码受影响,如果需要修改,只需要花较少的时间就能找出要修改的地方,并且迅速改完。方法封装:比如我们开发一个信用卡账单分期计算器,不同的银行分期的费率不一样,而且可能银行会进行调整,那么我们可能需要经常的修改calculateInterest()计算利息方法,但我们在调用时其实并不关心这些费率,也不关心是如何计算的。 修改前
我们可以把费率放到一个单独的方法里,对这里的逻辑进行隐藏 修改后
这样手续费的费率就别隔离出来类层面的封装:一段还是件以后,发现随着需求的增加和改变,以前计算手续费的工作方法中添加了越来越多的职责。新增行为通常还会带来一些成员变量和方法,最后使得包含接纳他们的类的主要职责变得模糊起来,所以这些内容抽取到一个新类中会让程序变得更加清晰。
面向接口开发 面向接口开发,而不是面向实现,依赖于抽象类而不是具体类。 比如我们创建一个爱吃香肠的猫
这样写没有问题,那么我们要创建一只爱吃鱼的猫呢,或则一直爱吃蔬菜的猫,这样写就比较笨拙了,我们对香肠进行抽象,就变成了
明显可以看出来我们这种写法更加灵活。 我们再来介绍一个例子,比如有一家公司要开发一个软件,需要设计师设计软件,需要程序员写代码,需要测试测试软件,下面的写法,公司与员工之间紧密耦合
我们来对这个代码进行优化,我们将所有的员工抽象出来一个员工类,然后有抽象方法doWork(),那么上面的代码我们就可以写为
但是这里我们的 公司还是跟员工有着耦合,如果我们要引入其他类型的员工时,就需要修改我们的公司类了,为了解决这个问题,我们需要将员工方法抽象出来,由子类去实现,这样灵活度就提升了
这样如果想做不同类型的东西,具体需要什么员工由继承者实现,无需对基类进行修改 组合优于继承 继承可能是最简便的代码复用方式,如果有两个类的代码相同,就可以创建一个通用的类作为基类,但是继承后可能会出现一些问题。子类不能减少超类的接口:你必须实现父类的所有抽象方法,即使他对你没什么用。在重写方法需要确保新行为与基类中的版本兼容:因为子类的所有对象都可能被传递给以超类对象为参数的任何方法,所以不兼容可能会导致某个方法崩溃继承打破了超类的封装:因为子类拥有访问父类信息的权限,而且还有能相反,就是程序员为了进一步扩展的方便而让超类知晓了子类的内部详细内容子类与超类紧密耦合:超类中的任何修改都可能会影响子类功能通过继承复用代码可能产生平行继承体系:继承通常 仅发生在一个维度中。只要出现了两个以上的维度,你就必须创建数量巨大的类组合,从而使类层次结构膨胀到不可思议的程度 组合是代替继承的一中方法,继承表示为”是”的意思(汽车”是”交通工具),而组合则表示为”有”的意思(汽车”有”一个引擎) 比如我们要创建汽车,那么我们的汽车分为两种,汽车Car和卡车Truck,且车可能是电动的Electric也可能是汽油的Combustion。所有的车又有手动驾驶manualControl和自动驾驶Autopilot。
上面使用继承在多个维度上进行扩展,那么子类的数量就会成倍的增长,而且子类中会有大量的重复代码,那么我们可以用组合的方式来去设计,汽车有引擎和操作方式,我们将这两种高纬度用组合的方式包含进汽车。
最终我们可以通过不同的引擎实现和不同的驾驶方式来任意拼装成对象的车型。 SOLID原则 Single Responsibility Principle(单一职责原则):修改一个类的原因只能有一个,即一个类只负责一个职责 尽量让每个类只负责软件中的一个功能,并将其完全封装在该类中,这个原则是为了减少代码的复杂度,同一个功能写200行代码,不如使用几个明确的方法,这样寻找问题也会明朗,不至于当修改某个地方时,需要翻看全部代码才能知道你要修改的地方。如果类负责的东西过多的话,那么就会有很多事导致你去修改这个类,在你修改的时候有可能会牵连到你不希望改动的地方。 举例,我们有个收银程序,收钱记账是我们的功能,所以我们修改入账的细节会修改该方法,但是中间掺杂了计算活动优惠的地方,当我们商品的活动优惠修改时,也要修改这里,那就违反了我们的单一原则,我们就要将计算活动优惠这一块单独拿出来,以避免修改活动时需要修改我们的收银程序。 修改前
修改后
Open/closed Principle(开闭原则):对于扩展,类应该是”开放”的,对于修改,类应该是”封闭”的。 就是一个类允许被拓展,但不应被修改,你可以对此类创建子类,在子类中增加变量,方法或则重写方法,这被称为开放的,在java中如果类被标记了final那这个类就不再是开放的。如果已经确定某个类做好了充足的准备供其他类使用,定义的接口不会再修改,这个类就是个封闭的。 如果一个类已经通过了开发,测试和审核后,而且被其他类的代码使用的话,那么修改他的代码就会变成有风险的。你可以创建一个该类的子类来覆写它的某些行为,而不是直接修改该类。 这条原则并不能应用于所有对类的修改,比如你发现某个类存在某些缺陷,请直接对其进行修复,而不是使用创建子类来弥补父类的问题。 举个例子,我们的商城程序中有一个计算商品运费的订单类Order,针对不同的重量有不同的价格,但是当我们增加运输的方式时,比如空运,那么就要对Order类进行修改。这里可以使用策略模式来解决这样的问题 修改前:
修改后:
这样以后再添加运输方式就不需要修改Order类,可以通过拓展Transport类来实现新的运输方式。同时这个也满足单一职责原则。Liskov Substitution Principle(里氏替换原则):当你扩展一个类时,要保证子类的兼容性。即当创建子类时,要能保证在不修改客户端代码的情况下将子类对象作为父类对象进行传递。 该原则意味着,当你在创建子类并覆写父类的方法时,应该是对父类的方法进行扩展而不是完全重写,要保证方法的兼容性。这一点在开发框架和程序库时非常重要,因为其中的类将会在别人的代码中使用而你是无法直接访问和修改这些代码的。 1、子类方法的参数类型必须与其超类的参数类型相匹配或更加抽象。 假如我们有一个方法用于修理汽车repair(Car car)。客户端使用该方法时会将“汽车Car”对象传递到该方法。这时我们创建一个子类重写了repair()方法使其能够给任何“交通工具Transport,即Car的超类”维修。如果将该子类对象传递到客户端代码,程序仍然能正常工作,因为他可以维修任意的交通工具包括小汽车。但是如果我们覆写方法时将参数换为“ElectricCar电动车,即Car的子类”,那么当我们将该子类对象传递到客户端时,就可能会出现异常。当然在java中继承覆写方法已经限制必须与父类重写的方法相同,所以java继承中不会出现该情况。 2、子类方法的返回值类型必须与超类方法的返回值类型或其子类相匹配 假如我们有一个类的方法是buyCar():Car。客户端执行该代码返回结果是任意类型的“汽车”。子类重写时可以返回电动车buyCar():ElectricCar,客户端会到一个电动车,电动车当然也属于汽车,所以一切正常,但如果返回的是Transport交通工具,那么客户端调用时可能会返回飞机,那么就会出现问题了。同样的这在java的继承中也是不会出现的。 3、子类中的方法不应抛出基础方法预期之外的异常类型。这个java中也不会出现 4、子类不应该加强其前置条件 例如,基类方法中有一个int类型的参数,如果子类重写该方法时,要求传递给该方法的参数值必须为正数,否则抛出异常,这就属于加了前置条件。客户端代码之前能够传入负数,现在使用子类传入负数会导致系统出错。 5、子类不应该削弱其后置条件 加入某个类有个方法需要使用数据库,该方法应该在接受到返回值后关闭所有活跃的数据库连接。但你创建了一个子类并对其进行修改,是的数据库保持连接以便重用。但客户端可能对你的意图一无所知。由于他认为你已经关闭的数据库的连接,所以他在调用后直接退出了方法,导致了没用的数据库连接继续消耗系统资源造成资源的浪费。 6、超类的不变量必须保留 这很可能是所有规则中最不“形式”的一条。不变量是让对象有意义的条件。例如,猫的不变量是有四条腿、一条尾巴和能够喵喵叫等。不变量让人疑惑的地方在于它们既可通过接口契约或方法内的一组断言来明确 定义,又可暗含在特定的单测试和客户代码预期中。 不量的规则是最容易违反的,因为你可能会误解或没有意 识到一个复杂类中的所有不变量。因此,扩展一个类的最安全做法是引入新的成员变量和方法,而不要去招惹超类中已有的成员。当然在实际中,这并非总是可行。 7、子类不能修改超类中私有成员变量的值 什么?这难道可能吗?原来有些编程语言允许通过反射机制来访问类的私有成员。还有一些语言(Python 和 JavaScript)没有对私有成员进行任何保护。 我们来举个违反替换原则的例子
只读文件中的保存行为没有任何意义,因此子类试图在重写后的方法中重置基础行为来解决这个问题。只读文件 ReadOnlyDocuments 子类中的 save 保存 方法会在被调用时抛出一个异常。基础方法则没有这个限制。这意味着如果我们没有在保存前检查文档类型,客户端代码将会出错。代码也将违反开闭原则,因为客户端代码将依赖于具体的文档类。如果你引入了新的文档子类,则需要修改客户端代码才能对其进行支持。
修改后我们把只读文档类作为层次结构中的基类后,这个问题得到了解决。一个子类必须扩展其超类的行为,因此只读文档变成了层次结构中的基类。可写文件现在变成了子类,对基类进行扩展并添加了保存行为。 Interface Segregation Principle(接口隔离原则):客户端不应被强迫依赖于其不使用的方法。 应尽量细化接口的功能,使得客户端不必实现不需要的方法。根据接口隔离原则,你必须将“臃肿”的方法拆分为多个颗粒度更小的具体方法。客户端必须仅实现其实际需要的方法。否则,对于“臃肿”接口的修改可能会导致程序出错,即使客户单根本没有使用修改后的方法。 java中只允许单继承,但可以实现多个接口,所以我们可以将功能分类,分为不同的接口,在接口中定义相关方法,如果有需要可以实现所有接口,也可以只实现部分接口。 例如我们创建了一个程序库,为了能让程序方便的与多种云计算供应商进行整合。但我们最初只支持阿里云。所以我们使用阿里云的功能定义了一个接口类,但后面我们又接入了腾讯云发现阿里云中的功能腾讯云中只实现了一部分,那我们在接入腾讯云时,根据之前的接口定义会有一些无法实现的方法。
虽然你可以实现该方法,直接返回无用的结果,但这并不是最好的解决方案,我们可以将阿里云的功能细分出多个接口,这样接入其他云服务时可以根据供应商的不同功能来实现不同的接口。
但我们尽可能不要过度使用该原则,不要对已经划分很具体的接口进行再次划分。创建的接口越多,代码就越复杂。Dependency Inversion Principle(依赖倒置原则):高层次的类不应该依赖于次层次的类。两者都应该依赖于抽象接口。抽象接口不应该依赖于具体实现。具体实现应该依赖于抽象接口。 低层次的类:实现基础操作(例如磁盘操作、网络数据传输、连接数据库等) 高层次的类:包含复杂业务逻辑以指导低层次类执行特定操作。 例如我们在业务逻辑中应调用openReport(file),而不是先调用openFile(file),再调用readBytes(stream),再调用closeFile(file)这样一系列的方法。而是将这些方法抽象成接口在openReport中调用,那么这些接口就被视为是高层次的,现在我们可以基于这些接口来创建高层次的类,而不是使用低层次的具体类。这要别原始的依赖关系灵活很多。当低层次的类实现了这些接口,他们就会通过接口依赖于业务逻辑层,从而倒置了原始的依赖关系。 依赖倒置原则通常和开闭原则共同发挥作用:你无需修改已有类就能用不同的业务逻辑类扩展低层次的类。 举个例子,高层次的预算报告类(Budgereport)使用低层次的数据库类(MySQLDatabase)来读取和保存其数据。这意味着低层次类中的任何改变(如数据库发布新版本)都有可能会影响到高层次的类,但高层次的类不应该关心这些存储的细节。
修改前,高层次的类依赖于低层次的类。 我们可以创建一个描述读写操作的高层次接口,并让报告类使用该接口替代掉低层次的具体类。然后再修改或则拓展低层次的原始类来实现业务逻辑声明的读写接口
现在的结果是原始的依赖关系被倒置:低层次的类依赖于高层次的抽象。
2024最新激活全家桶教程,稳定运行到2099年,请移步至置顶文章:https://sigusoft.com/99576.html
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。 文章由激活谷谷主-小谷整理,转载请注明出处:https://sigusoft.com/19131.html