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

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

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

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

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

  • 开闭原则

概念:

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

目的:

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

一定要正确理解:

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

避免走火入魔:

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


  • 单一职责原则

概念:

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

目的:

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

一定要正确理解:

  《大话设计模式》(部分例子有些牵强附会) 用了将近一整章,以 “手机职责过多, 照相功能弱”举例说明单一职责原则。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类就不会产生“跑偏的继承”这样的耦合。所以最终,这个原则还是推荐面向接口和抽象编程。
举个例子就很明白了:比如要构造一个“狼”类, 因为狗和狼有很多相同点,所以你想要继承“狗”类,然而出现了不同点,狼是吃羊,而狗是保护羊。这时候千万不要继续继承且重写“保护羊”为“吃羊”。 而是应该去让狼和狗都去继承“犬科”这个共同的抽象类, 分别实现犬科“对羊产生何种行为”这个抽象方法。

和动态多态的关系:

动态多态有一个必要条件是“子类继承并重写父类方法”!那么“里氏替换原则”是不是和“动态多态”相互矛盾?
实际上不矛盾,而且动态多态正是里氏替换原则的完美展现。只是“子类继承并重写父类方法” 这句话 应该严格限定为 “子类继承并重写父类的抽象方法” (因为只要包含一个抽象方法这个的类就是抽象类,抽象类不能被实例化,没有实例,就压根不会出现与子类进行替换的情形)。

动态多态的实现过程:

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

需要注意的是(理解 abstract关键字 和 virtual关键字,用法上的本质区别):

  abstract 和 virtual 用法上最重要的区别是,抽象方法必须被子类实现,虚方法子类可以选择性实现。

  abstract 关键字,对多态很重要。
        子类重写父类的抽象方法完全符合里氏替换的原则,可以完全放心使用。

  virtual 关键字,很特别。
        如果在抽象类中,抽象类的子类不可能与其发生里氏替换(抽象类没有本类实例),所以 virtual 可以放心使用;
        而在非抽象类中,被 virtual 声明的方法,既可能被本类实例调用,也可能被子类继承后重写,此时子类和父类就有不同的实现,就违背里氏替换原则。
        但其实 如果 virtual 方法是个空方法 或 子类在重写时又调用了父类的该方法,则也不算违背!相当于子类对父类进行扩展,而不是修改。

  所以,使用 virtual 的安全姿势应该是:只在抽象类中使用  virtual 方法为空 子类重写时又调用父类的该方法。

  virtual  常用于 钩子方法,参考 模板方法模式


  • 迪米特法则

概念:

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

目的:

降低类之间的调用耦合。让复杂的网状耦合变成简单的中心式耦合,便于系统模块化,不易在修改某一模块时影响其他模块。

一定要正确理解:

先看一个很值得回味的例子:
    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.

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

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

避免走火入魔:

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


  • 自问自答:

一、泛型的用途?

可以认为泛型T和Object一样,是所有类型的基类(实际优于Obejct)。

1.想在高层模块内部统一实例化低层模块的类时,可以用T(传递类,而不是类的对象)。

public abstract class BaseItemClass
{
    public abstract void Print();
}

public class ItemClass : BaseItemClass
{
    public override void Print() { Debug.Log("1111111"); }
}

//public class Class
//{
//    public void Print(BaseItemClass x)
//    {
//        x.Print();
//    }
//}

//public void Main()
//{
//    Class x = new Class();
//    x.Print(new ItemClass());
//}

public class Class<T> where T : BaseItemClass, new()
{
    public void Print()
    {
        new T().Print();
    }
}

public void Main()
{
    Class<ItemClass> x = new Class<ItemClass>();
    x.Print();
}

2. 用T代表一个复杂类型。“类+N个接口” 或 “N个接口”。

public interface ILog
{
    void Log();
}

public abstract class BaseItemClass
{
    public abstract void Print();
}

public class ItemClass : BaseItemClass, ILog
{
    public override void Print() { Debug.Log("1111111"); }

    public void Log() { Debug.Log("22222"); }
}

public class Class<T> where T : BaseItemClass, ILog, new()
{
    public void Print()
    {
        T t = new T();  //T 是一个 BaseItemClass+ILog 的复杂类型
        t.Print();
        t.Log();
    }
}

public void Main()
{
    Class<ItemClass> x = new Class<ItemClass>();
    x.Print();
}

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

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

或者,

ctor = type.GetConstructor() ;   ctor.Invoke();

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

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

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


NRatel
转载请说明出处,谢谢


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NRatel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值