企业应用架构笔记-软件设计的原则

软件设计的原则

     所有编程都是维护编程,因为你很少写原创代码。只有你在最初10分钟里键入的代码是原创的。仅此而已。-----Dave Tomas 和 Andy Hunt

软件开发的一个流行格言是:好的架构就是所有艰难决定最终都被证明是正确的架构。
在这里插入图片描述

软件设计的通用原则

可维护代码的基础包含两个核心原则—— 内聚与耦合。

内聚

内聚表示软件模块承担的众多职责关联性很强,不管是子程序,类,还是库。内聚
用来衡量通过类的各个方法、库的各个函数以及方法完成的各个动作表达的逻辑之间的距离。
内聚的度量范围从低到高,越高越好。

高内聚类有助于维护和重用,因为它们倾向于没有依赖。另一方面,低内聚使类的目的难以理解,并使软件变得僵硬和脆弱。降低内聚导致创建的类在职责(即方法)上共同点很少,并且引用不同的不相关的活动。
总结成实用准则就是:内聚原则建议创建非常专注的类,有少量方法表示逻辑上相关的操作。如果方法之间的逻辑距离增加,你只需创建一个新类。

极限编程先锋Ward Cunningham建议我们把内聚定义为与类拥有的职责数量成反比的东西。

耦合

耦合衡量两个软件模块(如类)之间存在的依赖程度。
耦合度量范围从低到高,越低越好。
低耦合并不意味着你的模块与另一个完全隔离。它们肯定允许通讯,但它们应该通过一组定义明确和稳定的接口来做。每个模块都应该可以在没有了解另一个模块的内部实现的情况下工作。相反,高耦合妨碍测试和重用代码,使理解代码变得繁琐。它也是导致设计僵硬和脆弱的主要原因之一。

低耦合与高内聚有着强烈的相关性。系统的设计符合低耦合与高内聚通常都具备高可读性,可维护性,易于测试,以及良好重用。

关注点分离

一个有助于实现高内聚和低耦合的原则是关注点分离(Separation of Concerns, SoC)
关注点是软件功能的不同部分,像业务逻辑或者表现方式。SoC都是关于把系统分解成不同的 可能没有重叠的特性。在系统里每个你想要的特性都表示系统的一个关注点和一个方面。
SoC建议你每次只把注意力放在一个具体的关注点上。简单地说,在你把一个关注点关联到一个软件模块之后,你的注意力就放在构建那个模块上。就那个模块而言,任何其他关注点都是不相干的。

隔离

SoC具体可以通过使用模块化代码以及大量使用信息隐藏来实现。模块化编程鼓励为每个重要 特性使用单独的模块。模块有它们自己的公共接口,可以与其他模块通讯,也可以包含内部信息,自己使用。

只有公共接口的成员才对其他模块可见。用接口属性公共、私有、保护方式实现隔离

面向对象设计

OOD的主旨包含在这句话里:你必须找到相关对象,把它们划分成合适粒度的类,定义类的接口和继承体系,然后在它们之间建立关键关系.
总结下就是:找出相关对象,减少接口对象之间的耦合,以及善用代码重用。

相关类

找出相关对象的常见做法是标记出各个用例里的名词和动词。,名词引出类或属性,而动词引出类上的方法。

对接口编程

这里有个需要关注名词:横切关注点(cross-cuttingconcern)横切关注点是你需要放在类里 却又与类的需求没有太大关系的功能。

完全分离横切关注点需要使用依赖注入(D I)或服务定位器(S L)等模式,在这种情况下,类依赖的是某种抽象而不是实际编译的模块。一般而言,分离横切关注点意味着类是对接口而不是实现编程的。

对类实施提取接口重构,目的是提取出一个可以描述组件核心功能的接口,实现解耦。

组合与继承

继承:派生类不是简单地继承父类的代码。它实际上继承了上下文,因而获得父对象状态的某种可见性。
一方面,使用从父类继承而来的上下文的派生类可能会因为父类的后续更改而坏掉。此外,当你从一个类继承,你就进入了一个多态上下文,意味着你的派生类可以在任何接受父类的场景下使用。但是,并不能保证这两个类真的可以交替使用。

