软件设计原则与模式

1.单一职责-SRP

单一职责原则(Single Responsibility principle),这个名字非常容易让我们望文生义,我们可能会理解成,一个类只干一件事,这看起来似乎是一项再合理不过的要求了。因为,几乎所有的程序员都知道“高内聚、低耦合”,都知道该把相关的代码放到一起。

原因就在于,我们一开始的理解就是错的,我们把单一职责理解成了有关如何组合的原则,但实际上,单一职责是关于如何分解的。

那到底什么是单一职责原则呢/p>

正如 Robert Martin 所说,单一职责的定义经历了一些变化。在《敏捷软件开发:原则、实践与模式》中其定义是,“一个模块应该有且仅有一个变化的原因”;而到了《架构整洁之道》中,其定义就变成了“一个模块应该对一类且仅对一类行为者(actor)负责”。

单一职责原则和一个类只干一件事之间,最大的差别就是,将变化纳入了考量。

我们先分析第一个定义:一个模块应该有且仅有一个变化的原因。变化是我们最不愿意面对却不得不面对的事,因为变化会引发新的不确定性,可能是新增功能自身的稳定问题,也可能是旧有功能遭到破坏带来的问题。

所以,一个模块最理想的状态是不改变,其次是少改变,它可以成为一个模块设计好坏的衡量标准。

在真实项目中,一个模块之所以会频繁变化,关键点就在于能引起它改变的原因太多了。

怎么理解呢们来看一个例子。假设我们要开发一个项目管理的工具,自然少不了一个用户的类,我们可能设计出这样一个用户类:

看上去,这个类设计得还挺合理,有用户信息管理、有项目管理等等。没过多久,新的需求来了,要求每个用户能够设置电话 码,所以,你给它增加了一个新的方法:

过了几天,又来了新需求,要查看一个用户加入了多少项目:

就这样,左一个需求,右一个需求,几乎每个需求都要改到这个类。那会导致什么结果呢方面,这个类会不断膨胀;另一方面,内部的实现会越来越复杂。按照我们提出的衡量标准,这个类变动的频繁程度显然是不理想的,主要原因就在于它引起变动的需求太多了:

为什么要增加电话 码呢为这是用户管理的需求。用户管理的需求还会有很多,比如,用户实名认证、用户组织归属等等;

为什么要查看用户加入多少项目呢是项目管理的需求。项目管理的需求还会有很多,比如,团队管理、项目权限等等。

这就是两种完全不同的需求,但它们都改到了同一个类,所以,这个 User 类就很难稳定下来。解决这种问题,最好的办法就是把不同的需求引起的变动拆分开来。针对这里的用户管理和项目管理两种不同需求,我们完全可以把这个 User 类拆成两个类。比如,像下面这样,把用户管理类的需求放到 User 类里,把项目

管理类的需求放到 Member 类里:

如此一来,用户管理的需求只要调整 User 类就好,而项目管理的需求只要调整 Member 类即可,二者各自变动的理由就少了一些。

确实是这样的,想要更好地理解单一职责原则,重要的就是要把不同的关注点分离出来。在上面这个例子中,分离的是不同的业务关注点。所以,理解单一职责原则本质上就是要理解分离关注点。

按照之前的说法,分离关注点,应该是发现的关注点越多越好,粒度越小越好。如果你能看到的关注点越多,就可以构建出更多的类,但每个类的规模相应地就会越小,与之相关的需求变动也会越少,它能够稳定下来的几率就会越大。我们代码库里稳定的类越多越好,这应该是我们努力的一个方向。

不过,也许你会想,如果将这种思路推演到极致,一个类应该只有一个方法,这样,它受到的影响应该是最小的。的确如此,但我们在真实项目中,一个类通常都不只有一个方法,如果我们要求所有人都做到极致,显然也是不现实的。

那应该把哪些内容组织到一起呢就需要我们考虑单一职责原则定义的升级版,也就是第二个定义:一个模块应该对一类且仅对一类行为者负责。

需求为什么会改变为有各种提出需求的人,不同的人提出的需求,其关注点是不同的。在前面的那个关于用户的讨论中,关心用户管理和关心项目管理的可能就是两拨完全不同的人,至少他们在提需求的时候扮演的是两种不同的角色。

两种不同角色的人,两件不同的事,到了代码里却混在了一起,这是不合理的。所以,分开才是一个好选择。用户管理的人,我和他们聊 User,项目管理的人,我们来讨论 Member。

康威定律:一个组织设计出的系统,其结构受限于其组织的沟通结构

