dotnet 职业技术学院

博客

dotnet 职业技术学院

WinForms 下的高性能笔迹方法

lindexi 发布于 2020-04-21

在 WPF 中可以通过 StylusPlugIn 的方式快速从触摸线程拿到触摸数据,而 WinForms 没有这个机制,但是可以通过 Microsoft.Ink 组件和 WPF 相同在 RealTimeStylus 服务拿到触摸点

win7 无法启动 WPF 程序 D3Dcompiler_47.dll 丢失

lindexi 发布于 2020-04-10

本文记录 D3Dcompiler_47 丢失问题,在安装 KB4040973 KB3178034 完成的 win7 系统可能出现 D3Dcompiler_47 丢失,让 WPF 等软件无法启动

VisualStudio 2017 项目格式 自动生成版本号

lindexi 发布于 2020-04-07

最近我把很多项目都使用了 VisualStudio 2017 新项目格式,在使用的时候发现一些比较好用的功能。 本文告诉大家如何使用 VisualStudio 2017 项目格式自动生成版本号

程序猿修养 日志应该如何写

lindexi 发布于 2020-03-30

在和小伙伴讨论日志的时候,小伙伴说的是文件的读写,而实际上的日志在广义上包含了任何的输出方式,无论是控制台还是文件。而日志记录在哪不重要,重要的是什么日志应该记录,应该怎么记录和记录什么内容

win10 uwp smms图床

lindexi 发布于 2020-03-29

本文告诉大家如何在 UWP 中使用 sm.ms 图床服务上传图片,获取图片外链

推荐 .NET/C# 开发者安装的几款代码分析插件或对应的代码分析 NuGet 包

dotnet 职业技术学院 发布于 2020-03-23

如果你使用的是旧版本的 Visual Studio,那么默认的代码分析规则集是“最小建议规则集”。基于这个,写出来的代码其实只能说是能跑通过而已。随着 Roslyn 的发布,带来了越来越多更强大的代码分析器,可以为编写高质量的代码带来更多的帮助。

作为 .NET/C# 开发者,强烈建议安装本文推荐的几款代码分析器。


推荐

  1. Visual Studio 2019 自带的分析器
  2. Microsoft Code Analysis
  3. Roslynator
  4. Code Cracker
  5. Meziantou.Analyzer

类型

这里的分析器分为 Visual Studio 扩展形式的分析器和 NuGet 包形式的分析器。

Visual Studio 扩展形式的分析器可以让你一次安装对所有项目生效,但缺点是不能影响编译过程,只能作为在 Visual Studio 中编写代码时给出提示。

NuGet 包形式的分析器可以让某个项目中的所有成员享受到同样的代码分析提示(无论是否安装插件),但缺点是仅针对单个项目生效。

简介

Visual Studio 2019 自带的分析器

重构提示

IDE0051

上图生效的分析器就是 Visual Studio 2019 自带的分析器。在可能有问题的代码上,Visual Studio 的代码编辑器会显示一些文字效果来提醒你代码问题。比如这张图就是提示私有成员 Foo 未使用。

Visual Studio 2019 自带的分析器的诊断 ID 都是以 IDE 开头,因此你可以通过这个前缀来区分是否是 Visual Studio 2019 自带的分析器提示的。

另外,自带的分析器可谓非常强大,除了以上这种提示之外,还可以提示一些重复代码的修改。比如你修改了某段代码,它会提示你相似的代码也可能需要修改。

Microsoft Code Analysis

Microsoft Code Analysis 分为两种用法,一个是 Visual Studio 扩展的形式,你可以去这里下载安装或者去 Visual Studio 的扩展管理界面搜索安装;另一个是 NuGet 包的形式,你可以直接在项目的 NuGet 管理界面安装 Microsoft.CodeAnalysis.FxCopAnalyzers

这款分析器也是微软主推的代码分析器,可以分析 API 设计问题、全球化与本地化问题、稳定性问题、性能问题、安全性问题、代码使用问题等非常多的种类。

