题 MVC和MVVM有什么区别?


标准的“模型视图控制器”模式与Microsoft的模型/视图/ ViewModel模式之间是否存在差异?


1099
2018-03-20 20:09


起源


我一直在讨论这个问题所以我写了一篇文章解释了MVC,MVVM和MVP之间的区别。人们错过的一个区别是在MVC中,控制器选择视图(例如web服务器)。选择视图后,视图的状态未连接到控制器。 MVVM,视图的状态持续连接到ViewModel(具有一些类似于控制器的功能)。想想WPF / datacontext。实际上还有更多。 MVC vs MVP vs MVVM:差异解释 - students.cs.byu.edu/~cs340ta/spring2017/notes/08-MVC/... - i8abug
请注意,虽然MVVM是由Microsoft创造的,但许多非Microsoft开发人员和项目已经开始采用这种模式。这个评论是由麻烦的MS-haters部门带给你的。 - BoltClock♦
在MVVM工作了很长时间之后,我第一次使用MVC是令人沮丧的,直到我了解到我可以使用MVVM中的绑定技术来回传递ViewModels到浏览器。但正如Joel所说,从浏览器返回状态的唯一方法是在表单(使用名称/值)对中发布更改。如果你不太了解这一点。你将在MVC中度过难关。只需将控制器视为视图的依赖注入器即可完成设置。 - John Peters
这是一个关于高级[设计模式]的热烈问题。我很乐意建议在答案上使用图表。 - Ricardo
这是Joel文章的存档版本: web.archive.org/web/20150219153055/http://joel.inpointform.net/... - Tereza Tomcova


答案:


MVC / MVVM不是 两者任一 选择。

在ASP.Net和Silverlight / WPF开发中,这两种模式以不同的方式出现。

对于ASP.Net,MVVM习惯了 双向绑定 视图中的数据。这通常是客户端实现(例如使用Knockout.js)。另一方面,MVC是一种分离问题的方式 在服务器端

对于Silverlight和WPF,MVVM模式更具包容性和可以 出现 充当MVC的替代品(或将软件组织成独立职责的其他模式)。经常出现在这种模式中的一个假设就是 ViewModel 只需更换控制器 MVC (好像你可以替代 VM 对于 C 在首字母缩略词和所有将被原谅)...

ViewModel可以  必然取代了对单独控制器的需求。

问题是:要独立测试*,特别是在需要时可重用,视图模型不知道哪个视图显示它,但更重要的是 不知道它的数据来自哪里

*注意:在实践中,控制器会从ViewModel中删除需要进行单元测试的大部分逻辑。然后,VM变成一个愚蠢的容器,几乎不需要测试。这是一件好事,因为VM只是设计师和编码器之间的桥梁,所以应该保持简单。

即使在MVVM中,控制器通常也包含所有处理逻辑,并决定使用哪个视图模型在哪些视图中显示哪些数据。

从我们目前所看到的ViewModel模式的主要好处是从XAML代码隐藏中删除代码 使XAML编辑成为一项更独立的任务。我们仍然会根据需要创建控制器,以控制(无双关语)我们应用程序的整体逻辑。

我们遵循的基本MVCVM指南是:

  • 查看 显示某种形状的数据。他们不知道数据来自哪里。
  • 的ViewModels 持有某种形状的数据和命令,他们不知道数据或代码的来源或显示方式。
  • 楷模 持有实际数据 (各种背景,商店或其他方法)
  • 控制器监听和发布事件。控制器提供控制所见数据和位置的逻辑。控制器向ViewModel提供命令代码,以便ViewModel实际上可重用。

我们也注意到了 雕塑代码框架 实现MVVM和类似于Prism的模式。它还广泛使用控制器来分离所有用例逻辑。

不要认为控制器已被View-models淘汰。

我已经开始了一个关于这个主题的博客,我将尽我所能添加。将MVCVM与通用导航系统相结合存在一些问题,因为大多数导航系统只使用视图和虚拟机,但我将在后面的文章中介绍它。

使用MVCVM模型的另一个好处是 在应用程序的生命周期中,只有控制器对象需要存在于内存中 并且控制器主要包含代码和少量状态数据(即微小的存储器开销)。与需要保留视图模型的解决方案相比,这使得内存密集型应用程序要少得多,并且它非常适合某些类型的移动开发(例如,使用Silverlight / Prism / MEF的Windows Mobile)。这当然取决于应用程序的类型,因为您可能仍需要保留偶尔缓存的VM以获得响应。

