用心理解设计模式——设计模式的原则

设计模式总是看完又忘了, 忘了再翻出来看。
我想,应该抽时间仔细捋一遍。

理想的软件实现应该是:依照功能需求设计接口,模块化组装,各模块之间只通过接口耦合,模块内部实现丝毫不关心。就像使用电子元件组装电子产品,可以做到即插即用,灵活拆卸更换,易于扩展功能,方便复用。

设计模式是前人总解出来,让软件开发变得清晰、需求变化变得容易的套路。有人说设计模式是荼毒,实际上,要么是因为他生搬硬套,拿着锤子看什么都是钉子; 要么走火入魔用力过猛,不知过犹不及。

设计模式的(或面向对象的)一系列原则如下。(以为很容易理解,实际 查阅理解+码字整理耗费NRatel两三天时间。。请擦亮双眼,若有错勿被误导)

开闭原则

概念:

软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的。

目的:

这是一个理想的目的。只通过扩展来应对需求的变化,不修改已实现好的代码,不会影响现已完成的功能。

一定要正确理解:

它就是为了解决 “每次加扩展功能都需要改动代码,新代码给旧代码带来bug,让问题越改越多” 的问题。
NRatel认为,每次需求变更,都应该优先考虑是否可以通过扩展,而不是修改类的方式去实现。如果不能扩展,就想办法通过重构使得可以扩展。万不得已再去直接修改类。实际上,抽象化、接口化设计是开闭原则的关键。因为系统的抽象层总相对稳定,很少或不需要修改。实际的业务只需要对抽象层进行具体实现和扩展。

避免走火入魔:

扫描二维码关注公众号,回复: 4147399 查看本文章

有种困境叫“无限预留扩展可能”,这会严重拖慢开发速度。


单一职责原则

概念:

一个类应该只有一个发生变化的原因。

目的:

修改某一功能时,不会影响到其他功能运作(因为一个功能只对应一个类),为类减负,将一个耦合的模块拆分为多个非耦合的模块。

一定要正确理解:

《大话设计模式》(不推荐入手,部分例子有些牵强附会) 这本书用了将近一整章,以“手机职责过多, 照相功能弱”举例说明单一职责原则。NRatel认为,这个举例极其失败和错误,严重偏离单一职责的概念和本意,非常误导人。。因为,首先每个成品必然要具备不只一种功能。成品作为最高层模块,由多个低层模块通过"接口"这样的低耦合方式组装。它虽然具备多职责,但对它进行修改(拆换低层模块)并不难,并不违背高内聚低耦合的准则。 另外,这个举例试图讲述“功能多,导致不能专精”这样的道理,这显然牛头不对马嘴啊。。 NRatel认为 “单一职责” 主要是针对中低层模块而言, 它本质上就是单纯的为了:细分中低模块的功能的职责,让在发生“某一功能故障或需求变更”时,只需拆卸更换该“功能故障或需求变更”对应的那一个中低层模块,而不用动其他模块,不影响其他功能运作。

避免走火入魔:

明白是什么是 “单一职责”!,控制粒度,适度拆分。“单一”并非纯粹的单一,而是指不必继续拆分的、变化时总是同时修改的“一个整体” 。不可能也绝不应该拆分到甚至“一个类最后只有一个行为方法”这样子。。。


接口隔离原则

概念:

客户(client)应该不依赖于它不使用的方法。

目的:

解除不应该出现的耦合,拆分臃肿接口,避免“只需要接口中一部分定义,却不需要另一部分定义,继承实现很尴尬”的问题。 同时,也提高了接口的复用性。

一定要正确理解:

理论上,接口被继承后,应该严格按语义实现每一个接口中的方法。 因为一旦继承一个接口, 其他模块就会认为你具备这一接口的功能, 就可能调用这个接口, 没有准确实现,就会出现问题。
经常遇到 “需要的方法集合 是 已有接口中方法集合的真子集” 这样的问题。
这时候一般有三种处理方式。1.错误的方式:直接继承,但只实现需要的方法,其他用不到的方法抛出异常。2.不推荐的方式:直接重新定义一个接口。这种方式虽然没啥大问题,但复用性不强。3.正确的方式: 将原接口拆分满足自己需求的接口A和其他B。 原接口继承A和B。

区别“单一职责原则”和"接口隔离原则":

单一职责原则是对类来说。主要为了拆分类的、无功能相关性的职责,以避免修改类的某一功能影响到其他功能。
接口隔离原则是对接口来说。主要用来防止“继承接口,但因为实际不需要,所以给出无效实现”这样的问题。这是一种完全无用的、无谓的耦合。

避免走火入魔:

按需拆分,不要无故拆的太零散。