比如下图是稳定性的一个问题,直接 catch 了一个 Exception 基类:

catch

配置提示

虽然你可以通过配置规则严重性来消除提示,但是这样写通常代码也比较容易出现一些诡异的问题而难以定位。

Microsoft Code Analysis 分析器的诊断 ID 都是以 CA 开头,因此你可以通过这个前缀来区分是否是 Microsoft Code Analysis 分析器提示的。

Microsoft.CodeAnalysis.FxCopAnalyzers 的 NuGet 包实际上是一组分析器的合集,包括:

如果你想安装这款 NuGet 包,并不需要特别去 NuGet 包管理器中安装,也不需要命令行,只需要去项目的属性页面,选择“安装”就好了。如下图:

安装分析器

Roslynator

是第三方开发者开发的,代码已在 GitHub 上开源,社区非常活跃:

提供了 500 多个代码分析和重构。更值得推荐的一个原因是他为 Visual Studio 原本的很多报告了问题的代码提供了生成解决问题代码的能力。

Code Cracker

Code Cracker 是第三方开发者开发的,代码已在 GitHub 上开源:

由于这款分析器的出现比 Visual Studio 2019 早很多,所以待 Visual Studio 2019 出现的时候,他们已经出现了一些规则的重复(意味着你可能同一个问题会被 Visual Studio 报一次,又被 Code Cracker 报一次)。

虽然部分重复,但 Code Cracker 依然提供了很多 Visual Studio 2019 和 Microsoft Code Analysis 都没有带的代码质量提示。

比如,如果你代码中的文档注释缺少了某个参数的注释,那么它会给出提示:

CC0097

Code Cracker 支持的所有种类的代码分析都可以在这里查得到:

Meziantou.Analyzer

这款插件是对其他几款分析器的重要补充。如果说其他几款分析器可以帮你解决一些基本设计问题或者 Bug 的话,这款分析器可以帮你发现更大范围的问题。

最典型的,也是我推荐这款分析器的最大原因是 —— 区域和本地化!

你的每一个 ToString(),每一个字符串比较,每一个字典的构造……他都提醒你需要考虑区域问题,然后提供给你区域问题的推荐代码!

提醒需要考虑区域问题

提供的建议

配置代码分析严重程度

你的项目中对于某项规则严重性的看法也许跟微软或其他第三方分析器不一样,因此你需要自己配置规则集的严重性。

关于如何配置代码分析严重程度,你可以阅读:

git 需要知道的1000个问题

lindexi 发布于 2020-03-23

2020-3-22-常见软件的GC算法解析

dotnet 职业技术学院 发布于 2020-03-22

今天和大家一起解析下常见的GC算法设计。


什么是GC

GC是一种软件进行自动的内存回收的方式。

如果软件运行过程中,发现某些对象没有了引用(或者称之为不可达)的状态时,就会启动GC过程。将这部分内存进行释放。以避免程序员因为忘记显示释放内存导致程序出现out of memory。

GC的过程

GC的过程主要分为标记、移动和压缩

标记

软件先分析堆中的所有内存对象,判断是否“存活”。

如果一个对象没有保持着被其他存活对象引用,就需要被清理

移动

将标记为存活的对象移动到另一个内存空间(老生代)

压缩

由于内存清理之后会出现很多碎片(非连续的小段可用内存),因此往往需要对其进行移动,确保大块的内存可用空间。(有时候移动和压缩会放在一起操作)

image-20200322161334489

GC算法分析

为什么GC时要移动对象至另一空间

这里我们先思考一个问题,为什么需要把存活的对象移动到另一个内存空间。

首先,GC是一个非常耗性能的过程。

因为在GC过程中,你的程序中各个对象的引用指向的内存地址可能发生改变。

image-20200322161546326

如果此时你仍然在执行程序,就可能访问到错误的内存地址。

因此GC期间会挂起程序的执行,也就是我们俗称的Stop the world。

在这一个限制下,我们设计GC算法的时候,必须做到尽可能少的进行GC

如何才能尽可能少的进行GC呢?

