0%

面向对象的设计原则

写代码也是有原则的,我们之所以使用设计模式,主要是为了适应变化,提高代码复用率,使软件更具有可维护性和可扩展性。如果我们能更好的理解这些设计原则,对我们理解面向对象的设计模式也是有帮助的,因为这些模式的产生是基于这些原则的。这些规则是:单一职责原则(SRP)、开放封闭原则(OCP)、里氏代替原则(LSP)、依赖倒置原则(DIP)、接口隔离原则(ISP)、合成复用原则(CRP)和迪米特原则(LoD)。下面我们就分别介绍这几种设计原则。

  1. 单一职责原则(SRP):

    1. SRP(Single Responsibilities Principle)的定义:就一个类而言,应该仅有一个引起它变化的原因。简而言之,就是功能要单一。
    2. 如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其它职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。(敏捷软件开发)
    3. 软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。

    小结:单一职责原则(SRP)可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。责任过多,引起它变化的原因就越多,这样就会导致职责依赖,大大损伤其内聚性和耦合度。

  2. 开放关闭原则(OCP)

    1. OCP(Open-Close Principle)的定义:就是说软件实体(类,方法等等)应该可以扩展(扩展可以理解为增加),但是不能在原来的方法或者类上修改,也可以这样说,对增加代码开放,对修改代码关闭。
    2. OCP的两个特征: 对于扩展(增加)是开放的,因为它不影响原来的,这是新增加的。对于修改是封闭的,如果总是修改,逻辑会越来越复杂。

    小结:开放封闭原则(OCP)是面向对象设计的核心思想。遵循这个原则可以为我们面向对象的设计带来巨大的好处:可维护(维护成本小,做管理简单,影响最小)、可扩展(有新需求,增加就好)、可复用(不耦合,可以使用以前代码)、灵活性好(维护方便、简单)。开发人员应该仅对程序中出现频繁变化的那些部分做出抽象,但是不能过激,对应用程序中的每个部分都刻意地进行抽象同样也不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。

  3. 里氏代替原则(LSP)

    1. LSP(Liskov Substitution Principle)的定义:子类型必须能够替换掉它们的父类型。更直白的说,LSP是实现面向接口编程的基础。

    小结:任何基类可以出现的地方,子类一定可以出现,所以我们可以实现面向接口编程。 LSP是继承复用的基石,只有当子类可以替换掉基类,软件的功能不受到影响时,基类才能真正被复用,而子类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

  4. 依赖倒置原则(DIP)

    1. DIP(Dependence Inversion Principle)的定义:抽象不应该依赖细节,细节应该依赖于抽象。简单说就是,我们要针对接口编程,而不要针对实现编程。
    2. 高层模块不应该依赖低层模块,两个都应该依赖抽象,因为抽象是稳定的。抽象不应该依赖具体(细节),具体(细节)应该依赖抽象。

    小结:依赖倒置原则其实可以说是面向对象设计的标志,如果在我们编码的时候考虑的是面向接口编程,而不是简单的功能实现,体现了抽象的稳定性,只有这样才符合面向对象的设计。

  5. 接口隔离原则(ISP)

    1. 接口隔离原则(Interface Segregation Principle, ISP)指的是使用多个专门的接口比使用单一的总接口要好。也就是说不要让一个单一的接口承担过多的职责,而应把每个职责分离到多个专门的接口中,进行接口分离。过于臃肿的接口是对接口的一种污染。
    2. 使用多个专门的接口比使用单一的总接口要好。
    3. 一个类对另外一个类的依赖性应当是建立在最小的接口上的。
    4. 一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。
    5. “不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构。”这个说得很明白了,再通俗点说,不要强迫客户使用它们不用的方法,如果强迫用户使用它们不使用的方法,那么这些客户就会面临由于这些不使用的方法的改变所带来的改变。

    小结:接口隔离原则(ISP)告诉我们,在做接口设计的时候,要尽量设计的接口功能单一,功能单一,使它变化的因素就少,这样就更稳定,其实这体现了高内聚,低耦合的原则,这样做也避免接口的污染。

  6. 组合复用原则(CRP)

    1. 组合复用原则(Composite Reuse Principle, CRP)就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分。新对象通过向这些对象的委派达到复用已用功能的目的。简单地说,就是要尽量使用合成/聚合,尽量不要使用继承。
    2. 要使用好组合复用原则,首先需要区分”Has—A”和“Is—A”的关系。 “Is—A”是指一个类是另一个类的“一种”,是属于的关系,而“Has—A”则不同,它表示某一个角色具有某一项责任。导致错误的使用继承而不是聚合的常见的原因是错误地把“Has—A”当成“Is—A”.例如:鸡是动物,这就是“Is-A”的表现,某人有一个手枪,People类型里面包含一个Gun类型,这就是“Has-A”的表现。

    小结:组合/聚合复用原则可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;其次才考虑继承,在使用继承时,需要严格遵循里氏替换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。

  7. 迪米特法则(Law of Demeter)

    1. 迪米特法则(Law of Demeter,LoD)又叫最少知识原则(Least Knowledge Principle,LKP),指的是一个对象应当对其他对象有尽可能少的了解。也就是说,一个模块或对象应尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立,这样当一个模块修改时,影响的模块就会越少,扩展起来更加容易。
    2. 关于迪米特法则其他的一些表述有:只与你直接的朋友们通信;不要跟“陌生人”说话。
    3. 外观模式(Facade Pattern)和中介者模式(Mediator Pattern)就使用了迪米特法则。

    小结:迪米特法则的初衷是降低类之间的耦合,实现类型之间的高内聚,低耦合,这样可以解耦。但是凡事都有度,过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

