重视公众号,提早Get更多技能好文

当开发大型软件时,编写易于保护和扩展的代码变得至关重要。SOLID准则是一组指导准则,可帮助咱们完成高质量、易于保护的代码。这些准则旨在使软件架构更加健壮、灵敏和可扩展,然后削减后期保护成本。

SOLID准则由Robert C. Martin在他的书本《Agile Software Development, Principles, Patterns, and Practices》中提出。它们代表了一组面向方针编程(OOP)的最佳实践。下面将介绍SOLID准则的五个组成部分。

  • 单一责任准则(Single Responsibility Principle, SRP): 一个类应该只要一个引起它改变的原因。这意味着一个类应该只要一项责任,因而它应该只要一个原因需求被修正。经过将代码分解成更小、更简略的部分,咱们能够轻松地对其进行修正和保护,然后进步代码的可读性和可保护性。

  • 敞开关闭准则(Open-Closed Principle, OCP): 软件实体(类、模块、函数等)应该是对扩展敞开的,但对修正关闭的。这意味着当咱们需求添加新功用时,咱们应该尽或许地利用现有的代码,而不是修正它们。这能够经过运用接口和笼统类等OOP技能来完成。

  • 里氏替换准则(Liskov Substitution Principle, LSP): 全部引证基类的当地有必要能够透明地运用其子类的方针。这意味着子类应该能够替换其基类,而且程序的行为不会受到影响。遵从LSP准则能够确保代码的正确性和安稳性。

  • 接口阻隔准则(Interface Segregation Principle, ISP): 客户端不应该依靠于它不需求的接口。这意味着咱们应该将接口拆分红更小、更特定的部分,以防止客户端代码依靠于不必要的接口。这将进步代码的可保护性和可扩展性。

  • 依靠回转准则(Dependency Inversion Principle, DIP): 高层模块不应该依靠于低层模块,它们应该依靠于笼统。笼统不应该依靠于细节,细节应该依靠于笼统。这能够经过依靠注入(DI)等技能完成。遵从DIP准则能够下降模块之间的耦合度,然后进步代码的灵敏性和可扩展性。

单一责任准则

SRP 是 SOLID 五大规划准则中最简单被误解的一个,也许是姓名的原因,很多程序员以为这个准则便是指:每个模块都应该只做一件事。然而,这并不是 SRP 的全部。在实际环境中,软件体系为了满足用户需求,必然要做出这样那样的修正,所以 SRP 的最终描述就变成了:

任何一个软件模块都应该只对某一类行为者负责。

“软件模块”指的是一组紧密相关的函数和数据结构。相关这个词实际上就隐含了 SRP 这一准则。代码和数据便是靠着与某一类行为者的相关性被组合在一起的。

下面看一个违反 SRP 准则的比如:

工资办理程序中的 Employee 类有三个函数,calculatePay()、reportHours()和save()。这三个函数分别对应三个不同的行为者。

软件架构之SOLID原则

这三个函数被放在同一个类中,这样做实际上是将三类行为者的行为耦合在了一起,这有或许导致CFO团队的指令影响到了COO团队所依靠的功用。

有很多不同的办法能够处理上面的问题,每一种办法都需求将相关函数划分红不同的类,即使每个类都只对应一类行为者。

小结: 单一责任准则首要讨论的是函数与类之间的联系。

开闭准则

开闭准则是由 Bertrand Meyer 在1988年提出的,该规划准则以为:

规划杰出的计算机软件应该易于扩展,一起抵抗修正。

一个杰出的软件架构师会努力将旧代码的修正需求量降至最小,甚至为0。

下面经过一个比如来了解开闭准则:

假定咱们需求规划一个在Web页面上展现财务数据的体系,页面上的数据需求滚动展现,其间负值显现为红色。接下来,该体系的全部者又要求用同样的数据生成一份报表,该报表能够用黑白打印机打印,一起报表格式要得到合理分页等。

  • 首先,咱们能够将不同需求的代码分组(SRP)

软件架构之SOLID原则

  • 然后再调整这些分组之间的依靠联系(DIP)

软件架构之SOLID原则

这儿很重要的一点是这些单线框的边界都是单向跨过的。也是说,上面的全部组件之间的联系都是单向依靠的。

假如组件A不像因为组件B的修正而受到影响,那么就该让组件B依靠于组件A。

小结: OCP 是咱们进行架构规划的主导准则,其首要方针是让体系易于扩展,一起限制其每次被修正所影响的范围。完成办法是经过将体系划分为一系列组件,并将这些组件间的依靠联系按层次结构进行组织,使得高阶组件不会因低阶组件被修正而受到影响。

里氏替换准则

里氏替换准则是由美国计算机科学家Barbara Liskov 在1987年提出的。她在一篇名为《数据笼统和层次》的论文中首次提出了该准则,该准则强调了在面向方针编程中承继的运用,即子类方针应该能够替换其父类方针而且依然能够保持原有的行为表现。

下面经过一个简略的比如来了解 LSP:

软件架构之SOLID原则

上述规划是契合 LSP 准则的,因为 Bi11ing 应用程序的行为并不依靠于其运用的任何一个衍生类。也便是说,这两个衍生类的方针都是能够用来替换 License 类方针的。

接口阻隔准则

“接口阻隔准则”这个姓名来自下图这种软件结构。

软件架构之SOLID原则

有多个用户需求操作 OPS 类。但是 User1 只需求运用 op1, User2 只需求运用 op2,User3 只需求运用op3。