我们每次GC时,尽可能保证扫描的对象最大比例的释放内存。这样可用内存越多,触发GC的次数就会越少。

OK,我们再引进一个社会工程学知识,就是越新创建的对象,被清理的概率越高。

如果一个对象经过了一次GC,仍然存活了下来,那么它很有可能再下一次GC中存活。

那么将其放在一个单独的内存空间(老生代)中,可以有效的减少这些长生命周期对象的GC次数。

而仅对这些新生成的对象(新生代)进行GC,可以使用更少的对象扫描,完成近似相当的内存释放。

只有当老生代的内存空间也满了,才会进行老生代的GC。

image-20200322161745963

老生代对象移动到哪里?

我们再思考一下,如果老生代也满了,那么GC时内存对象还能移动到哪里去呢?

在开一个“老老生代”?那“老老生代”也满了呢?

我们当然不能无限的开辟这么多的内存空间,放置GC存活的对象,这样内存的有效使用率太低了。

这里我们会在原始的老生代内存空间直接移动对象,将那些被回收的对象产生的内存碎片压缩即可。

image-20200322161946782

.NET的GC设计

.NET的GC设计很简单,就同我们刚刚分析的情况基本一致。

它将内存分为三个区域,第0代,第1代,第2代。

所有的新对象的内存会分配至第0代。

当第0代满了,触发GC将第0代清空,将存活对象提升至第1代。

当第1代满了,再存活对象提升至第2代。

当第2代满了,GC后的存活对象直接在第2代的空间进行碎片压缩。

V8的GC设计

在.NET的GC设计中可能会出现这种情况,在第0代GC过程中,最新生成的内存对象被提升到了第1代中。

但是这些对象很可能是一些短生命周期的对象,仅仅是“偶然”在GC之前生成罢了。

但是这些对象被提升至第1代之后,很可能在之后的多次GC中,都不能被清理。

这些“无用”的内存占用就会消耗我们软件的内存空间。

V8 采用了另一种设计思想。

V8仅有新生代和老生代两个空间,并且它将新生代分成了From和To两个半空间。

每次新对象直接生成在From半空间内。

当触发GC时,From半空间内存活的对象被移动到To半空间内。

然后令To成为新的From半空间,From成为新的To半空间,即“半空间翻转”

在下一次GC时,检查GC存活对象,如果已经经历了一次GC,那么提升至老生代,否则还是移动到To半空间。

image-20200322162423865

在V8的这种设计下,对象必须经历2次GC后才能提升到老生代。

避免了因为生成时机问题,导致内存中的“生命周期”延长。

JVM的GC设计

V8半空间的设计也会带来的一个问题就是,新生代的内存可用空间只有一半。

那么有没有什么方式可以增加一些空间呢?

JVM设计GC时将新生代分成了三块,eden,s0和s1(From幸存空间,To幸存空间)。

所有的对象创建时,内存分配至eden区域。

而当第一GC时存活对象提升至s0区域(From幸存空间)。

再下一次GC时,会将eden和s0一起进行GC,并且将存活对象移动至s1区域(To幸存空间)。

然后和V8一样s0和s1,发生“幸存空间翻转”

image-20200322162634767

仅当对象在幸存空间存活超过了8次,才提升至老生代。

这种设计基于如下现实:每次GC仅有少部分对象存活。

因此JVM就将需要进行翻转的半空间缩小了,这样就能有更大的空间用于新对象内存分配。

后记

这里我们分析了.NET,V8和JVM的GC设计,但是并不是说明那种软件的GC算法更好,所有的算法架构都是进行一种设计取舍。

我们需要知道大部分情况下,GC能够直接帮我们处理内存问题。

而在那些内存敏感的应用场景,期望依赖某一软件的GC设计原生机制来自动处理,也是不会奏效的。


参考文档:

2020-3-22-64匹马8赛道选前8的算法解析

dotnet 职业技术学院 发布于 2020-03-22

今天和大家讲一道很火的面试题——64匹马8赛道选前8的算法解析。


题目