注意:这篇文章已被多次编辑,并没有专门针对所提出的狭义问题,所以我已经更新了第一部分,现在也涵盖了这一点。在下面的评论中,大部分讨论仅涉及ASP.Net,而不是更广泛的图景。这篇文章旨在涵盖在Silverlight,WPF和ASP.Net中更广泛地使用MVVM,并尝试避免人们用ViewModel替换控制器。


579
2017-08-22 09:19



是不是MVC用于构建整个(Web)应用程序,而MVVM在MVC的View组件中使用?因此整个范例将是M(VVM)C - Tomasz Zielinski
@Tomasz Zielinski:是的,但“他们被使用的地方”不是问题(或者我的回答)。我的观点是控制器在MVVM中仍然有用。 - Gone Coding
我同意。我的评论是由突然的启蒙引起的,而不是因为我不同意你。 - Tomasz Zielinski
@Justin:我看到那句话的措辞有点含糊不清。我实际上意味着更容易支持所有组件的单元测试,而不仅仅是改进ViewModels的测试(正如你所指出的那样,在MVCVM中实际上并没有那么多......这就是你想要的)。控制器的真正好处在于,您实际上正在从ViewModel(人们不断推迟控制器逻辑)中移除大多数测试要求,并将其放在可以测试的地方(主要是控制器和模型)。重用注释特定于该句子中的VM。我编辑过它。 - Gone Coding
@TomaszZielinski M(MVVM)C - Mohamed Emad


我认为理解这些首字母缩略词的最简单方法就是暂时忘掉它们。相反,考虑一下它们所源自的软件。它实际上归结为早期网络和桌面之间的差异。

第一个首字母缩写词MVC起源于网络。 (是的,它可能曾经存在过,但网络是如何向大量的Web开发人员推广的。)想想数据库,HTML页面和代码。让我们稍微改进一下以获得MVC:对于»database«,让我们假设数据库加接口代码。对于»HTML pages«,我们假设HTML模板和模板处理代码。对于»code inbetween«,我们假设代码将用户点击映射到可能影响数据库的操作,肯定会导致显示另一个视图。就是这样,至少为了这个比较的目的。

让我们保留这个网络内容的一个特征,不像今天这样,但是十年前它存在,当Javascript是一个卑微,卑鄙的烦恼时,真正的程序员很好地避开了:HTML页面本质上是愚蠢和被动的。浏览器是瘦客户端,或者如果你愿意的话,是一个糟糕的客户端。浏览器中没有智能。整页重新加载规则。每次都会重新生成»view«。

让我们记住,这种网络方式尽管风靡一时,但与桌面相比却是非常落后的。如果您愿意,桌面应用程序是胖客户端或富客户端。 (甚至像Microsoft Word这样的程序也可以被认为是客户端,文档的客户端。)他们是充满智慧的客户,充满了关于他们数据的知识。他们是有状态的。它们缓存了他们在内存中处理的数据。没有这样的废话作为整页重新加载。

这种丰富的桌面方式可能是第二个首字母缩写词MVVM。不要被信件所愚弄,因为C的遗漏仍然存在。他们需要。什么都没有被删除。我们只添加一件事:有状态,缓存在客户端上的数据(以及处理该数据的智能)。该数据,实际上是客户端上的缓存,现在称为“ViewModel”。它允许丰富的交互性。就是这样。

  • MVC =模型,控制器,视图=基本上是单向通信=差的交互性
  • MVVM =模型,控制器,缓存,视图=双向通信=丰富的交互性

我们可以看到,使用Flash,Silverlight,以及 - 最重要的是 - Javascript,网络已经接受了MVVM。浏览器不能再被合法地称为瘦客户端。看看他们的可编程性。看看他们的记忆消耗。查看现代网页​​上的所有Javascript交互性。

就个人而言,我发现这个理论和首字母缩略词业务通过查看它在具体现实中所指的内容更容易理解。抽象概念是有用的,特别是在具体问题上展示时,所以理解可能会完整。


212
2018-02-13 14:11