依赖倒置原则

概念:

  1. 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
  2. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

目的:

消除高低层模块之间的直接耦合。避免低层模块扩展或改动影响高层模块。

一定要正确理解:

依赖倒置原则可以说是设计模式的最最最核心思想。
通常,人们惯用的思维方式是“认为低层不会变,先创建“不变”的低层,然后让高层依赖低层”(依赖可以认为是直接地 使用、调用、调动、管理等行为)。
能举的例子很多, 比如:
1),先构造底层框架、工具,然后让处于高层的业务层调用;
2),先煮个面,构造张三、李四这些客人吃面的行为;
。。。

看起来好像没毛病,实际
1).底层框架也会因为有bug而改动,这个时候,我们还是希望bug的修改不要影响到业务层。
2),吃面也可能因为换口味,改成吃米饭、海参、鲍鱼了,这个时候,不应该去改变张三李四的“吃”这个行为。

依赖倒置原则,鼓励面向抽象接口编程,让高低模块都依赖于抽象接口,以此建立稳定的抽象层,然后再在抽象层的基础上扩展。这样,高低模块之间交互仅使用抽象接口, 不发生其他交互依赖, 在抽象接口不变的情况下,无论低层模块怎么变化,都可以让高层模块保持原样。

应用到以上例子:
1),搭低层框架之前,先制定要由业务层调用的接口集合。不管底层怎么改bug,因为接口没变,业务层不用改动。
2),做面前先构建“食物类”,实现“可以被吃”的接口。张三李四都去调“食物被吃”这个方法,不管做什么,张三李四都可以不用操心了。

避免走火入魔。

1.接口之间可以直接相互依赖,不必再去依赖接口的父接口。
2.实际上,由低到高,最后总是要发生依赖。
已知,这些情况下,肯定会发生依赖。遇到这些情况,除了考虑 结合泛型,将直接依赖推迟到更高层(应该这样做。依赖发生在越高层越好),就不要浪费时间去避免了。
1).以参数形式传入到当前对象方法中的对象;
2).当前对象的成员对象;
3).成员集合中的元素;
4).当前对象创建的对象。


里氏替换原则

概念:

所有引用基类的地方都能够被其子类替换,并没有任何异常。
子类可以实现父类的抽象方法,但是不能重写父类的非抽象方法。
子类中可以增加自己特有的方法。
当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

目的:

降低类之间的继承耦合, 动态多态的理论基础。

一定要正确理解:

NRatel认为,里氏替换原则是针对面向对象的“继承”特性,再次阐述了“开闭原则”和“依赖倒置原则”。
开闭原则的表现:1).子类通过继承复用父类的方法,在此基础上扩展自己的行为方法,2).父类的抽象方法不会也不能在任何地方被调用, 继承并实现父类的抽象方法可以认为是一种对约定的扩展,这是对扩展开放。不允许子类修改父类非抽象方法,因为修改后子类会改变父类的行为,违反了继承的“子类 is a 父类”这一基本原则。这是对修改关闭。
依赖倒置原则的表现?:注意注意!当出现 “类A需要继承并重写类B的非抽象方法 ”时,几乎可以认为A类和B类其实不应该是父子关系,而应该是同属关系(同属于共同父类的两个子类)。 应该先寻找A类和B类的共同抽象父类C(或接口),然后让A、B分别继承并实现C类(或接口)。这样,A类和B类就不会产生“跑偏的继承”这样的耦合。所以最终,这个原则还是推荐面向接口和抽象编程。
举个例子就很明白了:比如要构造一个“狼”类, 因为狗和狼有很多相同点,所以你想要继承“狗”类,然而出现了不同点,狼是吃羊,而狗是保护羊。这时候千万不要继续继承且重写“保护羊”为“吃羊”。 而是应该去让狼和狗都去继承“犬科”这个共同的抽象类, 分别实现犬科“对羊产生何种行为”这个抽象方法。

和动态多态的关系:

动态多态有一个必要条件是“子类继承并重写父类方法”!那么“里氏替换原则”是不是和“动态多态”相互矛盾?
实际上不矛盾,而且动态多态正是里氏替换原则的完美展现。只是“子类继承并重写父类方法” 这句话 应该严格限定为 “子类继承并重写父类的抽象方法” (因为只要包含一个抽象方法这个的类就是抽象类,抽象类不能被实例化,所以没有实例,那它的任何方法,包括刚才的抽象方法都不可能被本类实例调用。那么,这个方法就完全和子类自己扩展的方法一样,只会被子类对象调用,不会被父类对象调用。所以完全不影响里式替换)。

动态多态的实现过程:

按照里式替换原则,可以首先针对抽象的父类(或接口)进行编程,然后子类继承并重写父类的抽象方法,再在运行时用子类替换父类(看起来是父类类型接受子类对象),完成动态绑定,这样,程序在实际执行时就会动态地执行实际接受到的特征对象的、被重写了的特征方法,而不是父类的方法,这个过程就是动态多态。

需要注意的是:

NRatel感觉(“窃以为!!!”,认为此处不对的兄dei,可以留言讨论) abstract 这个关键字对多态很重要,只有重写父类的抽象(重点!!!要抽象要抽象)方法才符合里氏替换的原则,它是正确、理想的方式。而 virtual 这个关键字很尴尬,因为 如果是在非抽象类中,被 virtual 声明的方法,既可能被本实例调用,也可能被子类继承后重写,这样就违背了里氏替换原则。 而如果在抽象类中,被virtual声明的方法,因为不存在本类实例,不可能被本类实例调用,似乎又和使用 abstract 声明一样了。


迪米特法则

概念:

又叫最少知道原则,一个对象应当对其他对象有尽可能少的了解,不和陌生人说话,只与直接的朋友通信。

目的:

降低类之间的调用耦合。让复杂的网状耦合变成简单的中心式耦合。

一定要正确理解:

先看一个示例:
when one wants a dog to walk, one does not command the dog's legs to walk directly; instead one commands the dog which then commands its own legs.

减少类间的调用耦合,便于系统模块化,不易在修改某一模块时影响其他模块。
迪米特法则要求尽可能降低软件实体之间通信的的宽度和深度。
尽可能将类设计为不变类(不可变对象),(本条很多人都一笔带过,要理解其具体含义,实际是为了降低类的“写权限”,除了最少知道,还要最少操作)。
尽可能地降低类及类成员的访问权限(降低类的“读权限”)。
尽可能减少与其他类通信。
只和朋友通信。陌生类之间如果有通信需求,可建立与两者都是“朋友”的“朋友类”,然后各自与这个“朋友类”通信。
朋友包括:当前对象的this;以参数形式传入到当前对象方法中的对象;当前对象的成员对象;成员集合中的元素;当前对象创建的对象。
实际上根本不用记,这些都是类无可避免、必须以及肯定会耦合的对象。反过来说,无可避免、必须以及肯定会耦合的对象都是类的朋友。

说到底,还是要面向接口编程和面向抽象编程。如果设计时注意一下,让接口之间松耦合,然后按照既定接口实现,就基本不会出现意外的、复杂的调用耦合。

避免走火入魔:

应该避免引入过多的、只是为了传递调用关系的中介类,使系统效率降低、臃肿不堪。


自问自答:

一、什么时候应该结合泛型?

可以被认为泛型和Object一样,是所有类型的基类(实际优于Obejct)。
1.当高层模块(Manager或工厂类等)需要持有低层模块(普通对象/产品类等)的实例(直接持有、聚合、组合),且低层模块 继承自接口/抽象类时,由于接口/抽象类不能被实例化,如果仍然想让高层模块不直接依赖低层模块,而是依赖低层模块的抽象(依赖倒置)。那么,此时应该使用泛型T。

2.当高层模块需要持有低层模块的实例(直接持有、聚合、组合),且低层模块 继承自 “类+接口” 或 “ 多个接口” 时, 由于无法使用其中的一个类或一个接口代表该低层模块的抽象,如果仍然想让高层模块不直接依赖低层模块,而是依赖低层模块的抽象(依赖倒置)。那么,此时应该使用泛型T。

需要限定 限定where T : 低层模块继承的接口/抽象类。
如果该抽象类是无成员的,可以使用 new(),
如果是有成员的要利用反射进行实例化。

反射创建实例的方法:
T t = (T)Assembly.GetAssembly(typeof(T)).CreateInstance(typeof(T).ToString());
或者
T t = (T)Activator.CreateInstance(typeof(T), parcelName);

二、接口和抽象类区别和使用?。

接口 是对对象的对外的行为(public方法)的定义。是一个准则、一个约束。 是最少知道原则的一种体现。
抽象类 是对对象行为的实现(包含private成员和方法)。更具体的,是对多个拥有相同行为的对象(不同对象可能拥有其他不同的行为)的公共部分的实现。
抽象类在层次上比接口高一层。
在使用时, 接口是第一选择,只在必须定义成员变量的时候考虑使用抽象类。因为:继承自抽象类的类,继承了抽象类的实现,灵活性受到约束。

--------------------------------------------------NRatel割--------------------------------------------------


NRatel
转载请说明出处,谢谢


猜你喜欢

转载自blog.csdn.net/NRatel/article/details/83663255
今日推荐