DotNetty完全教程(七)

Excerpt

ChannelPipeline和ChannelHandleContext介绍ChannelPipeline是一系列ChannelHandler连接的实例链,这个实例链构成了应用程序逻辑处理的核心。下图反映了这种关联:ChannelHandlerContext提供了一个ChannelPipeline的上下文,用于ChannelHandler在Pipeline中的交互,这种交互十分的灵活,不仅…


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

ChannelPipeline和ChannelHandleContext

介绍

ChannelPipeline是一系列ChannelHandler连接的实例链,这个实例链构成了应用程序逻辑处理的核心。下图反映了这种关联:

ChannelHandlerContext提供了一个ChannelPipeline的上下文,用于ChannelHandler在Pipeline中的交互,这种交互十分的灵活,不仅是信息可以交互,甚至可以改变其他Handler在Pipeline中的位置。

特性

  1. 每一个Channel都会被分配到一个ChannelPipeline,这种关联是永久性的。在Netty中是关联,在DotNetty中这种关联被进一步的强绑定,变成了一个Channel中存在一个Pipeline。
  2. 对于Pipeline来说,入站口被当作Pipeline的头部,出站口被当作尾部。虽然我们看到有两条线,但是在Pipeline中其实是线性的,在事件传播的时候,如果Pipeline发现这个事件的属性(入站出站)跟下一个Handler不匹配,就会跳过这个Handler,前进到下一个。
  3. 一个Handler可以既作为入站处理器也作为出站处理器。
  4. 修改Pipeline

  5. 为了保证ChannelHandler处理事件的高效性,在Handler中不能有阻塞代码,但是如果遇到了一些阻塞API,就需要用到DefaultEventExecutorGroup,其功能是把这个事件的处理从原先的EventLoop中移除,送到一个专门的执行事件处理器中进行处理,从而不阻塞Pipeline。

ChanelPipeline的事件


我们可以看到fire方法都是调用下一个Handler中的方法,我们可以在合适的时机调用下一个Handler中的方法以实现数据的流动。

这里我们注意一下,Write方法并不会将消息写入Socket中,而是写入消息队列中,等待Flush将数据冲刷下去。

Context的API支持


Pipeline和Context

我们可以发现,Pipeline上也有fire–的方法,Context也有类似的方法,他们的差别在于,Pipeline或者Channel上的这些方法引发的事件流将从Pipeline的头部开始移动,而Context上的方法会让事件从当前Handler开始移动,所以为了更短的事件流,我们应该尽可能的使用Context的方法。

使用ChannelHandlerContext

  1. 获取当前Channel

    1
    2
    IChannelHandlerContext ctx = ...;
    IChannel channel = ctx.Channel
  2. 获取当前pipeline

    1
    2
    3
    4
    5
    6
    7
    // 注意一下在Netty中可以直接通过context获取pipeline,在DotNetty中需要从Channel中获取
    // Netty
    IChannelHandlerContext ctx = ...;
    IChannel channel = ctx.pipeline
    // DotNetty
    IChannel channel = ctx.Channel;
    IChannelPipeline pipeline = channel.Pipeline;
  3. 写入pipeline让事件从尾端开始移动

    1
    2
    3
    4
    IChannel channel = ctx.Channel;
    IChannelPipeline pipeline = channel.Pipeline;
    channel.WriteAndFlushAsync("Hello World!");
    pipeline.WriteAndFlushAsync("Hello World!");

注意,Write是出站事件,他的流动方向是从末尾到头部,这个一定要注意。在pipeline或者channel中写入事件,都是从最末尾开始流动,在Context中写入是从当前Handler中开始移动,这个我们已经在很多地方都说明了这样的不同。

应用

  1. 协议切换
    因为我们可以通过Context获取Pipeline的引用,获取了pipeline之后又可以动态的加载和删除Handler,利用这个特性我们可以实现协议的切换,
  2. 随时随地使用Context
    这里我们补充一个知识,Context和Handler的关系是一对一的,而不是一个Context对应多个Handler,这就让我们可以缓存下Context的引用,在任何时候进行使用,这里的任何时候可以是不同的线程。举个例子就是我们之前写的回声程序是在收到信息之后发送,但是复杂一点我们需要在按下按钮的时候发送一条数据,这时候我们可以在连接之后缓存Context的引用,在按下按钮的时候使用Ctx.Write();方法来发送一条数据。