MVC并非源自网络。 Trygve Reenskaug在20世纪70年代将MVC引入Smalltalk-76。 - Arialdo Martini
即使它被改为“MVC通过Web应用程序设计推广”。我认为这是没有适当引用的推测。 - Dan Bechard
Arialdo:谢谢,我不知道Smalltalk-76。 (当时玩其他玩具。:)除了笑话,有趣的是这些概念有多久了。 - @Dan,我写的是:“[MVC]可能在[网络]之前就已存在,但网络是如何在大众网络开发者中普及的。”我仍然认为这是正确的。我没有引用它,但后来我觉得我不需要,因为当我在过去十年初作为网络开发人员开始时,MVC大规模普及是我个人经历的一部分。 Apache Struts当时很流行,有很多针对MVC的bean。 - Lumi
MVC不是“基本上是单向通信”,因为浏览器一直在发出Get和Posts。 Get和Posts都可以更改查询字符串中的字段值。这为浏览器提供了将信息发送回控制器的充分机会。 MVC建立在HTTP 1.0之上,它总是考虑到双向通信。 - John Peters
谢谢Lumi。这对我来说比其他答案更有意义。这是对的吗?我不知道。但从我的角度来看,它至少是连贯的。 - gcdev


MVVM  模型 - 视图ViewModel 类似于MVC, 模型 - 视图控制器

控制器 被替换为 视图模型。 ViewModel位于UI层下方。 ViewModel公开视图所需的数据和命令对象。您可以将此视为一个容器对象,该视图用于获取其数据和操作。 ViewModel从模型中提取数据。

拉塞尔东 博客会更详细地讨论 为什么MVVM与MVC不同


166
2018-03-20 20:18



句子“控制器被视图模型替换”是不正确的。在MVVM中,控制器的作用是数据绑定(或者如果使用它,则按约定绑定)。 - DaniCE
MVVM只有在使用WPF的双向数据绑定时才有意义。否则MVC / MVP等就足够了。 - Jeff
@DaniCE:Josh Smith: If you put ten software architects into a room and have them discuss what the Model-View-Controller pattern is, you will end up with twelve different opinions. … - sll
@OmShankar第11个不是来自你自己。共有10人,总共12人。这句格言意味着这些模式的定义是如此开放,以至于至少有两个人会混淆到足以产生一种以上的观点。 - Dan Bechard
@DaniCE这实际上是WPF数据绑定的重点,微软发明了MVVM,因为它可以完全绕过控制器,(声称“控制器被视图模型替换”这句话是不正确的,因为有一个幕后的控制器,基本上就像声称一个声明“更高级别的语言取代使用更具可读性的神秘机器代码”是不正确的,因为幕后机器语言仍在使用......) - yoel halb


首先,MVVM是MVC模式的一个进程,它使用XAML来处理显示。 本文 概述了两者的一些方面。

Model / View / ViewModel架构的主要推力似乎是在数据之上(“模型”),还有另一层非可视组件(“ViewModel”),它们更紧密地映射数据的概念到数据视图的概念(“视图”)。它是View绑定的ViewModel,而不是Model直接绑定的ViewModel。


85
2018-03-20 20:17



我认为你引用的段落很好地总结了恕我直言。 ViewModel的一个方面是它是视图模型的展平/更改版本。许多其他MV *模式绑定了 真实 模型。 - Daniel Auger
“许多其他MV *模式再次绑定真实模型”?真的吗?我认为视图总是应该绑定到MVC中的控制器,无论如何。 - PlagueHammer
Nocturne:在经典的MVC中,View与控制器没什么关系,它主要与Model绑定。把它想象成一个机器人 - 模型代表机器人关节的位置,View是一个液晶显示器,你可以看到机器人,控制器是例如键盘。在这样的设置中,视图取决于模型,即机器人的空间位置,您可以在监视器上看到它是模型的直接表示。 - Tomasz Zielinski
@Nocturne daniel似乎在说正式所有MV *都应该使用一个单独的VM,许多开发人员只是忽略它,并传递实际模型,事实上MVC的规范中没有任何东西不允许它,但是在MVVM中VM必须负责模型和视图之间的转换 - yoel halb
我会这样说:模型是DB架构的壁橱。运行查询时,它可以将数据投影到模型层的强类型中。视图模型是事物的集合,包括模型对象,但是可以并且确实保持关于数据的视图状态。控制器只是视图模型和视图之间的交通警察,当然视图仅与视图状态有关。 - John Peters