对象组合:需要一个新的类型持有基类型的实例,一般通过私有成员引用它:使用这个 对象而不是改变这个对象来实现它的目标。外部调用到达包装类,包装类把这个调用委托给内部持有的类的实例。


```csharp
public RegisteredUser {
	private User theUser; 
	public RegisteredUser() {
		/ / 你可以使用你想要的任何延迟加载策略来实例化
		/ / 这里不使用延迟加载
		theUser = new User();
	}
	public object DoWork() {
		var data = theUser.DoSomeWork();
		/ / 执行一些操作
		return Process(data);
	}
	p riva te object Process(object data) {
}

组合起来的两个类没有明显的关系;你不能在需要User实例的地方使用RegisteredUser类。如 果最后证明这是一个问题,你可以让两个类实现某个通用的lUser接口。

面向对象反思

要真正写出有效的代码,我们认为开发者应该关注以下3 点:

• 隔离可能改变的部分。
• 有时候类是不需要的;如果你所要的就是一个函数,那就只用函数。
• 明白在现实世界里出现的是事件而不是模型,而事件只包含数据。

综上:

耦合、内聚和SoC等通用原则,加上OOD(面向对象设计原则)原则,就如何设计软件应用程序给了我们一些相 对通用的指导。

SOLID 原则

Robert C. Martin就干净和更有效的软件设计给出了 5 个基础原则。
• 单一责任原则(SRP)
• 开放/封闭原则(OCP)
• 里氏代换原则(LSP)
• 接口分离原则(ISP)
• 依赖反转原则(DIP)

单一责任原则

“一个类有且只有一个改变理由”

原则的整个要点是每个类在理想情况下应该围绕一个核心任务构建。

这里的重点本质上不是简化,而是通过暴露数量非常有限的责任使这个类与系统的交集更小,这样一来,当需求改变时,需要编辑这个类的可能性就会更小了。

SRP经常被盲目地用于衡量代码的质量。在极端情况下,你会面临产生很多贫血类(只有属性和少量甚至没有行为)的严重风险。

所以:在写代码时,记住类应该尽可能简单,专注于一个主要的核心任务。但那不应该变成宗教教义或者衡量性能和质量的方式。

开放/封闭原则

“模块应该对扩展开放,但对修改封闭”

对扩展开放基本上意味着现有的类应该是可扩展的,可以用作构建其他相关功能的基础。但在实现其他相关功能时,你不应该修改现有代码,因而对修改封闭。

OCP鼓励使用组合、接口和泛型等编程机制生成在不修改源代码的情况下可以扩展的类。如果你使用组合,你可以在不触及现有组件的情况下在它们之上构建新的功能。

里氏代换原则

“子类应该可以替换它们的基类”

里氏代换原则的本质是派生类不能限制基类执行的条件。类似地,派生类不能避免产生一些父类保证要有的结果。

简而言之,派生类需要的不能比父类多,提供的不能比父类少。

接口分离原则

“不应该强制客户依赖于它们不用的接口”

正确实施这个原则意味着把接口分解成多组函数,以便每个客户更容易匹配它真正感兴趣的那组函数。

未能充分遵守接口分离原则会导致实现很复杂以及很多方法根本没有实现。此外,客户被迫依赖于它们不用的接口,而且这些客户还受制于这种接口的改变。

依赖反转原则

“高级模块不应该依赖于低层模块。二者都应该依赖于抽象”

依赖反转是表达“对接口而不是实现编程”背后的概念的正式方式。

当你写一个方法并且需要调用外部组件时,作为一名开发者,你应该思考你要调用的这个函数是这个类私有的还是一个外部的依赖。

如果是一个外部的依赖,你就把它抽象成一个接口,然后对这个接口继续编码。剩下的问题就是如何把这个接口变成某些具体的可调用的实例。这就是依赖注入等模式的事情了。

处理依赖的模式

举例:

void Copy() {
Byte byte;
 while(byte = ReadFromStream()) 
 WriteToBuffer(byte);
}

这里的伪代码依赖于两个低层模块:读取器和写入器。根据DIP,你应该把依赖抽象成接口

void Copy() {
	Byte byte; 
	IReader reader; 
	IWriter writer;
	//仍需实例化读取器和写入器的变量
	while(byte = reader.Read()) 
		writer・Write(byte);
}

提供读取器和写入器组件的实例,你需要某种更高级别的规范。换句话说,你需要模式。

服务定位器模式

void Copy()
{
	Byte byte; 
	van reader = ServiceLocator.GetService<IReader>(); 
	van writer = ServiceLocator.GetService<IWriter>(); 
	while(byte = reader.Read()) 
		writer.Write(byte);
}

使用一个中央组件在特定抽象请求时定位并返回需要使用的实例。服务定位器可以嵌在需要它的代码里使用。它充当一个工厂,包含的发现和激活逻辑可以达到你想要的复杂程度

public class ServiceLocator
{
	public Object GetService(Type typeToResolve) { ・・・} 
	public T GetService<T>() { ・•・} 
	public Object GetService(String typeNickname) ( ... }
}
具体地,GetService方 法可以根据这条建议写成这样:
public Object GetService(String typeNickname)
{
if (typeNickname == "sometype") 
return new SomeType();
 if (typeNickname == "someothertype") 
 return new SomeOtherType();
}

显然,服务定位器可以采用很复杂的实例化方式(间接创建,对象池,单例),从某个配置文件读取抽象类型和具体类型的映射。

在多数情况下,服务定位器被看作反模式。理由是你的代码最终会遍布服务定位器类的引用。更糟糕的情况是,直到运行时你才会发现错误。

依赖注入模式

void Copy(IReader reader^ IWriter writer)
{
	Byte byte; 
	while(byte = reader.Read()) 
		writer.Write(byte);
)

依赖列表现在显式地放在方法签名里,不需要你在代码里自己调用服务定位器组件。此外,为每个发现的依赖创建实例的重担也转移到别的地方了。

向一个类注入依赖有3种方式:使用构造函数、写入属性或者接口。3个技术都是有效的,选择哪个取决于你。我们一般通过构造函数注入,因为这样可以从一开始就清楚表明一个类有什么依赖。

模式的价值

有了需求和设计原则,你就能胜任解决问题的任务了。但是,在通往解决方案的路上,系统地把设计原则用到问题上迟早会把你引向一个熟悉的设计模式。这是肯定的,因为最终模式就是其他人已经找到并收录的解决方案。

简而言之,模式可能是一个终点,你根据它们重构;也可能是一种手段,你可以用来应对明确匹配特定模式的问题。模式对于你的解决方案来说并不是附加价值,它们的价值帮助作为架构师或开发者的你寻找解决方案

重构

・ 提取方法:把几行代码移到一个新建的方法,使原来的方法变得更短,从而促进了可读性和
代码重用。
・提取接口:把现有的类里的公共方法变成一个新建的接口。这样的话,你促进了接口编程和
低耦合模块。

・ 封装字段:使用一对get和 set方法包装类里的一个字段。

防御性编程

防御式编程就是在你拥有的每个方法里小心检查输入数据,通过文档以及其他方式
明确告知每个方法实际上会做什么。做到这点有两种方式:老的方式和现代的高效的方式。

“如果一那么一抛出”模式

广泛使用“如果一那么一抛出"模式一般是为了验证要运行的公共方法的前置条件。它与生成的输出和不变条件无关。“如果一那么一抛出”这个小工具挺有用的,尤其是在你把它用到任何从外部接收的数据上时。

软件契约

契约式设计把软件组件之间的交互描述成契约,权利与义务得到明确表达和强制实施。

总结

无论用何种模式,最终会演变为严重的生存问题,而可维护性高于一切,可维护性才是王道。

代码的可读性对于代码来说是另一个宝贵资产。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值