有64匹马,一共有8个赛道,想要找出最快的8匹马,要比赛最少多少轮才可以?

解析

这道题初步一看会让人觉得摸不到头脑。

我们试着先用图表示下。

假设每一匹马是一个图的一个节点,用有向线段A->B表示马A比马B快

最终我们可以找到一条从最快马到最慢马的单向路径。

那么我们可以有这样的约束:

  • 如果有A->B,A->C,B->C,我们只保留A与C之间最长的路径

image-20200322093328904

  • 如果从起始点出发,任意两点的路径长度一致,意味着这两点之间仍然需要比赛确定名次。

image-20200322093745354

所以我们就可以先搭建这样一个节点图。

这里每一个节点的数字代表马的真实排名,当然我们现在只是作为上帝视角知道这个排名。

image-20200322094442259

我们首先随机分成8组进行比赛,可以获得如下的有向图

为了实现连通,这里增加了一个dumb节点,表示所有节点的起始节点

image-20200322095048249

可以看到例如1-8节点同dumb节点的距离都是1,9-16节点同dumb节点的距离都是1,不满足约束。

所以1-8节点之间,9-16节点之间都需要进行比赛。

但是哪一些先比呢?

我们可以看到,如果节点1和节点2先比,且节点1比节点2快,那么dumb同节点2所在的子树中所有节点的距离都会加1,所以节点9同节点10 的快慢也自然出现了。

因此我们就可以总结出第三条约束:

  • 如果有多组相同距离的节点需要比较,优先让距离短的节点进行比较。

image-20200322095850571

OK,我们对节点1-8进行比赛。

我们可以看到同dumb节点距离为1的节点现在只有一个,即第一名已经获得。

我们还看到图中半透明的节点在这一轮之后距离已经大于8,所以直接可以再算法中抛弃

image-20200322100709171

所以简化图形得到如下结果

image-20200322101043795

此时同dumb距离为1的节点只有1个,满足约束。

节点2和节点9 同dumb距离都为2 ,需要进行比赛。

但是一次比赛有8个赛道呢,不可能只拿2匹马进行比较。

这里我们分析下,如果节点2赢了,那么节点3,9,10距离都是3,需要进行比较;

如果节点9赢了,节点2,17距离都是3需要进行比较。

所以我们可以把距离为3的节点3,10,17加入进行一起比赛。

image-20200322101947667

此时我们已经有了5匹马,还差3匹。但是距离为4的节点中有4匹马

这里在决策上我们只能任意选择其中的3匹。

因此结果从这里开始变化。

在加入距离为3的节点之前,我们可以出现的关系如图

image-20200322115504545

然后我们分别看看4种分支情况结果。

显然节点25在4种情况下都被淘汰,那么它参与比赛的意义都不大,因此不要节点25是最优

而相对的,节点9,10,11,17,18,都有可能是潜在的第4名,所以我们知道节点一定“需要”一次比赛,“战胜”这些潜在对手。因此不要节点4参与比赛的情况是最坏的。

因此我们可以得到另一条“潜在规则”:

  • 让事实上更快的节点优先参与比赛,可以得到次数的下限
  • 让事实上更快的节点最后参与比赛,可以得到次数的上限

image-20200322115556985

最少次数

所以我们如果选择最少次数结果如下

image-20200322122716505

对节点 5,6,7,9,10,12,13,20进行比赛

image-20200322123254962

再对节点7,8,9,14,15,22进行比赛获得结果

所以总共为8+1+1+1=11次

最多次数

所以我们如果选择最多次数结果如下

image-20200322124041330

再选择节点4,9,5,10,12,11,20,13

image-20200322124450417

再选择节点6,7,9,10,11,14,15,22

image-20200322124758247

最后让节点8和9比一次

这样上限就是8+1+1+1+1=12次

算法实现

在算法实现上,可以定义一个树,

每次从最上层开始,选择同层节点数大于1的节点,进行比赛

然后更新树,使节点选择最深的层数,并且抛弃深度大于8的子树‘

