雁过留声 发表于 2025-3-26 13:43:34

企业应用架构笔记-软件筹划的原则

软件筹划的原则

   所有编程都是维护编程,因为你很少写原创代码。只有你在最初10分钟里键入的代码是原创的。仅此而已。-----Dave Tomas 和 Andy Hunt
软件开辟的一个盛行格言是:好的架构就是全部艰巨决定终极都被证明是正确的架构。
https://i-blog.csdnimg.cn/direct/73d85ddd82f14b62a483e95b5bdaf3cc.gif
软件筹划的通用原则

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

内聚表示软件模块承担的众多职责关联性很强,不管是子程序,类,还是库。内聚
用来权衡通过类的各个方法、库的各个函数以及方法完成的各个动作表达的逻辑之间的距离。
内聚的度量范围从低到高,越高越好。
高内聚类有助于维护和重用,因为它们倾向于没有依靠。另一方面,低内聚使类的目的难以明确,并使软件变得僵硬和脆弱。降低内聚导致创建的类在职责(即方法)上共同点很少,并且引用不同的不相干的活动。
总结成实用准则就是:内聚原则建议创建非常专注的类,有少量方法表示逻辑上相干的操纵。如果方法之间的逻辑距离增加,你只需创建一个新类。
极限编程先锋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方法包装类里的一个字段。
防御性编程

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

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

左券式筹划把软件组件之间的交互形貌成左券,权利与任务得到明确表达和强制实验。
总结

无论用何种模式,终极会演变为严峻的生存问题,而可维护性高于一切,可维护性才是王道。
代码的可读性对于代码来说是另一个名贵资产。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 企业应用架构笔记-软件筹划的原则