您可以 看一个解释 Windows环境中的MVVM模式:

在Model-View-ViewModel设计模式中,应用程序由三个通用组件组成。 enter image description here

  • 模型:这表示您的应用消耗的数据模型。例如,在图片共享应用程序中,此图层可能表示设备上可用的图片集以及用于读取和写入图片库的API。

  • 视图:应用程序通常由多个UI页面组成。向用户显示的每个页面都是MVVM术语中的视图。视图是用于定义和设置用户看到的样式的XAML代码。模型中的数据将显示给用户,ViewModel的工作是根据应用程序的当前状态向UI提供此数据。例如,在图片共享应用程序中,视图将是向用户显示设备上的专辑列表,专辑中的图片以及可能向用户显示特定图片的另一个用户的UI。

  • 视图模型:ViewModel将数据模型或简单模型与应用程序的UI或视图联系起来。它包含用于管理模型中数据的逻辑,并将数据公开为XAML UI或视图可绑定的一组属性。例如,在图片共享应用程序中,ViewModel将公开一个专辑列表,并为每个专辑公开一个图片列表。 UI不知道图片来自何处以及如何检索图片。它只知道ViewModel公开的一组图片并将其显示给用户。


44
2018-06-12 20:48



请注意,虽然引用的文章适用于使用Microsoft Stack(特别是Windows Phone)和XAML的开发,但并非必须如此。 - Richard Nalezynski


我认为其中一个主要区别是在MVC中,你的V直接读取你的M,然后通过C来操作数据,而在MVVM中,你的VM充当M代理,并为你提供可用的功能V.

如果我没有充满垃圾,我很惊讶没有人创建混合,你的VM只是一个M代理,C提供所有功能。


35
2018-05-21 11:38



+1。我认为这个词是正确的。但是关于制造混合M-MProxy-V-C的分离是不是太多了?我认为使用M-V-C就足够了,而M是一个完全支持Binding的模型。 ;) - ktutnik
+1。正如我在上面评论的那样,我认为MVC用于构建整个(web)应用程序,而MVVM用于MVC的View组件。 - Tomasz Zielinski
@ktutnik:模型通常位于服务器上,而ViewModel存在于客户端上。因此,HTML直接绑定到服务器端Model是不可行的。因此,我们需要ModelView,它充当从模型中提取的本地的,未保存的工作数据集,例如使用例如AJAX / JSON。 - Tomasz Zielinski
视图确实“读取”了模型数据,因为它已经被控制器放在那里。我喜欢把它称为控制器的“数据注入”,因为它实际上是负责的控制器。所有视图都在我脑海中的渲染和火灾事件中进行。 - John Peters
我道歉但不同意MVVM的解释。 ViewModel不了解视图或视图的外观或响应方式,而模型同样不了解ViewModel。实际上,View甚至不应该知道模型,只是ViewModel。模型应该表示数据和应用程序状态,ViewModel应该将状态转换为支持UI的数据(我建议此时使用所有原语),View应该对ViewModels转换作出反应。数据通常是相同的,但它仍然应该通过ViewModel进行包装和重新传递,并且不存在任何控制器。 - Michael Puckett II


简单的区别:(受Yaakov的Coursera AngularJS课程的启发)

enter image description here

MVC (模型视图控制器)

  1. 楷模: 模型包含数据信息。不调用或使用Controller和View。包含业务逻辑和表示数据的方式。某些形式的某些数据可能会显示在视图中。它还可以包含从某些源检索数据的逻辑。
  2. 控制器: 充当视图和模型之间的连接。查看调用Controller和Controller调用模型。它基本上通知模型和/或视图以适当地改变。
  3. 视图: 处理UI部分。与用户互动。

MVVM (模型视图模型)

视图模型

  1. 它是视图状态的表示。
  2. 它包含在视图中显示的数据。
  3. 响应视图事件,即表示逻辑。
  4. 调用业务逻辑处理的其他功能。
  5. 永远不要直接要求视图显示任何内容。

17
2017-12-14 00:20





MVVM是一个改进(有争议)的 演示模型 模式。我说有争议,因为唯一的区别在于WPF如何提供进行数据绑定和命令处理的能力。


16
2018-03-27 21:22