直至树中每个节点有且仅有一个子节点。

扩展性想法

当然作为面试题,除了这样的过程,还可以有各种思路扩展:

  • 比如如果可以计时,那么就可以8次跑完之类的

参考文档:

C# 枚举转字符串

lindexi 发布于 2020-03-20

有时候需要把枚举转字符串,那么如何把枚举转字符串?

win10 uwp 使用 asp dotnet core 做图床服务器客户端

lindexi 发布于 2020-03-17

本文告诉大家如何在 UWP 做客户端和 asp dotnet core 做服务器端来做一个图床工具

2020-3-15-一文看懂CAP定理

dotnet 职业技术学院 发布于 2020-03-15

今天和大家介绍下CAP定理。


什么是CAP定理

在了解什么是CAP定理之前,我们分别看下C,A,P是什么?

  • 一致性(Consistency):你对一个系统执行写操作后,紧接着的一次读操作能够获取正确的值。
  • 可用性(Avaliablity):在任意时刻对系统的请求都能得到及时响应
  • 分区容错性(Partition Tolerance):你的分布式系统允许任意节点之间的任意数量消息丢失。

为了方便大家理解,我下面使用图例来解释着三点的含义。

我们先建立一个具有2个节点的分布式系统,节点之间可以相互通信,进行状态同步。

客户端可以访问任意一个节点进行数据的读写。

image-20200315145251184

一致性

我们先看一致性。

我们对节点1写入状态$S_{1}$,然后立刻从节点2进行读取,如果读取的状态也是$S_{1}$,那么我们认为这个系统是满足一致性要求的。

image-20200315145932650

有同学说这个很难么?

对于本地数据库不难,但是对于分布式系统难。

因为对于分布式系统来说,节点1和节点2可能位于不同的物理设备,甚至是不同地区,因此,必须通过其他通信方式进行数据的同步,才能在节点2得到一致的数据。

image-20200315152105951

可用性

我们再看可用性。

可用性是在任意时刻对系统的请求都能得到及时响应,对于分布式系统来说是对任意节点的请求都可以得到响应。

有同学会说,这不就是保证硬件,网络等设施的正常么?这个不应该是运维同学的事情么?

要注意CAP定理中的可用性可不是在考虑这些硬件设施的可用性。

我们可以暂时假设客户端到系统的任意节点的网络是可靠的,这样看起来可用性是完全“没问题”的。

不过这种“没问题”真的是你想要的么?注意一下任意时刻,任意节点这个关键信息。

假如我们在节点2同步完成之前,就进行了节点2的状态读取,此时读出来的数据是$S_{0}$!

我们的一致性原则被破坏了。

现在还会觉得可用性简单么?

image-20200315152212546

分区容错性

我们先跳过可用性的拷问,来看下分区容错性。

什么是分区?分区指的是分布式系统中,各个节点存在2种以上不同的状态。

我们在下图中节点1和节点2的同步期间,两个节点的状态不一致,这个时刻就是一个分区时刻。

image-20200315153444143

分区容错指的是你的系统允许出现分区的情况,并且仍然能够正常运行。

CAP定理

回到主题,什么是CAP定理?

CAP定理就是说对于分布式系统,不能够同时满足一致性,可用性和分区容错性。

我们刚刚已经看到,在允许系统存在分区的情况下(即满足分区容错性),如果要保证任意时刻任意节点可响应(即满足可用性),那么读取的数据可能就会不一致(即一致性被破坏);

而如果我们期望在分区情况下保持读取数据一致(即同时满足分区容错性和一致性),那么要么在同步状态完成前,让客户端的请求等待或者直接返回失败,要么只允许从节点1进行读取,(即可用性被破坏)。

那有办法同时保证一致性和可用性么?有,例如我们常见的在单个节点部署的关系数据库,就可以认为同时保证了一致性和可用性。但是如果你是单节点,必然不会出现分区的情况,也就无从谈满足分区容错性了。

CAP定理是指导,不是实际规则