在这种情况下,User1 虽然不需求调用 op2、op3,但在源代码层次上也与它们构成依靠联系。这种依靠意 味着咱们对 OPS 代码中 op2 所做的任何修正,即使不会影响到 User1 的功用,也会导致它需求被从头编译和部署。 这个问题能够经过将不同的操作阻隔成接又来解決,详细如图所示:

软件架构之SOLID原则

User1 的源代码会依靠于 U1Ops 和 op1,但不会依靠于OPS。这样一来,咱们之后对OPS做的修正只要不影响到 User1 的功用,就不需求从头编译和部署 User1 了。

**小结:**任何层次的软件规划假如依靠了它并不需求的东西,就会带来意料之外的麻烦。

依靠回转准则

依靠回转准则(DIP)首要想告知咱们的是,假如想要规划一个灵敏的体系,在源代码层次的依靠联系中就应该 多引证笼统类型,而非详细完成。

咱们每次修正笼统接口的时候,一定也会去修正对应的详细完成。相反,当咱们修正详细完成时,却很少需求修正相应的笼统接口。所以咱们以为接口比完成更安稳。

下面经过一个比如来了解DIP

以一个图书馆办理体系为例,假定体系需求支撑三种不同类型的书本:小说、历史书本和科学书本。一起,体系需求能够供给书本的分类、名称、作者等信息,并支撑借出和归还书本的功用。

按照依靠回转准则,咱们能够经过笼统类或接口来定义一个书本的通用接口,全部的详细书本类型都完成该接口,如下所示:

protocol Book {
    var name: String { get }
    var author: String { get }
    var type: String { get }
    var isBorrowed: Bool { get set }
}
class Novel: Book {
    var name: String
    var author: String
    var type: String = "小说"
    var isBorrowed: Bool = false
    init(name: String, author: String) {
        self.name = name
        self.author = author
    }
}
class HistoryBook: Book {
    var name: String
    var author: String
    var type: String = "历史书本"
    var isBorrowed: Bool = false
    init(name: String, author: String) {
        self.name = name
        self.author = author
    }
}
class ScienceBook: Book {
    var name: String
    var author: String
    var type: String = "科学书本"
    var isBorrowed: Bool = false
    init(name: String, author: String) {
        self.name = name
        self.author = author
    }
}

此时,全部书本类型都完成了Book接口,而且都能供给相同的特点和办法。这样一来,咱们在运用这些不同类型的书本时,就能够依靠于它们完成的相同接口,而不是依靠于详细的书本类型。

下面是一个简略的借阅功用完成的比如:

class Library {
    var books: [Book] = []
    func borrow(book: Book) {
        guard let index = books.firstIndex(where: { $0.name == book.name }) else {
            print("该书不存在")
            return
        }
        if books[index].isBorrowed {
            print("该书已被借出")
        } else {
            books[index].isBorrowed = true
            print("借书成功")
        }
    }
    func returnBook(book: Book) {
        guard let index = books.firstIndex(where: { $0.name == book.name }) else {
            print("该书不存在")
            return
        }
        if !books[index].isBorrowed {
            print("该书未被借出")
        } else {
            books[index].isBorrowed = false
            print("还书成功")
        }
    }

在这个比如中,Library类并不依靠于详细的书本类型,而是依靠于Book接口。这使得咱们能够轻松地将来添加更多的书本类型,而无需更改Library类的代码。

优秀的软件架构师会花费大精力来规划接口,以削减未来对其进行改动。 究竟争夺在不修正接口的情况下为软 件添加新的功用是软件规划的根底常识。

也便是说,假如想要在软件架构规划上追求安稳,就有必要多运用安稳的笼统接口,少依靠多变的详细完成。下面有几条详细的编码守则:

  • 应在代码中多运用笼统接口,尽量防止运用那些多变的详细完成类。
  • 不要在详细完成类上创立衍生类。在静态类型的编程语言中,承继联系是全部全部源代码依靠联系中最强的、最难被修正的,所以咱们对承继的运用应该格外小心。即使是在略微便于修正的动态类型语言中,这条守则也应该被仔细考虑。
  • 不要掩盖(override)包含详细完成的函数。调用包含详细完成的函数一般就意味着引入了源代码等级的依靠。即使掩盖了这些函数,咱们也无法消除这其间的依靠。这些函数承继了那些依靠联系。在这儿,操控依靠联系的唯一办 法,便是创立一个笼统函数,然后再为该函数供给多种详细完成。
  • 应防止在代码中写入与任何详细完成相关的姓名,或者是其他简单变动的事物的名宇。这根本上是DIP准则的别的一个表达办法。

假如想恪守上述的编码守则,咱们就有必要对那些易变方针的创立过程做一些特别处理,这样的谨慎是很有必要的,因为,根本在全部的编程语言中,创立方针操作都免不了在源代码层次上依靠方针的详细完成。

在大部分编程语言中,人们都会挑选笼统工厂形式来处理源代码依靠问题。

软件架构之SOLID原则

这条曲线将整个体系划分为两部分组件:笼统接口层与详细完成层边界。笼统接口组件中包含了应用的全部高阶业务规矩,而详细完成组件则包含了全部这些规矩所需求做的详细操作及其相关的细节信息。

小结: 依靠回转准则是一条非常重要的软件规划准则,它能够帮助咱们规划出高度解耦的体系,进步体系的灵敏性和可扩展性。


注:本文大部分内容来自于《架构整洁之道》