什么是软件架构?可以用多个层次来解答。最高层,
软件模式定义整体软件的结构。再往下一个层次,和软件应用的目的有关。
再往下一个层次,它可以是模块划分和模块互联。这是设计模式的领域,
包,类,元素,这个层次是本文研究的主题。

架构和依赖

很多软件的设计以一个非常清晰的设计开始,但是再过一段时间,软件的设计开始变质,然后再过一段时间腐烂的地方开始流脓,慢慢在软件的迭代中凸显出来。即使微小的改动都需要很大的努力。以至于工程师和一线项目经理迫切要求软件重构。、

这种重构很少能够成功,即使设计者初始目的没错,但是他们会发现在移动打靶,因为系统不断发展和迭代,新的设计必须跟上。老系统的毛病不断在新的设计中积累,最终“亦使后人而复哀后人矣”。

糟糕设计的特点

糟糕的设计会有四个特点,

  • 扩展性差
    典型的特征就是对一个模块改动导致一系列Bug,项目经理不再轻易敢让工程师们修改代码。这样由于一个设计问题变成一个管理问题。
  • 易碎性
    非常接近于第一种特点,一个地方的改动导致一个不想关的模块突然出问题。
  • 不可移动性
    工程师在想要实现一个功能的时候,突然想到原来实现过类似的逻辑,
    所以找到哪个工程的源码,发现依赖太多,分不出来。代码的可重用性太差,
    最后不得不再实现一遍。
  • 粘性
    两种形式-设计粘性和环境粘性,设计粘性体现在难于使用,易于犯错
    环境粘性就是即使微小的改动都要付出很大的代价,比如一点改动导致整个项目重新编译。

这四种特征是糟糕架构的典型特征。一个应用如果表现出这种特征,那就是烂到骨子里了,成为豆腐渣工程。

需求改动

设计退化的直接原因非常好理解,原始的设计没有预料到的需求改动,导致一些不是特别
了解原始设计方案的工程师做设计妥协,积少成多,最终无可救药。
但是,我们不能将责任归于需求改动上,因为如果软件设计不能做到弹性,可复原,
再需求改动时,灵活调整,那就是设计的问题。

依赖管理

什么导致了设计变得糟糕,因为新的,未在计划内的依赖引入,导致模块间不合理的依赖布局。
为了阻止这种悲剧,需要建立依赖防火墙,使得一个被划分的域内,整体来看不依赖于任何其他域。

面向对象编程类设计原则

  • The Open Closed Principle OCP
    模块开放地接收扩展而不是接收修改。
    模块写好了,可以添加新的类去修改整个模块的功能(即扩展),而不是修改原来写好的类。
    修改源封装类通常是封装技术垃圾的表现。

  • 动态多态性

下面的LogOn函数每次一个新的Modem加入,都要修改其源函数,还有很多冗长的If-else或者switch

 struct Modem {
    enum Type {hayes, courrier, ernie) type;
 };
 
    struct Hayes
    {
    Modem::Type type;
    // Hayes related stuff
    };
    
    struct Courrier
    {
    Modem::Type type;
    // Courrier related stuff
    };
    
    struct Ernie
    {
    Modem::Type type;
    // Ernie related stuff
    };
    
    void LogOn(Modem& m,
    string& pno, string& user, string& pw)
    {
    //冗长的if-else语句
    if (m.type == Modem::hayes)
    DialHayes((Hayes&)m, pno);
    else if (m.type == Modem::courrier)
    DialCourrier((Courrier&)m, pno);
    else if (m.type == Modem::ernie)
    DialErnie((Ernie&)m, pno)
    // ...you get the idea
    }

作为OCP的一个例子,考虑下面一种模式:

设计原则和模式---Robert C.Martin

LogOn(Modem modem)
  • LSP(The Liskov Substitution Principle)里氏替换

可以理解为面向对象中的继承,子类可以完全替代基类。
设计原则和模式---Robert C.Martin
典型的例子就是初学Java时的机动车辆(Vehicle)和小汽车(Car)

void User(Base base){
        Base.somemethod()
}

随后,作者引入椭圆-圆的设计来演示LSP的作用引入约定编程(Spring Boot提倡的Programming By Contract)和OCP和LSP之间的关系。

圆是一个长距和短距相等的椭圆,但是很有可能会因为小聪明犯了大错误。

设计原则和模式---Robert C.Martin
但是圆只需要一个圆心和半径,于是可能为了强撑继承的关系(按道理来讲的确该这么做),再设置长短距时。

void Circle::SetFoci(const Point& a, const Point& b)
{
itsFocusA = a;
itsFocusB = a;
}

设计的工程师当然能够自圆其说地完成程序的设计和运行,但是客户端发生调用时,就会出错

void f(Ellipse& e)
{
Point a(-1,0);
Point b(1,0);
e.SetFoci(a,b);
e.SetMajorAxis(3);
assert(e.GetFocusA() == a);
assert(e.GetFocusB() == b);
assert(e.GetMajorAxis() == 3);
}

客户端将Circle对象传入函数f(Ellipse& e)中,会发现改了b,但是获取的时候b没有改变。当然这里有很多workaround来周旋这个问题,不过这不是重点。

面向约定编程:为了保持子类能够完全替代基类完成功能的目的,子类必须
遵守基类的约定。上面的例子就是圆破坏了椭圆的约定,它忽略了最后一个
后面的一个短距。因为椭圆没有长短距相同的约定,但子类Circle有。

  • DIP( Dependency Inversion Principle (DIP))依赖反转原则
	Depend upon Abstractions. Do not depend upon concretions.

因为抽象类和接口是不容易变的(遵照OCP原则,你甚至不该去改变它),而且抽象类和接口是程序实现和程序设计的咬合点,是设计可以扩展和改变的地方,一个合格的软件结构应该是这样。
设计原则和模式---Robert C.Martin

这其实就是DI(依赖注入),Spring in Action第一章节作者已经给出很详细的说明,参见第一章。

  • ISP(Interface Segregation Principle)
Many client specific interfaces are better than one general
purpose interface

这个原则其实说的是,不应该设计胖接口,抽象要分层次,一个胖接口试图包罗万象,导致代码臃肿,耦合度高。
这个原则其实是接口设计应该遵守的规范。如果一个外部接口提供一个大的方面的功能,比如啥?还是比如还是机动车辆(Vehicle)和小汽车(Car)和货车(Trunk)

设计原则和模式---Robert C.Martin
一个方法按说应该只接收接口,这是DI给我们的提示。

比如上面的设计就是一个反例,货车的敞篷功能简直就是扯淡,但是还得去实现它,所以两种不同类别的子类具体逻辑污染了接口,导致接口变胖,一个改进方法是将抽象分层。

设计原则和模式---Robert C.Martin
这样实现一个接口层次,能将给模块更小的耦合性,也符合DI给我们的暗示。

相关文章:

  • 2021-11-30
  • 2021-11-27
  • 2021-09-08
  • 2022-12-23
  • 2021-06-19
猜你喜欢
  • 2021-11-30
  • 2021-12-02
  • 2022-02-22
  • 2022-01-07
  • 2022-12-23
  • 2021-12-03
  • 2021-05-15
相关资源
相似解决方案