由于CAP定理中指出不能够同时满足一致性,可用性和分区容错性,许多同学往往会陷入所谓的“三选二”的陷阱中。而由于分布式系统中,节点之间的通信故障时不可避免的,即系统必须要求满足分区容错性,部分同学甚至得出了可用性和一致性不可兼得的结论。

不要被CAP定理限制住,套用加勒比海盗中的一句台词“The CAP Theorem is more what you call guidlines, than catual rules.”CAP定理是指导,不是实际规则。

我们不需要绝对

首先我们需要明确2点:

  • CAP定理是正确的。但是CAP定理中的场景都是绝对的场景,即强一致性,任意时刻可用性,绝对的分区
  • 我们不需要绝对。

我们使用具体的场景思考下CAP定理。

假如你的业务是新闻网站,有很多的读者(比如3000个)同时(1s之内)对你的文章进行点赞,不同的读者在刷新前看到的点赞数值相差5个,对你的业务影响有多大?

假如你需要在你的文章发布的第一时刻(比如200ms之内),你的所有读者就获得最新的文章么?如果你的读者最晚在发布后的5分钟之内刷到的还是昨天的“旧闻”,分布后的第6分钟才看的这篇文件的影响有多大?

我们必须要明确的一点是,对于分布式系统来说,分区仅仅是非常短暂的一个瞬时状态,最终会通过状态同步等方式结束分区,达到一致性。

我们的业务也许不能够允许发生长达1h的分区状态,但是例如100ms的分区状态我们是可以接受的。

因此我们可以在在CAP的理论框架下,根据业务进行适当调整,实现分布式系统的高可用和最终一致。

这也是BASE理论兴起的原因(Basically Available(基本可用),Soft State(软状态)和Eventually Consistent(最终一致性))

可以参考的举措

  • 对于一致性,写完立刻读的情形有多少?不在进行写操作的客户端能不能在短时间放弃一致性,获取高可用性?
  • 对于进行写操作的客户端(如发送博客),能不能利用本地缓存数据避免立刻读取刚刚写入的数据,或者直接使用其他界面仅通知写入完成?
  • 能不能让写完的用户直接读取刚刚写入的同一节点,而不是读取还未同步的其他节点,牺牲可用性换取一致性?

后记

我们了解了CAP定理,了解了一致性,可用性和分区容错性不可以同时满足。

我们也看到我们仍然可以通过业务调整实现最终一致的高可用分布式系统。

希望我们在实施架构设计时能够更好的运用CAP定理,而不是被其限制。


参考文档:

2020-3-10-PPT文档解析之母版

dotnet 职业技术学院 发布于 2020-03-10

今天和大家介绍下PPT文档解析中的母版。


什么是母版

我们平时看到的PPT展示,实际上是有多层内容组成的。

分别是页面(Slide),布局(SlideLayout)和母版(SlideMaster)

我们使用PowerPoint或者WPS打开PPT文件,调到母版视图,就能看到左边列出的母版和布局(或者叫版式)。

image-20200310194045365

PPT文件中,可以创建多个母版,每一个母版创建多个布局。

在制作PPT时,依赖于不同的布局进行创建,不但减少了重复的工作量,也大大减少了PPT文件的体积。

在渲染时,软件会依次在三个层级渲染母版,布局,页面中的元素。最终呈现的是三个层级叠加的显示。

image-20200310193703299

PPT文档中的母版

我们解压一个pptx文件,在其中的ppt文件夹下可以看到母版,布局,页面三者的信息。

image-20200310194701794

我们在slides文件夹下任意打开一个slide的xml文件,但是发现里面并没有任何布局或者母版的相关信息。

image-20200310195051790

这是因为,PPT文档中将例如资源,样式,母版等外部的信息,定义在一个单独的文件中,然后通过一个rels文件绑定两者的关系。

我们可以在slides\_rels文件夹下找到对应的.xml.rels文件,看到里面有一个relationship节点,指向了对应的SlideLayout文件的相对路径。

image-20200310195411129

同样的,SlideLayout文件也通过同样的方式指向SlideMaster文件。

