[从设计到架构]第四回:依赖的哲学(上)
[从设计到架构]第四回:依赖的哲学(上)
发布日期:2008.12.02 作者:Anytao
© 2008 Anytao.com ,Anytao原创作品,转贴请注明作者和出处。
本文将介绍以下内容:
- 关于依赖和耦合
- 面向抽象编程
- 依赖倒置原则
- 控制反转
- 依赖注入
- 工厂模式
- Unity框架应用
说在,开篇之前 |
在老子的“小国寡民”论中,提出了一种理想的社会状态:民至老死,不相往来。这是他老人家的一种社会理想,老死不相往来的人群呈现了一片和谐景象。因为不发生瓜葛,也就无所谓关联,进而无法倒置冲突。这是先祖哲学中的至纯哲理,但理想的大同总是和现实的生态有着或多或少的差距,人类社会无法避免联系的发生,所以小国寡民的理想成为一种美丽的梦想,不可实现。同样的道理,映射到软件“社会”中,也就是软件系统结构中,也预示着不同的层次、模块、类型之间也必然存在着或多或少的联系,这种联系不可避免但可管理。正如人类社会虽然无法实现小国寡民,但是理想的状态下我们推崇和谐社会,把人群的联系由复杂变为简单,由曲折变为统一,同样可以使得这种关联很和谐。所以,软件系统的使命也应该朝着和谐社会的目标前进,对于不同的关系处理,使用一套行之有效的哲学,把复杂问题简单化,把僵化问题柔性化,这种哲学或者说方法,在我看来就是:依赖的哲学,也就是本文所要阐释的中心思想。
*Hot:《你必须知道的.NET》 |
1 引言
因为在公司内部进行设计原则和设计模式的培训,我的第一个任务就是和大家就依赖倒置原则进行沟通。作为5大设计原则之一的DIP原则,单纯的由概念而实例在我认为并不能完全阐释清楚:
- 什么是依赖倒置?
- 为什么依赖倒置?
- 如何依赖倒置?
- 控制反转、依赖倒置、依赖注入这些概念,你认识但是否熟悉?
- Unity、ObjectBuilder、Castle这些容器,你相识但是否相知?
- 面向接口、面向抽象、开放封闭,这些思想,你了解但是否了然?
2 什么是依赖,什么是抽象
2.1 关于依赖和耦合:由小国寡民到和谐社会
在老子的“小国寡民”论中,提出了一种理想的社会状态:民至老死,不相往来。这是他老人家的一种社会理想,老死不相往来的人群呈现了一片和谐景象。因为不发生瓜葛,也就无所谓关联,进而无法倒置冲突。这是先祖哲学中的至纯哲理,但理想的大同总是和现实的生态有着或多或少的差距,人类社会无法避免联系的发生,所以小国寡民的理想成为一种美丽的梦想,不可实现。同样的道理,映射到软件“社会”中,也就是软件系统结构中,也预示着不同的层次、模块、类型之间也必然存在着或多或少的联系,这种联系不可避免但可管理。正如人类社会虽然无法实现小国寡民,但是理想的状态下我们推崇和谐社会,把人群的联系由复杂变为简单,由曲折变为统一,同样可以使得这种关联很和谐。所以,软件系统的使命也应该朝着和谐社会的目标前进,对于不同的关系处理,使用一套行之有效的哲学,把复杂问题简单化,把僵化问题柔性化,这种哲学或者说方法,在我看来就是:依赖的哲学,也就是本文所要阐释的中心思想。
因为,“耦合是不可避免的”,所以我们首先就从认识依赖和耦合的概念开始,来一步步阐释我们的依赖哲学思想:
- 什么是依赖和耦合
依赖,就是关系,代表了软件实体之间的联系。软件的实体可能是模块,可能是层次,也可能是具体的类型,不同的实体直接发生依赖,也就意味着发生了耦合。所以,依赖和耦合在我看来是对一个问题的两种表达,依赖阐释了耦合本质,而耦合量化了依赖程度。因此,我们对于关系的描述方式,就可以从两个方面的观点来分析:
从依赖的角度而言,可以分类为:
- 无依赖,代表没有发生任何联系,所以二者相互独立,互不影响,没有耦合关系。
- 单向依赖,关系双方的依赖是单向的,代表了影响的方向也是单向的,其中一个实体发生改变,会对另外的实体产生影响,反之则不然,耦合度不高。
- 双向依赖,关系双方的依赖是相互的,影响也是相互的,耦合度较高。
从耦合的角度而言,可以分类为(此处回归到具体的代码级耦合概念,以方便概念的阐释):
- 零耦合,表示两个类没有依赖。
- 具体耦合,如果一个类持有另一个具体类的引用,那么这两个类就发生了具体耦合关系。所以,具体耦合发生在具体类之间的依赖,因此具体类的变更将引起对其关联类的影响。
- 抽象耦合,发生在具体类和抽象类的依赖,其最大的作用就是通过对抽象的依赖,应用面向对象的多态机制,实现了灵活的扩展性和稳定性。
不同的耦合,代表了依赖程度的差别,我们以“粒度”为概念来分析其耦合的程度。引用中间层来分离耦合,可以使设计更加的优雅,架构更加的柔性,但直接的依赖也存在其市场,过度的设计也并非可取之道。因为效率与性能同样是设计需要考量的因素,过多的不必要分离会增加调用的次数,造成效率浪费。在下文分析依赖倒置原则的弊端之一正是对此问题的进一步阐述。
- 耦合是如何产生的?
那么,软件实体之间的耦合是如何产生呢?回归我们每天挥洒的代码片段,其实我们在重复的创造着耦合,并且得益于对这种耦合带来的数据通信。如果我们将历史的目光回归到软件设计之初,人类以简单的机器语言来实现最简单的逻辑,给一个输入,实现一个输出,可以表达为:
随着软件世界的革命,业务逻辑的复杂,以上的简单化处理已经不足以实现更复杂的软件产品,在系统内部的复杂度成为一个超越人脑可识别的程度时,例如:
因此,人类开始发挥重组和简单化处理的优势,我们不得不在软件设计上做出平衡。平衡的结果就是通过对复杂的系统模块化,把复杂问题简单处理,从而达到能够被人脑识别的目的。基于这种指导原则,随着复杂度的增加模块的划分更加朝着精细化发展,尤其是面向对象程序设计理论的出现,使得对复杂的处理实现了更科学的理论基础。然而,复杂的问题可以通过划分实现简单的功能模块或者技术单元,但由此应运而生的子单元会越来越多,而且越来越多的子单元必须发生数据的通信才能完成统一的业务处理,所以产生的数据通信管理也越来越多。对于子单元的管理,也就是我们本文关注的核心概念-依赖,成为新的软件设计问题,那么总结前人的经验,提炼今人的智慧,我们对耦合的产生做以如下归纳:
- 继承
- 聚合
- 接口
- 方法调用和引用
- 服务调用
- 设计的目标:高内聚(High cohesion)、低耦合(Low coupling)
- 面向抽象编程
- 低耦合,高内聚
- 封装变化
- 实现重用:代码重用、算法重用
对了,就是这些平凡的字眼,汇集了面向对象思想的核心内容,也是本文力求阐释的禅意心经。关于面向抽象编程和封装变化,我们会在后面详细阐释,在此我们需要将注意力关注于“低耦合,高内聚”这一目标。
低耦合,代表了实现最简单的依赖关系,尽可能的减少类与类、模块与模块、层次与层次、系统与系统之间的联系。低耦合,体现了人类追求简单操作的理想状态,按照软件开发的基本实现技巧来追求软件实体之间的关系简单化,正是大部分设计模式力图追求的目标;低耦合,降低了一个类或一个模块发生修改对其他类或模块造成的影响,将影响范围简单化。在我们阐释的依赖关系方式中,实现单向的依赖,实现抽象的耦合,都是实现低耦合的基础条件。
高内聚,一方面代表了职责的统一管理,一方面体现了关系的有效隔离。例如单一职责原则其实归根结底是对功能性的一种指导性体现,将功能紧密联系的职责封装为一个类(或模块),而判断的准则正是基于引起类变化的原因。所以,封装离不开依赖,而抽象离不开变化,二者的概念和本质都是相对而言的。因此,高内聚的目标体现了以隔离为目标进行统一管理的思想。
那么,为了达到低耦合、高内聚的目标,通常意义上的设计原则和设计模式其实都是朝着这个方向实现的,因此我们仅仅小结并非普遍意义的规则:
- 尽可能实现单项依赖
- 不需要进行数据交换的双方,不要实现多此一举的关联,人们将此形象称为,不要向陌生人说话(Don't talk to strangers)
- 保持内部的封装性,关联的双方不要深入实现细节进行通信,这是保证高内聚的必须条件。
2.2 关于抽象和具体
什么是抽象呢?我们首先不必澄清什么是抽象,而从什么算抽象说起,稳定的、高层的则代表了抽象。就像一个公司,最好保证了高层的稳定,才能保证全局的发展。在进行系统设计时,稳定的抽象接口和高层逻辑,也代表了整个系统的稳定与柔性。兵熊熊一窝,将良良一窝,系统的逻辑也正如着代表打仗,良好的设计都是自上而下的。而对具体的编程实践而言,接口和抽象类则代表了语言层次的抽象。
追溯概念的分析,我们一一过招,首先来看依赖于具体:
因此,为了分离这种紧耦合,最好的办法就是隔离,引入中间层来分离变化,同时确保中间层本身的稳定性,因此抽象的中间层是最佳的选择。
例如:
public interface IUserService
{
}
public class UserService : IUserService
{
}
下面依赖于具体:
public class UserManager
{
private UserService service = null;
}
下面依赖于抽象:
public class UserManager
{
private IUserService service = null;
}
二者的区别仅在于引入了接口IUserService,从而使得UserManager对于UserService的依赖由强减弱。这种方式也在我们的Ezsocio项目中进行service层的设计方式。然而对于依赖的方式并非仅此一种,设计模式中的智慧正是通过各章编程技巧进行依赖关系的设计,值得我们关注和学习,本文也在下文进行相关设计模式的讨论。
对WCF熟悉的读者一定不难看出这种实现方式如此类似于WCF的推荐模式,这是契约编程的基本思想。关于WCF及SOA的相关内容,本文将在后文进行相关的讨论。
总结一番,什么是抽象,什么是具体?在我看来,抽象就是系统中对变化封装的战略逻辑,体现了系统的必然性和稳定性,能够被具体层次复用和覆写;而具体则包含了与具体实现相关的逻辑,体现了系统的动态性和变动性。因此,抽象是稳定的,而具体是变动的。
Bob大叔在《敏捷》一书直言,程序中所有的依赖关系都应终止于抽象类或者接口,就是对面向抽象编程一针见血的回应,其原因归根结底源自于我们对抽象和具体的认知和分解:关联应该终止于抽象,而不是具体,保证了系统依赖关系的稳定。具体类发生的修改,不会影响其他模块或者关系。那么如何做到这种理想的依赖于抽象的设计呢?
- 层次清晰化
所以,清晰的层次划分,进而形成的模块化,是实现系统抽象的必经之路。
- 分散集中化
- 具体抽象化
- 封装变化点
这一设计原则中我们还将之称为SoC(Separation of Concerns)原则,定义了对于实现理想的高耦合、低内聚目标的统一规则。
2.3 设计的哲学
之所以花如此篇幅来讲述一个看似简单的问题,其实最终理想是回归到软件设计目标这个命题上。如果悉心钻研就可发现,设计的最后就是对关系的处理,正如同生活的意义在于对社会的适应一样。因此,回归到设计的目标上我们就可知,完美的设计过程就是对关系的处理过程,也就是对依赖的梳理过程,并最终形成一种合理的耦合结果。
所以,面向对象并不神秘,我们以生活的现实眼光来看更是如此。把面向对象深度浓缩起来,我觉得可以概括为:
- 目标:重用、扩展
- 核心:低耦合、高内聚
- 手段:封装变化
- 思想:面向接口编程、面向抽象编程
其实,就是这么简单。在这种意义上来说,面向对象思想是现代软件架构设计的基础。下面我们以三层架构的设计为例,来进一步感受这种依赖哲学的具体应用。关于依赖的抽象和对变化隔离的基本思路,其实也是实现我们典型三层架构(或者)多层架构的重要基础。只要使各个层次之间依赖于较稳定的接口,才能使得各个层次之间的变化被隔离在本层之内,不会造成对其他层次的影响,这完全符合开放封闭原则追求的优良设计理念。将这种思路表达为设计,可以表示为:
在此,IDataProvider作为隔离业务层和数据层的抽象,IService作为隔离业务层和表现层的抽象,保证了各个层次的相对稳定和封装。而体现在此的设计逻辑,就正是我们对于抽象和耦合基本目标概念的体现,例如作为重用的单元,抽象隔离保证了对外发布接口的单一和稳定,所以达到了最高限度的重用;通过引入中间的稳定的接口,达到了不同层次的有效隔离,层与层之间体现为轻度耦合,业务层只持有IDataProvider就可以获取数据层的所有服务,而表现层也同样如此;最后,这种方式显然也直接实践了面向接口编程,面向抽象编程的经典理念。
同样的道理,对于架构设计的很多概念,放大可以扩展为面向服务设计所借鉴,放小这正是我们反复降调的依赖倒置原则在类设计中的基本思想。因此,牢记对我影响至深的一位大牛的说法:软件设计的任何问题,都可以通过引入中间逻辑了解决。而这个中间逻辑,很多时候被封装为抽象,是最为合理和智慧的解决方案。
让我们再次高颂《老子》的小国寡民论,来回味关于依赖哲学中,我们如何实现更好的和谐统一,如何遵守科学的软件管理思想:"邻国相望,鸡犬之声相闻,民至老死,不相往来。"
3 认识依赖倒置原则(DIP)
3.1 什么是依赖倒置?
Bob大叔在《Agile Principles, Patterns, and Practices》一书中对依赖倒置原则进行了精辟的总结为:
- 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
- 抽象不应该依赖于具体,细节应该依赖于抽象。
其实著名的好莱坞原则更形象的阐述了这一思想:你不要调我,我来调你。不管是通俗的还是高尚的,却都不约而同的揭示了依赖倒置原则的最核心思想就是:
依赖于抽象,对接口编程,对抽象编程!
相较而言,从实际的生活中来看依赖倒置,就像下面这个示例揭示的一样。
3.2 从实例开始
综合对依赖倒置的认识,结合到具体的程序实现而言,依赖倒置预示着程序中的依赖关系不应是具体的类型,而是归咎于抽象类和接口。下面我们通过一个简单的实例来分析符合依赖倒置和违反依赖倒置,对于系统设计的影响和区别。我们的需求是为某个遥控器生产商,实现一个万能遥控器,该遥控器可以对当前市场上的很多电子设备进行“打开”和“关闭”的操作,例如你可以使用Anytao牌遥控器打开海尔电视、创维电视等等,当然更理想的状态是可以打开电冰箱、电灯还有门窗等等,总之凡是可以互联的设备都是未来万能遥控器的新需求。
那么该遥控器厂商在设计之初,该如何去考虑实现一个可以打开任何设备的遥控器呢?这一重责首先落在了一位年轻气盛的小王设计师身上,因为遥控器厂家当前的直接客户只有海尔电视一家,所以他轻松的实现了下面的设计,并且兴高采烈的进行了大批量生产:
随后,厂商多了一个重量级客户长虹,所以小王不得不对初试设计进行了改造,勉强适应了新的需求,如下:
虽然小王应付了这次需求变动,但是原本的设计显然已经捉襟见肘。正当小王绞尽脑汁进行改造的同时,新的需求接踵而来:新飞冰箱、飞利浦照明、盼盼防盗门,一个接一个。小王的最终设计变成了这般摸样:
哎,真是太累了。每一次的需求变更都伴随着小王对遥控器Remote的再次摧残,Remote内部不断增加新的引用和操作处理,显然一个if/else式的判断布满了整个Open和Close的操作中,这种设计显然无法满足OCP对扩展开放、对修改封闭的要求。显然,如果想让卖出去的遥控器也适应新的需求,在小王当前的设计实现方案中是根本无法实现的,遥控器厂商总不能召回已经售出所有的遥控器,再拆开进行重新改造吧。
一筹莫展的小王,终于在崩溃之际想起了经验丰富的前设计师老王,并立即请教如何解决当前问题的思路。而老王也毫不含糊,给出了一个初步的实现:
在当前的设计中,老王的思路是让遥控器厂切断和各个厂家的直接联系,而是寻找所有电视厂商的领导(例如,电视机协会),请电视机协会制定所有电视机厂商必须遵守的打开和关闭等操作的契约,遥控器厂和电视机协会建立直接的联系而不是各个具体的电视厂商,于是便有了上述设计思路。而新的需求来临时,因为各个厂商必须遵守TurnOn和TurnOff的契约,所以轻松的万能遥控器可以应付所有的电视机品牌,实现的具体操作已经由遥控器转移到具体的厂商手上(顺便说说这也是所有权的倒置体现),轻松的小王终于大呼一口气。并且再接再厉修改了更完善的版本:
现在,遥控器基本实现了万能的要求,任何新的需求或者修改都可以轻松胜任。小王终于解决了原本设计的所有问题,带着感激盛情邀请老王吃饭致谢。席间就坐,小王请教老王二次设计的秘诀,老王神秘一笑沾酒在桌子上写了几个大字:依赖倒置。经历此次设计重构洗礼的小王,也在实战中体味了设计的精妙,看着依赖倒置几个字小王也会心的笑了。
万能遥控器的故事,是一个系统实现中经常的事儿。而这些设计在Ezsocio项目中有广泛的应用,例如对于DataProvider和Service的处理方式,正是一种典型的遵循DIP原则的设计思路。
3.3 为什么依赖倒置?
依赖倒置原则揭示了面向对象思想中一个最基本而最核心的话题,那就是:面向抽象编程。任何对依赖倒置原则的违反都不同程度的偏离了面向对象设计思想的轨道,所以如果你想自己的程序是否足够的OO,透彻的了解依赖倒置是必不可少的。
所以,要问答为什么依赖倒置这个话题,我觉得可以从以下几个方面来阐释:
- 依赖倒置是保证开放封闭的前提和基础。
- 依赖倒置是对抽象和依赖的基本原则和基本思想的哲学阐释。
- 依赖倒置是框架设计的核心思想。
- 依赖倒置是控制反转和依赖注入的思想基础。
综上而言,依赖倒置是对软件实体关系处理的基本思想原则,也是其他设计原则与设计模式的基础之一,因此遵守依赖倒置是实现OO的基本原则,是我们必须了解的基础性原则。下面,我们对此进行详细的说明和举例。
3.4 为什么是倒置?
鲁迅先生有云:其实地上本没有路,走的人多了也便成了路。对依赖倒置原则中的“倒置”二字而言,其实也类似于一条被很多人走过的路,因为习惯性的称呼走过的为“路”,所以只好把违反习惯的东西称为“倒置的路”。这倒置的含义,正基于此。
对于从结构化编程走过的人来说,基于软件复用的考虑,侧重于对具体模块的复用,因为也就习惯了从高层模块出发了构建系统流程的思维模式,所以那时的高手一出手就实现了高层依赖于底层的典型套路,例如:
高层模块通过自上而下的实现来完成系统功能的调用,将这种方式表达为代码就是:
// Release : code01, 2008/11/02
// Author : Anytao, http://www.anytao.com
public static void Main()
{
try
{
//Do something here.
}
catch
{
Log(true, "XMLLog");
}
}
public static void Log(bool isRead, string logType)
{
if (isRead)
ReadLog(logType);
else
WriteLog(logType);
}
然而,当软件设计的模式发展到面向对象阶段时,我们发现原来习惯的世界了已经变了。基于高层依赖于底层的弊政,也越来越被可扩展性的系统需求折磨的面目全非,例如如果日志记录的载体发生变化,当前设计中需要同时自上而下的修改实现的逻辑,同时避免出现越来越多的if/else结构。所以当新的依赖关系从传统的方式被完全扭转时,“倒置”二字就此诞生了。我们修改Log实现的设计思路,将可能变化的逻辑封装为抽象接口,使得高层依赖发生转换:
程序实现的逻辑早已被面向对象的设计思想所取代,我们新的实现变成了:
// Release : code02, 2008/11/02
// Author : Anytao, http://www.anytao.com
public class Client
{
public static void Main()
{
ILog myLogger = new XMLLog();
try
{
}
catch
{
myLogger.Write();
}
}
}
public interface ILog
{
void Read();
void Write();
}
public class XMLLog : ILog
{
public void Read()
{
}
public void Write()
{
}
}
所以,了解了历史才能正视现实,对于软件设计同样如此,只有认清楚依赖倒置产生的历史背景,我们才能更加熟练的驾驭倒置含义本身带来的误解,而将中心思想牢牢的把握在依赖倒置最核心的设计思想上,那还是万变不离其宗的:依赖于抽象,这简单的5个字上。
对于所属权关系的依赖问题上,我们看到,只有倒置的才是面向对象的,没有倒置的还是面向结构的。如果你的系统中存在着不合理的依赖关系,那么依赖倒置将是检查系统设计最好的标尺,这也是我们把握这一原则的实际意义之一。
3.5 如何依赖倒置?
如何依赖倒置的关键,还是体现在如何对抽象和具体的封装和分离,实践的基本思路就是封装变化。这正如我们在单一职责原则中反复强调,对一个类只有一个引起它变化的原因。我们实践依赖倒置,仍然可以从关注变化开始,详细的分析和预测系统中的变化点,然后针对每个可能的变化抽象出相对稳定的约束,这是我们实践依赖倒置原则最基本的方法步骤。
就原理而言,依赖倒置要求我们的设计:
- 少继承,多聚合
- 单向依赖(低耦合,高内聚)
- 封装抽象
- 对依赖关系都应该终止于抽象类和接口
就实践而言,经典的软件设计实践为我们提出了很多值得借鉴的思路,例如每个设计模式就是对一种特定情况的实践总结,在此我们继续列出一些经典的大师忠言,Bob大叔在《Agile Principles, Patterns, and Practices》一书对此进行了3点总结:
- 任何变量都不应该持有一个指向具体类的指针或者引用。
- 任何类都不应该从具体类派生。
- 任何方法都不应该覆写它的任何基类中的已经实现的方法。
- 系统架构应该有清晰的层次定义,层次之间通过接口向外提供内聚服务,正如在三层示例中的举例一样。
- 典型的以new进行的对象创建操作,是对依赖倒置原则的典型违反,我们将在后文进行详细讨论。
如何依赖倒置,我们阐释了一点原则还有一点方法,算是对实现依赖倒置的一点小结。然而,在实际的开发过程中,并没有一成不变的规则,当前的面向对象语言本身就提供了对抽象和封装的支持,为实现面向对象设计提供了基础机制。回顾软件开发的历史,我们不难看出依赖和封装哲学的发展轨迹,在结构化编程中函数是封装的基本单元;随着面向对象的发展C++/C#高级语言以类为基本单元,第一次将数据和行为有机的组合为一个逻辑单元,于是有了对于不同类之间的关系处理哲学;而SOA中封装的单元上升为service,是一种更高意义的逻辑封装,实现了更优良的逻辑封装和松散耦合关系。同样的道理,也体现在三层架构的分割和通信中,体现在ORM对表现层和领域层的分离中。
因此,依赖倒置是一种高度的智慧和经验总结,如何实现依赖倒置也是一种积累和不断的学习。
3.6 也有弊端
然而,一味的遵守原则,就等于没有原则。重要的是,我们需要把握其平衡,在进行开发中适当的把握其程度。Bob在《敏捷》中也提到这个问题,他总结了依赖倒置的两个弊端,同样需要我们必要的关注:
- 对抽象编程,需要增加必要的类和辅助代码进行支持,某种程度上增加了系统复杂度和维护成本;
- 当具体类不存在变化时,遵守依赖倒置是多此一举。所以,如果具体或细节没有变化可能时,我们没有必要通过抽象转嫁依赖是没有必要的处理。
所以,学习模式或者原则必须把握灵活处理,不能一味强行。
下集预告 |
在下篇中,我们将继续对依赖相关的问题进行讨论,基本的内容还包括:
近期发布,敬请期待。
*Hot:《你必须知道的.NET》 |
参考文献
- Martin Fowler,Inversion of Control Containers and the Dependency Injection pattern
- 装配脑袋,泛型技巧系列:用泛型打造可复用的抽象工厂
系列导航
[目录导航]
[第一回:设计,应该多一点]
[第二回:对象的旅行---对象和人,两个世界,一样情怀]
[第三回:设计的分寸]
2008/12/02 | http://anytao.cnblogs.com/
本文以“现状”提供且没有任何担保,同时也没有授予任何权利。 | This posting is provided "AS IS" with no warranties, and confers no rights.
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
Worktile,新一代简单好用、体验极致的团队协同、项目管理工具,让你和你的团队随时随地一起工作。完全免费,现在就去了解一下吧。
https://worktile.com