线程安全

在Netty中,如果想要将一个Handler用于多个Pipeline中,需要标注Shared,同时需要保证线程安全,因为这里可能有多线程的重入问题。

异常处理

  1. 入站异常无论在何时引发,都会顺着Pipeline继续向下流动,如果最后的Handler没有处理,则会被标记为未处理。所以为了处理所有的入站异常,我们可以在pipeline的尾端通过复写ExceptionCaught来处理所有pipeline上的异常。
  2. 在出站Handler中获取异常在Netty中需要使用ChannelFuture以及ChannelPromise这里先不做叙述

DotNetty完全教程(一)

Excerpt

写本系列文章的目的我一直以来都在从事.NET相关的工作,做过工控,做过网站,工作初期维护过别人写的网络库,后来自己写网络库,我发现在使用C#编程的程序员中,能否写出高性能的网络库一直都是考验一个程序员能力的标杆。为了写出高性能的网络库,我查阅了很多资料,发现Java的Netty有着得天独厚的设计以及实现优势,Java也因为Netty的存在,在开发大吞吐量的应用程序中得心应手。我想,.NET程序…


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

写本系列文章的目的

我一直以来都在从事.NET相关的工作,做过工控,做过网站,工作初期维护过别人写的网络库,后来自己写网络库,我发现在使用C#编程的程序员中,能否写出高性能的网络库一直都是考验一个程序员能力的标杆。为了写出高性能的网络库,我查阅了很多资料,发现Java的Netty有着得天独厚的设计以及实现优势,Java也因为Netty的存在,在开发大吞吐量的应用程序中得心应手。

我想,.NET程序员为什么不能使用这么好的应用程序框架。好在,Azure团队写出了DotNetty,使得.NET程序员也可以迅速的,便捷的搭建一个高性能的网络应用程序,但是,DotNetty并没有多少资料,项目代码中也没有多少注释,这对我们的学习以及使用带来了极大的障碍。

我通过对于Netty的研究,一步步的使用DotNetty来创建应用程序,分析DotNetty实现了哪些,没有实现哪些,实现的有何不同,希望通过最简单的描述,让读者能够了解DotNetty,无论是在工作学习中快速搭建网络应用程序还是通过分析Netty的思想,为自己写的网络库添砖加瓦都是十分有意义的。

本系列文章参考了《Netty实战》,感兴趣的同学可以去看看这本书。

Netty是什么

Netty 是一款用于创建高性能网络应用程序的高级框架。

Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器
和客户端

DotNetty是什么

DotNetty是微软的Azure团队仿造Netty编写的网络应用程序框架。

优点

  1. 关注点分离——业务和网络逻辑解耦;
  2. 模块化和可复用性;
  3. 可测试性作为首要的要求

历史

  1. 阻塞Socket通信特点:
    1. 建立连接要阻塞线程,读取数据要阻塞线程
    2. 如果要管理多个客户端,就需要为每个客户端建立不同的线程
    3. 会有大量的线程在休眠状态,等待接收数据,资源浪费
    4. 每个线程都要占用系统资源
    5. 线程的切换很耗费系统资源
  2. 非阻塞Socket(NIO)特点:
    1. 如图,每个Socket如果需要读写操作,都通过事件通知的方式通知选择器,这样就实现了一个线程管理多个Socket的目的。
    2. 选择器甚至可以在所有的Socket空闲的时候允许线程先去干别的事情
    3. 减少了线程数量导致的资源占用,减少了线程切换导致的资源消耗
  3. Netty特点
    在这里插入图片描述

Netty设计的关键点

异步和事件驱动是Netty设计的关键

核心组件

  • Channel:一个连接就是一个Channel
  • 回调:通知的基础
1
2
3
4
5
6
7
8
9
10
11
12
public class ConnectHandler : SimpleChannelInboundHandler<string>
{
public override void ChannelActive(IChannelHandlerContext context)
{
// 新的连接建立的时候会触发这个回调
base.ChannelActive(context);
}
protected override void ChannelRead0(IChannelHandlerContext ctx, string msg)
{
throw new NotImplementedException();
}
}
  • Future:通知的另一种方式,可以认为ChannelFuture是包装了一系列Channel事件的对象。回调和Future相互补充,相互结合同时也可以理解Future是一种更加精细的回调。

    但是ChannelFuture在DotNetty中被Task取代

  • 事件和ChannelHandler
    ChannelHandler是事件处理器,负责处理入站事件和出站事件。通常每一个事件都由一系列的Handler处理。

本文参考资料以及截图来自《Netty实战》