image-20200310195658640

使用OpenXMLSDK解析

由于PPT文档将relationship放在的单独的文件,我们在解析PPT文档时,必须对多个文件进行读取,这里增加了我们在解析PPT文档时的难度。

对于母版来说,更困难的一点是,slide中没有任何地方引用SlideLayout对应的relationship 的id。

这意味着如果我不解析rels文件,我根本不知道这个页面是否使用了母版。

这使得单纯的xml解析方式进行PPT文档处理变得异常困难。

所幸是如果我们是使用C#的话,微软开源了一个叫OpenXMLSDK的库,帮助我们快速解析,查询office文档。

它直接将母版信息存储到对应slide节点的属性中,所以直接可以通过slide轻松查询到对应的母版

例如我期望做页面背景的解析,就可以使用下面的方法。

image-20200310200618518

这样大家自主解析PPT文档(或者是openxml格式文件)都会变得很简单。


参考文档:

2020-3-8-MVC、MVP、MVVM模式演变简析

dotnet 职业技术学院 发布于 2020-03-08

今天和大家简单介绍下GUI设计中MVC、MVP 以及 MVVM 架构模式的演变。

由于MVC等相关模式的定义,实现都各有不同,加之作者认识水平有限,如有纰漏或不足,万望指正。


从GUI开始

MVC、MVP 以及 MVVM都是GUI设计中的架构模式。

那我们就先从GUI开始,思考下这些模式的本质目的。

什么是GUI?wiki的定义是用于操作计算机的图形界面。

或者使用英文单词(interface)接口能够更加清晰理解它的本质。

GUI就是提供了一种图形化的方式便于用户操作计算机。

image-20200308142131368

当然这里的操作计算机不仅仅是狭义上的操作计算机硬件,还可以是操作存储,数据库,运行时程序的内存模型等等。

其实从这里我们就可以提取出两个概念。界面(View)和模型(Model)。

GUI程序就是为了解决用户通过View处理Model的需求。

image-20200308141659491

第一个设计——“MV”模式

既然我们刚刚分析了GUI程序中天然存在View和Model的两个概念,那我们在进行设计时,自然会想到的第一个模型就是上一个小节提出的View-Model模型。

用户通过View上的操作更新Model的数据。Model的数据改变后,更新View的显示状态。

很好,我们有了第一个GUI的设计结构

image-20200308141659491

我们已经有了一个“MV”模式,但是它真的足够好么?

模式的目的是为了提高复用性,减少开发工作。

我们可以分析下GUI中,哪些是变化的,哪些是不变的?然后把不变的部分抽出。当然我们在处理其他软件设计时,也可以采用类似方式操作。

OK,大部分情况下,Model是不变的,而View是多变的。比如不同主题配色,根据用户操作状态,显示部分数据等等,都会改变View,或者有些软件可以使用一个Model对应多个View

image-20200308144327452

这样我们每次变更都需要重写整个View。

MVC模式——复用

我们再看一下“MV”模式中,各个部分的职责。

Model是完全被动的,他不知道外面世界的存在,只需要通过观察者模式,再自身数据变更时向外发出通知。

因此可以适应于任意种类,数量的View。

而View,承担了显示Model的数据,以及接收用户输入,并且更新显示状态以及Model数据的功能。

所以”MV”模式中的依赖关系是这样的。

image-20200308151905840

那么这里面有没有什么是相对来说不变的呢?

有。接收用户输入,并且更新显示状态以及Model数据就是一个相对不变的功能。

试想一个社交类应用。用户可以在注册界面,个人空间等多个地方(View)更改自己的用户名(操作更新Model数据)。但是这类操作是通用逻辑,没有必要每个View都进行实现。

此外例如点击跳转,页面切换等业务,如果写在View中,也会造成View之间的相互耦合,不利于复用。

所以我们可以把这部分业务逻辑抽取到一个单独的模块叫做Controller。

这样我们就更新了三者的职责:

  • Model:存储数据,在变更时发出通知
  • View:根据Model的数据进行显示
  • Controller:接收用户输入,并操作View,以及更新Model

