根据我一年级本科生的经验,并听取高年级学生的经验,学校和学院会教你如何编程,以及离散数学和微积分等所需的数学。但是当你离开大学进入这个行业时,你必须知道一些概念和原则才能轻松过渡。我们将讨论亲吻,干燥以及实线原则。
KISS原则
保持简单和直接!
通常,你会发现自己在一个团队中一起工作。一组从事项目不同方面工作的开发人员。如果你要切换并处理别人的代码,你想看到一个没有注释或变量的混乱代码吗?或者希望看到一个文档化的代码,很好地解释项目的这一部分?显然是后者。
如果你正在编写一个非常复杂的程序,并说你生病了。一周后,你恢复了健康,重新审视了你的项目。当然,你会失去编程的流畅性,但你想来看一段你不再理解的代码吗?或者看到一些你一开始就能稍微理解的东西?再一次,后者。
现在想象一下,你一个人在做一个项目,每天都在做。你会遇到bug,你需要调试它们。如果你的代码比实际更复杂,调试代码会有多容易?
以上所有场景都指向一件事——保持简单,愚蠢!
“任何傻瓜都能写出计算机能理解的代码。优秀的程序员编写人类可以理解的代码。”——马丁·福勒
最终,机器不会在乎你写了简单的代码还是复杂的代码来完成某项任务。相比之下,对于那些阅读并试图理解代码的人(包括你)来说,这绝对很重要。
但我该怎么接吻呢?(听起来很奇怪,我知道)
考虑一个示范班,学生。这将存储2个项目和一个键和值都是对的映射。第一对由两个字符串组成:模块的名称和id。第二对由2个double、已达到的标记和该模块的最大标记组成。
数据类学生(
val名称:字符串,
年龄:Int,
val moduleMarks:映射<对<字符串,字符串>,对<整数,整数>>
)
在做出这样的设计选择后,您现在需要记录学生的姓名以及他们得分超过80%的模块。
趣味学者(学生:列表<Student>):地图<String,列表<String>>{
val scholars=mutableMapOf<字符串,列表<字符串>>()
students.forEvery{学生->
学者[student.name]=student.moduleMarks
.filter{(_,(a,m))->a/m>0.8}
.map{((n,_),_)->n}
}
归国学者
} 即使在几天后回到这样的代码也会是灾难性的。虽然你可以说这是一个相当简单的例子,但有一些方法可以让它更简单。尽可能引入更多的抽象和变量。
数据类学生(
val名称:字符串,
年龄:Int,
val模块标记:映射<模块,标记>
)
数据类模块(
val名称:字符串,
val-id:字符串
)
数据类标记(
val实现:翻倍,
val最大值:双倍
) {
乐趣高于(百分比:双倍):布尔值{
已实现回报/最大值*100>百分比
}
趣味学者(学生:列表<Student>):地图<String,列表<String>>{
val scholars=mutableMapOf<字符串,列表<字符串>>()
students.forEvery{学生->
val模块80以上=学生模块标记
.filter{(_,mark)->mark.is高于(80.0)}
.map{(模块,_)->module.name}
学者[学生姓名]=模块80岁以上
}
归国学者
}
这增加了很多代码。但更重要的是,代码看起来更清晰,读起来像英语。
干燥原理
不要重复自己
如果你发现你一遍又一遍地执行相同的代码,请创建一个函数并重用它。在我的大学作业中,我正在处理一组对象(单元格),定义的大多数(如果不是全部)函数都要求我从集合中搜索和获取特定对象并对其进行操作。
公共类电子表格实现了BasicSpreadsheet{
私有最终设置<Cell>单元格;
以(权力)否决
公共双getCellValue(CellLocation位置){
单元格=单元格.stream()
.filter(单元格->单元格.location.equals(位置))
.findFirst()
.orElse(空);
返回单元格==null?0d:cell.getValue();
}
以(权力)否决
公共字符串getCellExpression(CellLocation位置){
单元格=单元格.stream()
.filter(单元格->单元格.location.equals(位置))
.findFirst()
.orElse(空);
返回单元格==null?“”:cell.getExpression();
}
以(权力)否决
public void setCellExpression(CellLocation位置,字符串输入)抛出InvalidSyntaxException{
单元格=单元格.stream()
.filter(单元格->单元格.location.equals(位置))
.findFirst()
.orElse(空);
// ...
}
// ...
} 这是上面的一个大代码。但是,当我键入这段代码时,我发现自己在不同的部分多次复制粘贴了同一段代码。因此,我将它们抽象为函数,并在任何地方重复使用。
公共类电子表格实现了BasicSpreadsheet{
私有最终设置<Cell>单元格;
以(权力)否决
公共双getCellValue(CellLocation位置){
返回getFromCell(位置,单元格::getValue,0d);
}
以(权力)否决
公共字符串getCellExpression(CellLocation位置){
返回getFromCell(位置,单元格::getExpression,“”);
}
以(权力)否决
public void setCellExpression(CellLocation位置,字符串输入)抛出InvalidSyntaxException{
单元格=findCell(位置);
// ...
}
// ...
私人手机findCell(手机位置){
返回单元格.stream()
.filter(单元格->单元格.location.equals(位置))
.findFirst()
.orElse(空);
}
private<T>T getFromCell(CellLocation位置,
功能<细胞,T>功能,
T默认值){
单元格=findCell(位置);
返回单元格==null?defaultValue:函数.apply(单元格);
}
} 这样,如果我意识到我的代码中有一个错误,我就不会在以下位置更改代码n不同的地方。在函数内部更改一次就足以修复所有地方的错误。
固体原则
这不是一个单一的原则,而是对软件开发至关重要的5个原则。
S——单一职责
一个类应该有一个,而且只有一个改变的理由。
这可能是最容易理解的原则。您定义的每个类/函数只能执行一个任务。假设您正在构建一个网络应用程序。
类存储库(
私有val-api:MyRemoteDatabase,
私有val-local:MyLocalDatabase
) {
fun fetchRemoteData()=流{
//正在获取API数据
val响应=api.getData()
//将数据保存在缓存中
var模型=模型解析(响应有效载荷)
val success=本地.addModel(模型)
如果(!成功){
emit(错误(“缓存远程数据时出错”)
return@flow
}
//从单一真实来源返回数据
model=本地.find(model.key)
发射(成功(模型))
}
}
上述代码违反了单一责任原则。该函数不仅获取远程数据,还负责在本地存储数据。这应该提取到另一个类中。
类存储库(
私有val-api:MyRemoteDatabase,
private val-cache:MyCachingService/*注意我更改了依赖关系*/
) {
fun fetchRemoteData()=流{
//正在获取API数据
val响应=api.getData()
val模型=缓存保存(响应有效载荷)
//发回数据
模型?。让{
发射(成功(it))
}?:emit(错误(“缓存远程数据时出错”)
}
}
//将所有缓存逻辑转移到另一个类
类MyCaching服务(
私有val-local:MyLocalDatabase
) {
暂停乐趣保存(有效载荷:有效载荷):型号?{
var模型=模型解析(有效载荷)
val success=本地.addModel(模型)
返回if(成功)
local.find(model.key)
其他的
无效的
}
}
请注意,存储库只负责获取发送上述模型的数据,而只负责将传入的有效负载保存到本地数据库中。这样做是一种很好的做法,因为有一种叫做关注点分离这提高了调试和可测试性。MyCaching服务
O–打开/关闭
软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。
这一原则基本上意味着不要编写在未来更改时会破坏客户端代码的软件代码.考虑一下您正在Kotlin构建一个web开发API。您已经设计了ParagraphTag、AnchorTag和ImageTag。在代码中,要求您比较两个元素的高度。
类段落标记(
val宽度:整数,
val高度:整数
)
类锚标签(
val宽度:整数,
val高度:整数
)
类图像标签(
val宽度:整数,
val高度:整数
)
//客户端代码
infix fun ParagraphTag.telerThan(锚点:AnchorTag):布尔值{
返回此.height>anchor.height
}
infix fun锚点标签.telerThan(锚点:ParagraphTag):布尔值{
返回此.height>anchor.height
}
infix fun ParagraphTag.telerThan(锚点:ImageTag):布尔值{
返回此.height>anchor.height
}
// ... 更多功能
叹气!这是一项艰巨的工作。现在你有了新的要求,要求你也要包含一个标题标签。您必须添加还有六个功能在客户端。这不仅乏味,你也修改客户端代码,以满足您的程序需求。
相反,声明一个接口——页面标签
界面PageTag{
val宽度:整数
val高度:整数
}
类段落标记(
覆盖值宽度:Int,
覆盖值高度:Int
):页面标签
类锚标签(
覆盖值宽度:Int,
覆盖值高度:Int
):页面标签
类图像标签(
覆盖值宽度:Int,
覆盖值高度:Int
):页面标签
//客户代码
infix fun PageTag.tallerThan(其他:PageTag):布尔值{
return this.height>other.height
} 现在你有了关闭用于进一步修改它的客户端代码。为了延伸你的功能,它是打开创建一个新类并实现,一切都会完美运行。页面标签
L——利斯科夫替补
如果S是T的一个子类型,那么任何可由T证明的性质也必须由S证明。
哦。数学?好吧,这可不好。相比之下,这是一个容易理解的原则。让我们考虑一个新的例子。
公开课鸟{
开玩笑(){
// ... 执行代码以飞行
}
开玩笑(){
// ...
}
}
类企鹅:鸟(){
覆盖趣味飞行(){
抛出UnsupportedOperationException(“企鹅不能飞”)
}
} 请注意,当类抛出异常时,上面的类不会抛出任何异常。 如果不破坏或修改客户端代码,则无法在客户端代码中将Penguin替换为Bird这违反了利斯科夫替代原则。扩展会破坏客户端代码,从而也违反了开/关原则。鸟企鹅企鹅鸟
解决这个问题的一种方法是更改您的设计实现。
公开课FlightlessBird{
开玩笑(){
// ...
}
}
公开课鸟:FlightlessBird(){
开玩笑(){
// ...
}
}
企鹅类:无飞鸟(){
// ...
}
类鹰:鸟(){
// ...
} 上面的代码解释了如果一个可以吃,那么所有子类也可以吃。同样,如果可以飞行,那么的所有子类也必须飞行。无飞鸟无飞鸟鸟鸟
I——接口隔离
接口不应强制其客户端依赖于它不使用的方法。
这个定义看起来并不可怕。事实上,这并不可怕。设想你正在制造一辆汽车、一架飞机和一辆自行车。由于它们都是车辆,因此您正在实现车辆接口。
接口车辆{
有趣的开关()
乐趣关闭()
有趣的驾驶()
有趣的飞行()
趣味踏板()
}
类别汽车:车辆{
override fun turnOn(){/*实现*/}
override fun turnOff(){/*实现*/}
覆盖乐趣驱动(){/*实现*/}
override fun fly()=单位
超控乐趣踏板()=单位
}
飞机等级:车辆{
override fun turnOn(){/*实现*/}
override fun turnOff(){/*实现*/}
override fun drive()=单位
override fun fly(){/*实现*/}
超控乐趣踏板()=单位
}
类别自行车:车辆{
override fun turn()=单位
覆盖乐趣关闭()=单位
override fun drive()=单位
override fun fly()=单位
override fun pedal(){/*实现*/}
} 讨厌!看看这些类是如何被迫实现它不需要的方法的?我也不能将类声明为抽象类。根据接口隔离原则,我们应该采用这种设计。
接口系统可运行{
有趣的开关()
乐趣关闭()
}
接口可驱动(){
有趣的驾驶()
}
接口Flyable(){
有趣的飞行()
}
接口踏板(){
趣味踏板()
}
汽车等级:系统可运行、可行驶{
override fun turnOn(){/*实现*/}
override fun turnOff(){/*实现*/}
覆盖乐趣驱动(){/*实现*/}
}
飞机等级:系统可运行、可飞行{
override fun turnOn(){/*实现*/}
override fun turnOff(){/*实现*/}
override fun fly(){/*实现*/}
}
类别自行车:可脚踏{
override fun pedal(){/*实现*/}
} 现在,这看起来更清晰了,也更容易通过接口引用不同的功能。
D——依赖倒置
1.高级模块不应依赖于低级模块;两者都应该依赖于抽象。
2.抽象不应取决于细节。细节应该取决于抽象。
这到底是什么意思?高级模块是业务或UI看到的模块。低级模块是处理应用程序复杂性的模块。回想一下我在《坚实责任原则》中的例子:
类存储库(
私有val-api:MyRemoteDatabase,
私有val缓存:MyCachingService
) {
fun fetchRemoteData()=流{
//正在获取API数据
val响应=api.getData()
val模型=缓存保存(响应有效载荷)
//发回数据
模型?。让{
发射(成功(it))
}?:emit(错误(“缓存远程数据时出错”)
}
}
类MyRemoteDatabase{
挂起乐趣getData():响应{/*…*/}
}
类MyCaching服务(
私有val-local:MyLocalDatabase
) {
suspend fun save():模型?{ /* ... */ }
}
类MyLocalDatabase{
暂停乐趣添加(模型:model):布尔值{/*…*/}
暂停乐趣查找(关键字:Model.key):模型{/*…*/}
}
它看起来不错,而且会完美工作。但是,如果将来我决定将本地数据库从PostgreSql更改为MongoDB;或者,如果我决定完全更改我的缓存机制,我将不得不更改整个实现细节,以及客户端代码。高级模块依赖于低级具体模块。
这是不对的。相反,您必须将功能抽象到接口中,并让具体的实现对其进行扩展。
接口CachingService{
suspend fun save():模型?
}
接口SomeLocalDb(){
暂停乐趣添加(模型:model):布尔值
暂停乐趣查找(关键字:Model.key):Model
}
类存储库(
私有val-api:SomeRemoteDb,
私有val缓存:CachingService
){/*实施*/}
类MyCaching服务(
私人val-local:SomeLocalDb
):CachingService{/*实现方法*/}
MyAltCachingService类(
私人val-local:SomeLocalDb
):CachingService{/*实现方法*/}
类PostgreSQLLocalDb:SomeLocalDb{/*实现方法*/}
类MongoLocalDb:SomeLocalDb{/*实现方法*/} 通过更改一个单词,您可以在整个应用程序中轻松更改存储库的不同实现。每次听它,我都会不寒而栗。
我花了相当长的时间来阐明这篇文章的每一个信息。我希望你喜欢读它,学到了一些东西。非常感谢。


![表情[baoquan]-拾光赋](https://blogs.ink/wp-content/themes/zibll/img/smilies/baoquan.gif)


暂无评论内容