在2009年,这个答案可能是一个很好的答案,但今天,没有争议,因为即使是来自MSFT的HTML Helper控件允许使用臭名昭着的“For”帮助者进行绑定。 Knockout就是客户端的数据绑定。 - John Peters
我在2009年说过这一点,因为社区中有太多人接受了这个答案。我说这是有争议的,因为MVVM和Presentation Model实际上是不同名称的相同模式。坦克在WPF中很受欢迎,它在今天的其他框架中通常被称为MVVM,但两个名称都是准确的。 - wekempf


viewmodel是用户界面元素的“抽象”模型。它必须允许您以非可视方式执行视图中的命令和操作(例如,对其进行测试)。

如果您使用过MVC,那么您可能有时会创建模型对象来反映视图的状态,例如,显示和隐藏某些编辑对话框等。在这种情况下,您使用的是viewmodel。

MVVM模式只是将该实践概括为所有UI元素。

并且它不是Microsoft模式,因为WPF / Silverlight数据绑定特别适合使用这种模式。但是,例如,没有什么能阻止你将它用于java服务器面。


13
2018-03-23 10:16





MVC是受控环境,MVVM是一个被动环境。 

在受控环境中,您应该拥有更少的代码和共同的逻辑源;它应始终存在于控制器内。然而;在Web世界中,MVC很容易分为视图创建逻辑和视图动态逻辑。创造存在于服务器上,而动态存在于客户端上。 ASP.NET MVC结合AngularJS可以看到很多,而服务器将创建一个View并传入Model并将其发送给客户端。然后,客户端将与View进行交互,在这种情况下,AngularJS将作为本地控制器进入。提交后,模型或新模型将传递回服务器控制器并进行处理。 (因此循环继续,当使用套接字或AJAX等时,这种处理还有很多其他的翻译,但是在所有架构上都是相同的。)

MVVM是一个反应环境,这意味着您通常会编写基于某些事件激活的代码(例如触发器)。在MVAM蓬勃发展的XAML中,使用内置的数据绑定框架可以很容易地实现这一点。如上所述,这可以在任何View中使用任何编程语言的任何系统上运行。它不是MS特定的。触发ViewModel(通常是属性更改事件),View会根据您创建的任何触发器对其做出反应。这可以获得技术,但底线是View是无状态的,没有逻辑。它只是根据值改变状态。此外,ViewModel是无状态的,逻辑非常少,而模型是具有基本零逻辑的状态,因为它们应该只保持状态。我将其描述为应用程序状态(Model),状态转换器(ViewModel),然后是可视状态/交互(View)。

在MVC桌面或客户端应用程序中,您应该有一个Model,并且Controller应该使用Model。基于Model,控制器将修改View。视图通常与具有接口的控制器相关联,因此Controller可以使用各种视图。在ASP.NET中,MVC的逻辑在服务器上略微向后,因为Controller管理模型并将模型传递给选定的视图。然后,View将根据模型填充数据并拥有自己的逻辑(通常是另一个MVC集,例如使用AngularJS完成)。人们会争论并将其与应用程序MVC混淆并试图做到这两点,此时维护项目最终将成为一场灾难。在使用MVC时,始终将逻辑和控制放在一个位置。不要在View后面的代码中(或通过JS for web)编写View逻辑来容纳Controller或Model数据。让Controller更改视图。应该存在于视图中的唯一逻辑是通过它使用的接口创建和运行所需的任何逻辑。一个例子是提交用户名和密码。无论是桌面还是网页(在客户端),只要View触发提交操作,Controller就应该处理提交过程。如果操作正确,您可以轻松找到适合您的MVC网站或本地应用程序的方法。

MVVM个人最喜欢,因为它完全被动。如果Model改变了状态,ViewModel会监听并转换该状态,就是这样!然后,View正在侦听ViewModel以进行状态更改,并且它还会根据ViewModel的转换进行更新。有些人称之为纯粹的MVVM,但实际上只有一个,我不在乎你如何争论它,而且它始终是Pure MVVM,其中View完全没有逻辑。