于是我们得到了一个新的模式——MVC模式,它的依赖关系如下。

相比于“MV”模式,一部分通用业务逻辑移动到了Controller,View变得更加轻量,易于扩展更新。

image-20200308152313340

当然,MVC在各个端的定义和实现也没有统一。比如如果没有View切换,Controller也不一定要依赖View。

例如Martin Fowler在这篇GUI Architectures文章中介绍的MVC就没有Controller和View的关键依赖。

image-20200308154049617

各个框架中MVC的实现方式,可以参考浅谈 MVC、MVP 和 MVVM 架构模式,其中有详细介绍,不再赘述。

MVP——可测试

可测试性是软件设计的一个重要的非业务需求。

我们看下MVC的可测试性。

我们都知道UI测试是最困难的测试之一。

由于View自身处理了Model数据渲染的逻辑,而这部分处在View中的逻辑就变得不容易测试了。

这种情况下,最简单的解决方案就是将这部分逻辑移动至容易测试的部分——我们的Controller。

这里因为Controller承担了一部分显示的逻辑,所以为了区分,就将其改名为Presenter。

这样我们就更新了三者的职责:

  • Model:存储数据,在变更时发出通知
  • View:根据Presenter的指导显示
  • Presenter:接收用户输入,决定View显示,以及更新Model

这里根据显示逻辑迁移的多少可以分为Supervising Controller和 Passive View模式

Supervising Controller承担了大部分的复杂逻辑,如下图所示。

image-20200308161958492

而Passive View则是将全部逻辑都交给了Presenter处理。

也正是View没有了渲染逻辑,所以他不需要从Model中拿数据,Model的数据更新只需要通知Presenter。

由Presenter处理后操作View渲染。

这样所有逻辑都可测,而且View同Model完成了解构。

image-20200308162442260

MVP解决了可测试性问题,但是也造成了Presenter的进一步庞大,而且和业务逻辑,甚至是显示逻辑也更加耦合。

MVVM——不同层次的模型抽象

MVVM是MVC的另一个变种,也是目前广泛使用的一种GUI模型。我们常见的WPF框架就是建立在MVVM模式的基础之上。

试想下有这样一个问题,我们要显示用户的博客空间。

我们期望在界面上让用户的昵称显示宋体、加粗、红色。

那么这个宋体、加粗、红色的信息应该放在那里呢?

Model层显然不行,这里的数据属于UI,不属于领域模型,加入Model层只会使Model更加庞大,且冗余。

View层?UI数据随View层的确是个好主意,但是如果这个数据会变化呢?比如当用户点击头像时,用户昵称要加粗;点击简介时,用户昵称不加粗。(大家就当这个设计师没有审美好了,一时间想不出更好的场景。。)

更好的方式仍然是显示和数据分类。

所以我们引入一个新的概念,ViewModel,来赋予Controller存储UI相关数据的能力。

这样我们就更新了三者的职责:

  • Model:存储数据,在变更时发出通知
  • View:根据ViewModel的数据进行显示
  • ViewModel:接收用户输入,更改Model数据,并更加Model的更新,更新自身数据并通知View

所以这三种的依赖变化如下。可以看到我们利用MVVM完成了根据可变性分离的单向依赖模型。

image-20200308165301003

对于WPF框架,还可以使用DataBinding和Command进行进一步解耦。

image-20200308165905308

后记

我们分析总结了GUI模式一步步的演变过程,以及各个模式解决的重点问题。

当然这里并不是说某个模式能够完全胜过另一个模式,而是根据具体场景选择合适的架构。

例如web应用这边使用MVVM就不是很合适,而更适合MVC。(Model和Controller在服务端,而View在用户端)

能够理解和明白各个架构模式的优劣才能够在使用时得心应手。


参考文档:

dotnet core 导出 COM 组件

lindexi 发布于 2020-03-06

在 dotnet core 3.0 支持将库导出为COM组件,本文告诉大家如何将代码导出为 COM 组件