在大学生活中,无论是学习编程还是数学知识如离散数学和微积分,你都会接触到许多新的概念和原则。一旦走出校园进入职场,要想顺利过渡,有三个重要的原则你需要了解,那就是 KISS、DRY 和 SOLID。这次我们来谈谈其中的 KISS 原则。
照片由 Ivan Aleksic 在 Unsplash 提供。
什么是 KISS 原则呢?那就是“保持简单,别复杂化!”它强调的是简单明了的设计和开发理念。想象一下你在团队中工作,与其他开发人员一起开发项目。当你阅读其他成员的代码时,你希望看到的是整洁、清晰的代码,而不是混乱无章的。同样地,当你在编写复杂的程序时,你也应该遵循这个原则,让你的代码易于理解和维护。
正如 Martin Fowler 所说,“任何傻瓜都能写出计算机能理解的代码。好程序员写出人类能理解的代码。”机器并不关心你的代码是简单还是复杂,但对于人类来说,阅读和理解代码是非常重要的。遵循 KISS 原则至关重要。
那么如何在实际编程中遵循 KISS 原则呢?以 Student 类为例,这个类存储了两个项目和一对映射关系。在设计这样的系统时,我们需要考虑如何让代码更加简洁易懂。比如,我们可以引入更多的抽象和变量来简化代码。下面是一个简化后的例子:
定义 Student 类时,我们可以使用更具描述性的属性名称和更简洁的语法来避免冗余和复杂性。例如:
data class Student(val name: String, val age: Int, val moduleScores: Map>)
在处理学生的成绩超过 80% 的模块时,我们可以使用更简洁的函数和逻辑来记录这些信息。例如:
fun filterHighScores(students: List): Map> {
val highScorers = mutableMapOf>()
students.forEach { student ->
val highScores = student.moduleScores.filter { (_, (score, _)) -> score / _ > 0.8 }
highScorers[student.name] = highScores.map { (_, moduleName) -> moduleName } ?: mutableListOf()
}
return highScorers
数据类学生
我们引入了新的数据类Student,它包含了学生的姓名、年龄以及模块成绩。每个模块的成绩以Map的形式存储,其中键是模块名称,值是Mark对象,包含了学生在此模块上获得的成绩以及该模块的最大成绩。我们还定义了一个Module类来存储模块的名称和ID。
接下来,我们定义了一个Mark类,它包含学生在此模块上获得的成绩(achieved)和该模块的最大成绩(maximum)。为了判断学生的成绩是否超过特定百分比,我们提供了一个方法isAbove。这个方法通过将学生的成绩除以模块的最大成绩然后乘以100来得到百分比,然后判断这个百分比是否超过给定的值。我们还提供了一个方法scholars,它接受一个学生列表作为参数并返回一个Map,这个Map的键是学生的名字,值是学生在所有模块中成绩超过80的模块名称列表。虽然这增加了代码量,但代码看起来更整洁,并且更容易理解。
遵循DRY原则
值得一提的是,我们的代码遵循了SOLID原则中的单一职责原则(Single Responsibility)。这是一个非常重要的设计原则,它告诉我们每个类或者函数都应该只做一件事。这样可以使代码更加清晰、易于理解和维护。如果我们尝试在一个函数中实现多个功能,那么这段代码就会违反单一职责原则。以Repository类为例,原来的代码实现了获取远程数据和存储数据到本地的功能,这违反了单一职责原则。我们应该将这两个功能分开,分别提取到不同的类中。这样,每个类都只负责一个特定的任务,使得代码更加清晰和易于管理。
现在让我们看一下“Spreadsheet”类的具体实现。它包含了一些核心方法,如获取单元格的值和表达式、设置单元格的表达式等。这些方法的实现都非常简洁明了。在内部,我们使用了Java的流操作来查找特定的单元格,然后通过函数式编程的方式对单元格进行操作。这种设计使得代码更加灵活和可复用。无论是获取数据还是设置数据,我们都可以使用相同的方法结构,只需改变函数和默认值即可。这种设计不仅提高了代码的可读性,还提高了代码的可维护性。
Repository类
在隐匿的数据库和缓存服务背后,有一个名为Repository的关键角色。它肩负着获取远程数据的重任,同时与MyRemoteDatabase和MyCachingService紧密相连。每当需要获取API数据时,它都会挺身而出。让我们深入其内部流程。
当调用`fetchRemoteData()`函数时,一个流畅的数据流开始了。Repository从API中抽取数据。随后,这些数据经过MyCachingService的处理并缓存到本地数据库中。如果缓存成功,数据将以模型的形式发出;否则,将发出一个错误信号。这种分离设计遵循了关注点分离的原则,使得缓存和获取数据各自独立,更加易于调试和测试。
MyCachingService类与关注点分离原则
MyCachingService的任务简单而明确——将数据保存到本地数据库。它不参与其他任何操作,只是默默地执行保存任务。这种设计遵循了关注点分离的原则,使得每个组件都能专注于自己的职责,提高了代码的可维护性和可扩展性。
开闭原则的实践
想象一下,你在构建一个Web开发API时采用了开闭原则。你创建了ParagraphTag、AnchorTag和ImageTag等类,这些类都有宽度和高度属性。在客户端代码中,你比较了两个元素的高度。这是一个很好的实践,因为它遵循了开闭原则——软件实体应该易于扩展,但不应修改。这意味着当你需要添加新的标签类型时(如Heading标签),你只需创建新的类并添加必要的比较函数,而无需修改已有的客户端代码。这大大提高了代码的灵活性和可维护性。
---
一、接口声明与扩展之路
让我们先定义一个接口——PageTag。它代表了页面的宽度和高度。想象一下,你在设计网页布局时,不同的元素标签如段落(Paragraph)、锚点(Anchor)和图片(Image)都有统一的尺寸要求。
```plaintext
声明一个通用的接口——PageTag。它很简单,只有两个属性:宽度和高度。
interface PageTag {
val width: Int
val height: Int
}
```
接下来,我们为不同的页面元素创建了继承自PageTag的类。比如,ParagraphTag、AnchorTag和ImageTag,它们都继承了PageTag接口,意味着它们都有宽度和高度属性。
现在,让我们深入客户端代码部分。你发现有一个名为`tallerThan`的函数,它允许两个PageTag对象比较高度。这是一个非常实用的功能!为了扩展这一功能,你可以创建新的类并实现PageTag接口,而所有这些都将无缝运行。想象你正在搭建一个可扩展的积木系统,每一个新创建的类都能无缝融入现有的体系结构中。
二、里式替换原则的魅力
想象一下,我们有一个Bird类,它有一些基本的飞行和吃食功能。但当Penguin继承自Bird时,由于企鹅不会飞,我们必须在其`fly()`方法中抛出异常。这时,如果我们试图用Penguin替换Bird的实例,客户端代码可能会因为未处理的异常而崩溃。这就是违反了里式替换原则——子类型必须能够无缝地替换其基类型。为了避免这种情况,我们可以重新设计我们的类结构,确保替换不会破坏现有的功能。通过引入FlightlessBird类作为中间层,我们可以确保企鹅虽然不会飞,但仍然可以像其他鸟类一样吃东西。这就是设计的灵活性和健壮性所在。
三、接口隔离原则的实践
再想象一下,你正在设计三种交通工具:汽车、飞机和自行车。虽然它们都属于Vehicle这个大类,但它们各自有不同的功能和需求。如果我们强制Vehicle接口包含所有三种交通工具的所有功能和方法,那么每种交通工具都会有大量的未使用方法。这就是接口隔离原则要告诉我们的:不应该强迫客户端依赖它们并不需要的方法或功能。在设计接口时,我们应该尽量保持它们的功能专一和简洁,确保每个接口只做一件事并且做好它。这样,无论是汽车、飞机还是自行车,都能以其特有的方式轻松实现Vehicle接口的要求。这是一种让代码更加清晰、可维护且可扩展的方法。
---
在面向对象编程的世界里,我们经常会遇到各种设计原则与模式,它们旨在帮助我们创建更加灵活、可维护和可扩展的代码结构。最近,我们针对一个关于Vehicle类及其子类的问题进行了重构,深入体验了接口隔离原则的魅力。接下来,我将为您详细解读这次重构背后的逻辑以及依赖倒置原则的核心含义。
让我们回顾一下重构前的设计。在最初的版本中,Vehicle接口强制所有的子类实现了一系列的方法,如turnOn、turnOff、drive、fly和pedal。并不是所有的子类都需要这些方法。例如,自行车并不需要飞行功能,而汽车并不需要踏板功能。这种设计违背了接口隔离原则的核心思想:每个接口应该只定义一个小范围的功能,以供实现者按需选择实现。为了满足这一原则,我们对代码进行了重构。
重构后的代码将原来大而全的Vehicle接口拆分为多个小接口,如SystemRunnable、Drivable、Flyable和Pedalable。每个小接口只定义了一种功能。这样,子类就可以根据自己的需求选择实现哪些接口。例如,Car类选择了SystemRunnable和Drivable接口,实现了turnOn、turnOff和drive方法;Aeroplane类选择了SystemRunnable和Flyable接口,实现了turnOn、turnOff和fly方法;而Bicycle类则只选择了Pedalable接口,实现了pedal方法。这样的设计更加简洁明了,有助于代码的维护和扩展。
接下来,我们来探讨一下依赖倒置原则。这一原则包括两个主要方面:
1. 高层次模块不应该依赖于低层次模块,两者都应当依赖于抽象。在软件开发中,高层次模块通常指的是业务逻辑或用户界面相关的代码,而低层次模块则指的是处理应用程序细节的代码。这意味着无论我们的业务逻辑如何变化,都应该通过抽象接口来与底层代码交互,而不是直接依赖于具体的实现类。这样可以降低代码的耦合度,提高系统的灵活性和可维护性。
2. 抽象不应该依赖于细节。这里的抽象指的是接口或抽象类,细节则指的是具体的实现类。这一原则的核心理念是确保我们的接口设计稳定且通用,不受具体实现的影响。当我们需要修改底层实现时,只要保证遵循了接口定义的行为,就不会影响到上层模块的代码。这有助于我们编写更加健壮和可扩展的代码。
通过遵循接口隔离原则和依赖倒置原则,我们可以创建出更加清晰、灵活和可维护的代码结构。这不仅有助于提高开发效率,还能为未来的系统扩展打下坚实的基础。在理解单一职责原则时,你可能会遇到这样的例子。让我们以一个名为 `Repository` 的类为例,它负责与远程数据库和缓存服务交互以获取数据。如果我们要更改本地数据库或缓存机制,我们可能需要重新编写大量的代码,因为当前实现紧密依赖于具体的实现细节。这种情况违背了单一职责原则,即一个类或模块应该只做一件事,并且做好它。
我们定义两个接口 `CachingService` 和 `SomeLocalDb`,它们分别代表缓存服务和本地数据库的功能。接着,我们的 `Repository` 类将依赖于这些接口而不是具体的实现类。这意味着我们可以为不同的缓存服务和本地数据库创建不同的实现类,如 `MyCachingService`、`MyAltCachingService`、`PostgreSQLLocalDb` 和 `MongoLocalDb` 等。所有这些实现类都将实现相应的接口。
现在,如果我们决定从 PostgreSQL 切换到 MongoDB 或更改缓存机制,我们只需要创建新的实现类并更改 `Repository` 中的引用或配置。整个应用程序的其他部分不需要进行任何更改。这样,我们可以轻松地维护和管理代码,同时保持其灵活性和可扩展性。
探索我的网站——一个富有深度的作品集
欢迎您来到我的网站,这里是我精心打造的作品集。在这个数字时代,我的网站就像是我的个人名片和工作室,展示着我的才华和创意。我倾尽全力将我的思想和热情融入到每一个项目中,通过代码、设计和思考创造出一个丰富多彩的视觉盛宴。在这里,您将看到我对互联网科技的独到见解和创新实践。
每一页都承载着我对于技术和艺术的追求。我的网站不仅仅是一个简单的展示平台,它更像是一座桥梁,连接着我与每一位访客的心灵。我希望通过这里,让更多的人了解我的工作,感受我的热情,并与我一起探索科技的无限可能。
在这里,您可以深入了解我所从事的项目,从构思到实现的全过程。每个项目背后都有一段独特的故事,讲述着我对技术的深入理解和对用户体验的执着追求。通过我的作品集,您可以见证我从一名新手蜕变为技术专家的成长历程。
我的网站还展示了我在不同领域的探索和实践。无论是软件开发、网页设计还是人工智能,我都倾注了心血去研究和实践。在这里,您可以一睹我对未来的展望和对技术的热爱。
我的网站也是您与我交流的平台。我真诚地欢迎每一位访客留下宝贵的意见和建议,共同为科技的进步和发展贡献智慧。在这里,我们不仅可以分享技术知识,还可以分享对生活的热爱和对未来的憧憬。
请深入探索我的网站,感受我的热情和创意。让我们一起在这个充满机遇和挑战的时代中,共同追求科技的美好未来。 |