Robert Martin 说,单一职责原则是基于康威定律的一个推论:一个软件系统的最佳结构高度依赖于使用这个软件的组织的内部结构。如果我们的软件结构不能够与组织结构对应,就会带来一系列麻烦,前面的那个例子只是一个小例子。

实际上,当我们更新了对于单一职责原则的理解,你会发现,它的应用范围不仅仅可以放在类这样的级别,也可以放到更大的级别。

我给你举个例子。我曾经接触过一个交易平台,其中有一个关键模型:手续费率,就是交易一次按什么比例收取佣金。平台可以利用手续费率做不同的活动,比如,给一些人比较低的手续费率,鼓励他们来交易,不同的手续费率意味着对不同交易行为的鼓励。

所以,对运营人员来说,手续费率是一个可以玩出花的东西。然而,对交易系统而言,稳定高效是重点。显然,经常修改的手续费率和稳定的系统之间存在矛盾。

经过分析,我们发现,这是两类不同的行为者。所以,在设计的时候,我们把手续费率设置放到运营子系统,而交易子系统只负责读取手续费率。当运营子系统修改了手续费率,会把最新的结果更新到交易子系统中。至于各种手续费率设置的花样,交易子系统根本不需要关心。

你看,单一职责原则也可以指导我们在不同的子系统之间进行职责分配。所以,单一职责原则这个看起来最简单的原则,实际上也蕴含着很多值得挖掘的内容。要想理解好单一职责原则:

我们需要理解封装,知道要把什么样的内容放到一起;

我们需要理解分离关注点,知道要把不同的内容拆分开来;

软件设计原则与模式

实际上,在真正的软件开发中,我们也没必要过于未雨绸缪,过度设计。所以,我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类

越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构

下面这几条判断原则,比起很主观地去思考类是否职责单一,要更有指导意义、更具有可执行性:

  • 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;

  • 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;

  • 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;

  • 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;

  • 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应 的方法拆分出来。

2. 类的职责是否设计得越单一越好

为了满足单一职责原则,是不是把类拆得越细就越好呢案是否定的。我们还是通过一个例子来解释一下。Serialization 类实现了一个简单协议的序列化和反序

列功能,具体代码如下:

如果我们想让类的职责更加单一,我们对 Serialization 类进一步拆分,拆分成一个只负责序列化工作的 Serializer 类和另一个只负责反序列化工作的 Deserializer

类。拆分后的具体代码如下所示:

虽然经过拆分之后,Serializer 类和 Deserializer 类的职责更加单一了,但也随之带来了新的问题。如果我们修改了协议的格式,数据标识从“UEUEUE”改为“DFDFDF”,或者序列化方式从 JSON 改为了 XML,那 Serializer 类和 Deserializer 类都需要做相应的修改,代码的内聚性显然没有原来 Serialization 高了。而且,如果我们仅仅对 Serializer 类做了协议修改,而忘记了修改 Deserializer 类的代码,那就会导致序列化、反序列化不匹配,程序运行出错,也就是说,拆分之后,代码的可维护性变差了。

实际上,不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、复用性、可维护性等。我们在考虑应用某一个设计原则是否合理的时候,也可以以此作为最终的考量标准。

总结

1.如何理解单一职责原则(SRP)/strong>

一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

2.如何判断类的职责是否足够单一/strong>

不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义

和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:

类中的代码行数、函数或者属性过多;

类依赖的其他类过多,或者依赖类的其他类过多;

私有方法过多;

比较难给类起一个合适的名字;

类中大量的方法都是集中操作类中的某几个属性。

3.类的职责是否设计得越单一越好/strong>

单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

2. 开放封闭原则-OCP

1.不修改代码

开放封闭原则是这样表述的:

软件实体(类、模块、函数)应该对扩展开放,对修改封闭。

这个说法是 Bertrand Meyer 在其著作《面向对象软件构造》(Object-Oriented Software Construction)中提出来的,它给软件设计提出了一个极高的要求:不

修改代码。

或许你想问,不修改代码,那我怎么实现新的需求呢案就是靠扩展。用更通俗的话来解释,就是新需求应该用新代码实现。

开放封闭原则向我们描述的是一个结果,就是我们可以不修改代码而仅凭扩展就完成新功能。但是,这个结果的前提是要在软件内部留好扩展点,而这正是需要我

们去设计的地方。因为每一个扩展点都是一个需要设计的模型。

举个例子,假如我们正在开发一个酒店预订系统,针对不同的用户,我们需要计算出不同的房价。比如,普通用户是全价,金卡是 8 折,银卡是 9 折,代码写出来可能是这样的:

这时,新的需求来了,要增加白金卡会员,给出 75 折的优惠,如法炮制的写法应该是这样的:

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年4月6日
下一篇 2022年4月6日

相关推荐