这是一个很小的例子:假设你想要按下按钮上的菜单。在MVC中,您将在界面中执行MenuPressed操作。控制器将在您单击“菜单”按钮时知道,然后根据另一种界面方法(如SlideMenuIn)告诉“视图”在菜单中滑动。往返是什么原因?如果控制器决定你不能或不想做其他事情,那就是为什么。除非控制器这样说,否则控制器应负责View中的View。然而;在MVVM中,动画中的幻灯片菜单应该是内置的和通用的,而不是被告知要滑动它将基于某些值这样做。所以它监听ViewModel,当ViewModel说,IsMenuActive = true(或者然而)动画发生。现在,据说我想提出另一点真的很清楚,请注意。 IsMenuActive可能是BAD MVVM或ViewModel设计。在设计ViewModel时,您永远不应假设View将具有任何功能,只需传递已翻译的模型状态即可。这样,如果您决定更改View以删除菜单并仅以其他方式显示数据/选项,则ViewModel无关紧要。那么你将如何管理菜单?当数据有意义的时候。因此,一种方法是给菜单一个选项列表(可能是一个内部ViewModel数组)。如果该列表包含数据,则Menu会知道通过触发器打开,如果没有,则它知道通过触发器隐藏。您只需在ViewModel中拥有菜单数据。不要决定在ViewModel中显示/隐藏该数据..只需翻译模型的状态。通过这种方式,View是完全被动的和通用的,可以在许多不同的情况下使用。

所有这些可能完全没有意义,如果你还没有至少略微熟悉每个的架构,并且学习它可能会非常混乱,因为你会在网上找到很多不好的信息。

所以...要记住要做到这一点的事情。首先确定如何设计您的应用程序和STICK TO IT。

如果你做MVC,这很棒,那么请确保你的Controller是可管理的并完全控制你的View。如果您有一个大视图,请考虑向具有不同控制器的View添加控件。只是不要将这些控制器级联到不同的控制器。维护非常令人沮丧。花点时间分别设计一些可以作为单独组件工作的东西......并且总是让Controller告诉Model提交或持久存储。 MVC的理想依赖设置是 查看←控制器→型号  或者使用ASP.NET(不要让我开始) 型号←查看控制器→型号(模型可以是相同的或完全不同的模型,从控制器到视图) ...当然,此时唯一需要知道Controller in View的内容主要是针对端点引用,以了解返回模型的位置。

如果你做MVVM,我会祝福你的善良灵魂,但要花时间去做吧!不要使用接口。让您的View根据值决定它的外观。使用模拟数据播放视图。如果您最终拥有一个显示菜单的视图(根据示例),即使您当时不想要它,也可以。你的观点正在发挥作用,并根据应有的价值做出反应。只需在触发器中添加一些要求,以确保在ViewModel处于特定转换状态时不会发生这种情况,或者命令ViewModel清空此状态。在您的ViewModel中,不要使用内部逻辑删除它,就像您从那里决定View是否应该看到它一样。请记住,您不能假设ViewModel中有菜单。最后,模型应该只允许您更改并最有可能存储状态。这是验证和所有将发生的地方;例如,如果Model无法修改状态,那么它只会将自己标记为脏或其他东西。当ViewModel实现这一点时,它将翻译脏的内容,然后View将实现此功能并通过另一个触发器显示一些信息。 View中的所有数据都可以绑定到ViewModel,因此只有Model和ViewModel完全不了解View对绑定的反应。事实上,模型也不知道ViewModel。在设置依赖项时,它们应该像这样指向并且只是这样 查看→ViewModel→模型  (这里有一个侧面说明......这也可能会引起争议,但我不在乎......不要把模型带到视图中。视图不应该看到一个模型时期。我给老鼠破解什么您已经看过的演示或者您是如何完成的,这是错误的。)

这是我的最后一个提示......看看一个设计良好但非常简单的MVC应用程序,并为MVVM应用程序做同样的事情。一个人将拥有更多的控制权,灵活性有限,而另一个则没有控制权和无限的灵活性。

受控环境适用于从一组控制器或(单一来源)管理整个应用程序,而被动环境可以分解为单独的存储库,完全不知道应用程序的其余部分在做什么。微管理与免费管理。

如果我没有让你感到困惑,请尝试与我联系......我不介意通过插图和示例全面详细介绍。

在一天结束的时候,我们都是程序员,并且在编码时我们内心仍然存在这种无政府状态......所以规则将被打破,理论将会发生变化,所有这一切都将最终结束......但是当我们工作时项目和大型团队,实际上有助于就设计模式达成一致并加以实施。有一天,它会在一开始就采取一些额外的小步骤,以后会节省成本。


11
2018-01-25 05:15