他们在学校不教你的编程原则

根据本科一年级学生的经验,并听取了前辈的经验,学校和学院会教你如何编程,以及所需的数学,如离散数学和微积分。但是当你离开大学进入这个行业时,你必须了解一些概念和原则才能轻松过渡。我们将讨论 KISS、DRY 和 SOLID 原则。

图片[1]-他们在学校不教你的编程原则
照片由 伊万·阿列克西奇 on Unsplash

KISS 原则

保持简单,笨蛋!

通常,您会发现自己在一个团队中一起工作。一组开发人员,负责项目的不同方面。如果你要切换并处理其他人的代码,你是否希望看到没有任何注释或变量的凌乱代码?或者希望看到一个文档齐全的代码来很好地解释项目的这一部分?显然是后者。

如果你正在编写一个非常复杂的程序,并说你生病了。您在恢复一周后重新访问您的项目。当然,你会失去编程的流程,但你想来看看你不再理解的代码吗?或者看到一些你可以稍微理解的东西?同样,后者。

现在想象一下,你正在独自从事一个项目,而且你每天都在做这个项目。您将遇到 bug,您需要调试它们。如果您的代码比必须的更复杂,调试代码会有多容易?

以上所有情况都指向一件事 — 保持简单,愚蠢!

任何傻瓜都可以编写计算机可以理解的代码。优秀的程序员编写的是人类可以理解的代码。

归根结底,机器不会关心你是写了简单的代码还是复杂的代码来完成某个任务。相比之下,对于阅读并尝试理解代码的人类(包括您)来说,这绝对很重要。

但是我该如何KISS呢?(我知道听起来很奇怪)

考虑一个模型类 Student。这将存储 2 个项目和一个 key 和 value 都是对的 map。第一对由 2 个字符串组成:模块的 name 和 id。第二对是 2 个双倍、达到的分数和该模块的最高分数。

data class Student(
val name: String,
val age: Int,
val moduleMarks: Map<Pair<String, String>, Pair<Int, Int>>
)

在做出这样的设计选择后,您现在需要记录学生的姓名和他们得分超过 80% 的模块。

fun scholars(students: List<Student>): Map<String, List<String>> {
val scholars = mutableMapOf<String, List<String>>()
students.forEach { student ->
scholars[student.name] = student.moduleMarks
.filter { (_, (a, m)) -> a / m > 0.8}
.map { ((n, _), _) -> n }
}
return scholars
}

即使过了几天又回到这样的代码,那将是灾难性的。尽管您可以争辩说这是一个相当简单的示例,但有一些方法可以使其更简单。尽可能引入更多的抽象和变量。

data class Student(
val name: String,
val age: Int,
val moduleMarks: Map<Module, Mark>
)

data class Module(
val name: String,
val id: String
)

data class Mark(
val achieved: Double,
val maximum: Double
) {
fun isAbove(percentage: Double): Boolean {
return achieved / maximum * 100 > percentage
}

fun scholars(students: List<Student>): Map<String, List<String>> {
val scholars = mutableMapOf<String, List<String>>()
students.forEach { student ->
val modulesAbove80 = student.moduleMarks
.filter { (_, mark) -> mark.isAbove(80.0)}
.map { (module, _) -> module.name }

scholars[student.name] = modulesAbove80
}
return scholars
}

这会增加很多代码。但更重要的是,代码看起来更简洁,读起来像英语。

DRY 原理

不要重复自己

如果您发现您正在一遍又一遍地执行相同的代码,请创建一个函数并重复使用它。在我的大学作业中,我正在处理一组对象(单元格),定义的大多数(如果不是全部)函数都要求我从该集合中搜索和获取特定对象并对其进行操作。

public class Spreadsheet implements BasicSpreadsheet {
private final Set<Cell> cells;

@Override
public double getCellValue(CellLocation location) {
Cell cell = cells.stream()
.filter(cell -> cell.location.equals(location))
.findFirst()
.orElse(null);

return cell == null ? 0d : cell.getValue();
}

@Override
public String getCellExpression(CellLocation location) {
Cell cell = cells.stream()
.filter(cell -> cell.location.equals(location))
.findFirst()
.orElse(null);

return cell == null ? "" : cell.getExpression();
}

@Override
public void setCellExpression(CellLocation location, String input) throws InvalidSyntaxException {
Cell cell = cells.stream()
.filter(cell -> cell.location.equals(location))
.findFirst()
.orElse(null);

// ...
}

// ...
}

这是上面的一个大代码。但是当我键入此代码时,我发现自己在不同部分多次复制粘贴同一代码块。因此,我将它们抽象为我在任何地方重复使用的函数。

public class Spreadsheet implements BasicSpreadsheet {
private final Set<Cell> cells;

@Override
public double getCellValue(CellLocation location) {
return getFromCell(location, Cell::getValue, 0d);
}

@Override
public String getCellExpression(CellLocation location) {
return getFromCell(location, Cell::getExpression, "");
}

@Override
public void setCellExpression(CellLocation location, String input) throws InvalidSyntaxException {
Cell cell = findCell(location);

// ...
}

// ...

private Cell findCell(CellLocation location) {
return cells.stream()
.filter(cell -> cell.location.equals(location))
.findFirst()
.orElse(null);
}

private <T> T getFromCell(CellLocation location,
Function<Cell, T> function,
T defaultValue) {
Cell cell = findCell(location);
return cell == null ? defaultValue : function.apply(cell);
}
}

这样,如果我意识到我的代码中存在错误,我就不必在 个不同的地方更改代码。在函数内部更改一次就足以修复所有错误。

SOLID 原则

这不是一个单一的原则,而是软件开发中至关重要的 5 个原则。

S — 单一职责

一个类应该有且只有一个更改的理由。

可能是最容易理解的原理。您定义的每个类/函数只能执行一个任务。假设您正在构建一个网络应用程序。

class Repository(
private val api: MyRemoteDatabase,
private val local: MyLocalDatabase
) {
fun fetchRemoteData() = flow {
// Fetching API data
val response = api.getData()

// Saving data in the cache
var model = Model.parse(response.payload)
val success = local.addModel(model)
if (!success) {
emit(Error("Error caching the remote data"))
return@flow
}

// Returning data from a single source of truth
model = local.find(model.key)
emit(Success(model))
}
}

上述代码违反了单一责任原则。该函数不仅获取远程数据,还负责在本地存储数据。这应该被提取到不同的类中。

class Repository(
private val api: MyRemoteDatabase,
private val cache: MyCachingService /* Notice I changed the dependency */
) {
fun fetchRemoteData() = flow {
// Fetching API data
val response = api.getData()

val model = cache.save(response.payload)

// Sending back the data
model?.let {
emit(Success(it))
} ?: emit(Error("Error caching the remote data"))
}
}

// Shifted all caching logic to another class
class MyCachingService(
private val local: MyLocalDatabase
) {
suspend fun save(payload: Payload): Model? {
var model = Model.parse(payload)
val success = local.addModel(model)
return if (success)
local.find(model.key)
else
null
}
}

请注意 how 只负责将传入的有效负载保存到本地数据库中,而 repository 只负责获取发送上述模型的数据。这样做是一种很好的做法,因为有一种叫做关注点分离的东西,它可以提高调试和可测试性。MyCachingService

O — 打开/关闭

软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。

这个原则基本上意味着不要编写在将来更改时会破坏客户端代码的软件代码。假设您正在使用 Kotlin 构建 Web 开发 API。您已经设计了 ParagraphTag、AnchorTag 和 ImageTag。在您的代码中,系统会要求您比较两个元素的高度。

class ParagraphTag(
val width: Int,
val height: Int
)

class AnchorTag(
val width: Int,
val height: Int
)

class ImageTag(
val width: Int,
val height: Int
)

// Client-code
infix fun ParagraphTag.tallerThan(anchor: AnchorTag): Boolean {
return this.height > anchor.height
}

infix fun AnchorTag.tallerThan(anchor: ParagraphTag): Boolean {
return this.height > anchor.height
}

infix fun ParagraphTag.tallerThan(anchor: ImageTag): Boolean {
return this.height > anchor.height
}

// ... more functions

叹息!那是很多工作。现在,您有新的要求,要求您也包含 Heading 标签。您必须在客户端再添加 6 个函数。这不仅很乏味,而且还需要修改客户端代码以满足程序的需要。

相反,声明一个接口 —PageTag

interface PageTag {
val width: Int
val height: Int
}

class ParagraphTag(
override val width: Int,
override val height: Int
) : PageTag

class AnchorTag(
override val width: Int,
override val height: Int
) : PageTag

class ImageTag(
override val width: Int,
override val height: Int
) : PageTag


// Client Code
infix fun PageTag.tallerThan(other: PageTag): Boolean {
return this.height > other.height
}

现在,您已经关闭了客户端代码,以便进一步修改它。为了扩展您的功能,可以创建一个新类并实现 ,一切都会完美运行。PageTag

L — Liskov 换人

如果 S 是 T 的子类型,那么任何可由 T 证明的属性也必须由 S 证明。

哦。数学?嗯,这并不好。相比之下,这是一个很容易理解的原则。让我们考虑一个新例子。

open class Bird {
open fun fly() {
// ... performs code to fly
}

open fun eat() {
// ...
}
}

class Penguin : Bird() {
override fun fly() {
throw UnsupportedOperationException("Penguins cannot fly")
}
}

请注意上面的 class 不会抛出任何异常,而 class 会抛出。您不能在客户端代码中将 Penguin 替换为 Bird,而不会破坏或修改它。这违反了 Liskov 替代原则。扩展会破坏客户端代码,因此也违反了 open/closed 原则。BirdPenguinPenguinBird

解决此问题的一种方法是更改 design implementation。

open class FlightlessBird {
open fun eat() {
// ...
}
}

open class Bird : FlightlessBird() {
open fun fly() {
// ...
}
}

class Penguin : FlightlessBird() {
// ...
}

class Eagle : Bird() {
// ...
}

上面的代码解释了如果 a 可以吃,那么 的所有子类也可以吃。同样,如果 可以飞行,那么 的所有子类也必须飞行。FlightlessBirdFlightlessBirdBirdBird

I — 接口隔离

接口不应强制其 Client 端依赖它不使用的方法。

这个定义看起来并不可怕。实际上,这并不可怕。假设您正在建造一辆汽车、一架飞机和一辆自行车。由于它们都是车辆,因此您正在实现 Vehicle 接口。

interface Vehicle {
fun turnOn()
fun turnOff()
fun drive()
fun fly()
fun pedal()
}

class Car : Vehicle {
override fun turnOn() { /* Implementation */ }
override fun turnOff() { /* Implementation */ }
override fun drive() { /* Implementation */ }
override fun fly() = Unit
override fun pedal() = Unit
}

class Aeroplane : Vehicle {
override fun turnOn() { /* Implementation */ }
override fun turnOff() { /* Implementation */ }
override fun drive() = Unit
override fun fly() { /* Implementation */ }
override fun pedal() = Unit
}

class Bicycle : Vehicle {
override fun turnOn() = Unit
override fun turnOff() = Unit
override fun drive() = Unit
override fun fly() = Unit
override fun pedal() { /* Implementation */ }
}

呸!看到类是如何被迫实现它不需要的方法的吗?我也不能将类声明为 abstract。根据界面分离原则,我们应该有这个设计。

interface SystemRunnable {
fun turnOn()
fun turnOff()
}

interface Drivable() {
fun drive()
}

interface Flyable() {
fun fly()
}

interface Pedalable() {
fun pedal()
}

class Car : SystemRunnable, Drivable {
override fun turnOn() { /* Implementation */ }
override fun turnOff() { /* Implementation */ }
override fun drive() { /* Implementation */ }
}

class Aeroplane : SystemRunnable, Flyable {
override fun turnOn() { /* Implementation */ }
override fun turnOff() { /* Implementation */ }
override fun fly() { /* Implementation */ }
}

class Bicycle : Pedalable {
override fun pedal() { /* Implementation */ }
}

现在,这看起来更简洁了,并且也更容易通过它们的接口引用不同的功能。

D — 依赖关系反转

1. 高级模块不应依赖低级模块;两者都应该依赖于抽象。

2. 抽象不应依赖于细节。细节应该取决于抽象。

这到底意味着什么?高级模块是业务或 UI 看到的那些模块。低级模块是那些处理应用程序复杂性的模块。回想一下我在 Solid Responsibility Principle 中的例子:

class Repository(
private val api: MyRemoteDatabase,
private val cache: MyCachingService
) {
fun fetchRemoteData() = flow {
// Fetching API data
val response = api.getData()

val model = cache.save(response.payload)

// Sending back the data
model?.let {
emit(Success(it))
} ?: emit(Error("Error caching the remote data"))
}
}

class MyRemoteDatabase {
suspend fun getData(): Response { /* ... */ }
}

class MyCachingService(
private val local: MyLocalDatabase
) {
suspend fun save(): Model? { /* ... */ }
}

class MyLocalDatabase {
suspend fun add(model: Model): Boolean { /* ... */ }
suspend fun find(key: Model.Key): Model { /* ... */ }
}

它看起来还不错,而且会完美运行。但是,将来,如果我决定将我的本地数据库从 PostgreSql 更改为 MongoDB;或者,如果我决定完全更改我的缓存机制,则还必须更改整个实现细节和客户端代码。高级模块依赖于低级 concrete 模块。

这是不对的。相反,您必须将功能抽象为接口,并让具体实现对其进行扩展。

interface CachingService {
suspend fun save(): Model?
}

interface SomeLocalDb() {
suspend fun add(model: Model): Boolean
suspend fun find(key: Model.Key): Model
}

class Repository(
private val api: SomeRemoteDb,
private val cache: CachingService
) { /* Implementation */ }

class MyCachingService(
private val local: SomeLocalDb
) : CachingService { /* Implement methods */ }

class MyAltCachingService(
private val local: SomeLocalDb
) : CachingService { /* Implement methods */ }

class PostgreSQLLocalDb : SomeLocalDb { /* Implement methods */ }
class MongoLocalDb : SomeLocalDb { /* Implement methods */ }

通过更改一个单词,您可以轻松地在整个应用程序中存储库的不同实现之间进行更改。每次我听它时,这都会让我感到不寒而栗。

我花了相当长的时间来阐明这篇文章中的所有信息。我希望您喜欢阅读它并学到了一些东西。谢谢!

根据我作为本科一年级学生的经验,并听取了前辈的经验,学校和学院会教你如何编程,以及所需的数学,如离散数学和微积分。但是当你离开大学进入这个行业时,你必须了解一些概念和原则才能轻松过渡。我们将讨论 KISS、DRY 和 SOLID 原则。

图片[1]-他们在学校不教你的编程原则
照片由 伊万·阿列克西奇 on Unsplash

KISS 原则

保持简单,笨蛋!

通常,您会发现自己在一个团队中一起工作。一组开发人员,负责项目的不同方面。如果你要切换并处理其他人的代码,你是否希望看到没有任何注释或变量的凌乱代码?或者希望看到一个文档齐全的代码来很好地解释项目的这一部分?显然是后者。

如果你正在编写一个非常复杂的程序,并说你生病了。您在恢复一周后重新访问您的项目。当然,你会失去编程的流程,但你想来看看你不再理解的代码吗?或者看到一些你可以稍微理解的东西?同样,后者。

现在想象一下,你正在独自从事一个项目,而且你每天都在做这个项目。您将遇到 bug,您需要调试它们。如果您的代码比必须的更复杂,调试代码会有多容易?

以上所有情况都指向一件事 — 保持简单,愚蠢!

任何傻瓜都可以编写计算机可以理解的代码。优秀的程序员编写的是人类可以理解的代码。

归根结底,机器不会关心你是写了简单的代码还是复杂的代码来完成某个任务。相比之下,对于阅读并尝试理解代码的人类(包括您)来说,这绝对很重要。

但是我该如何KISS呢?(我知道听起来很奇怪)

考虑一个模型类 Student。这将存储 2 个项目和一个 key 和 value 都是对的 map。第一对由 2 个字符串组成:模块的 name 和 id。第二对是 2 个双倍、达到的分数和该模块的最高分数。

data class Student(
val name: String,
val age: Int,
val moduleMarks: Map<Pair<String, String>, Pair<Int, Int>>
)

在做出这样的设计选择后,您现在需要记录学生的姓名和他们得分超过 80% 的模块。

fun scholars(students: List<Student>): Map<String, List<String>> {
val scholars = mutableMapOf<String, List<String>>()
students.forEach { student ->
scholars[student.name] = student.moduleMarks
.filter { (_, (a, m)) -> a / m > 0.8}
.map { ((n, _), _) -> n }
}
return scholars
}

即使过了几天又回到这样的代码,那将是灾难性的。尽管您可以争辩说这是一个相当简单的示例,但有一些方法可以使其更简单。尽可能引入更多的抽象和变量。

data class Student(
val name: String,
val age: Int,
val moduleMarks: Map<Module, Mark>
)

data class Module(
val name: String,
val id: String
)

data class Mark(
val achieved: Double,
val maximum: Double
) {
fun isAbove(percentage: Double): Boolean {
return achieved / maximum * 100 > percentage
}

fun scholars(students: List<Student>): Map<String, List<String>> {
val scholars = mutableMapOf<String, List<String>>()
students.forEach { student ->
val modulesAbove80 = student.moduleMarks
.filter { (_, mark) -> mark.isAbove(80.0)}
.map { (module, _) -> module.name }

scholars[student.name] = modulesAbove80
}
return scholars
}

这会增加很多代码。但更重要的是,代码看起来更简洁,读起来像英语。

DRY 原理

不要重复自己

如果您发现您正在一遍又一遍地执行相同的代码,请创建一个函数并重复使用它。在我的大学作业中,我正在处理一组对象(单元格),定义的大多数(如果不是全部)函数都要求我从该集合中搜索和获取特定对象并对其进行操作。

public class Spreadsheet implements BasicSpreadsheet {
private final Set<Cell> cells;

@Override
public double getCellValue(CellLocation location) {
Cell cell = cells.stream()
.filter(cell -> cell.location.equals(location))
.findFirst()
.orElse(null);

return cell == null ? 0d : cell.getValue();
}

@Override
public String getCellExpression(CellLocation location) {
Cell cell = cells.stream()
.filter(cell -> cell.location.equals(location))
.findFirst()
.orElse(null);

return cell == null ? "" : cell.getExpression();
}

@Override
public void setCellExpression(CellLocation location, String input) throws InvalidSyntaxException {
Cell cell = cells.stream()
.filter(cell -> cell.location.equals(location))
.findFirst()
.orElse(null);

// ...
}

// ...
}

这是上面的一个大代码。但是当我键入此代码时,我发现自己在不同部分多次复制粘贴同一代码块。因此,我将它们抽象为我在任何地方重复使用的函数。

public class Spreadsheet implements BasicSpreadsheet {
private final Set<Cell> cells;

@Override
public double getCellValue(CellLocation location) {
return getFromCell(location, Cell::getValue, 0d);
}

@Override
public String getCellExpression(CellLocation location) {
return getFromCell(location, Cell::getExpression, "");
}

@Override
public void setCellExpression(CellLocation location, String input) throws InvalidSyntaxException {
Cell cell = findCell(location);

// ...
}

// ...

private Cell findCell(CellLocation location) {
return cells.stream()
.filter(cell -> cell.location.equals(location))
.findFirst()
.orElse(null);
}

private <T> T getFromCell(CellLocation location,
Function<Cell, T> function,
T defaultValue) {
Cell cell = findCell(location);
return cell == null ? defaultValue : function.apply(cell);
}
}

这样,如果我意识到我的代码中存在错误,我就不必在 个不同的地方更改代码。在函数内部更改一次就足以修复所有错误。

SOLID 原则

这不是一个单一的原则,而是软件开发中至关重要的 5 个原则。

S — 单一职责

一个类应该有且只有一个更改的理由。

可能是最容易理解的原理。您定义的每个类/函数只能执行一个任务。假设您正在构建一个网络应用程序。

class Repository(
private val api: MyRemoteDatabase,
private val local: MyLocalDatabase
) {
fun fetchRemoteData() = flow {
// Fetching API data
val response = api.getData()

// Saving data in the cache
var model = Model.parse(response.payload)
val success = local.addModel(model)
if (!success) {
emit(Error("Error caching the remote data"))
return@flow
}

// Returning data from a single source of truth
model = local.find(model.key)
emit(Success(model))
}
}

上述代码违反了单一责任原则。该函数不仅获取远程数据,还负责在本地存储数据。这应该被提取到不同的类中。

class Repository(
private val api: MyRemoteDatabase,
private val cache: MyCachingService /* Notice I changed the dependency */
) {
fun fetchRemoteData() = flow {
// Fetching API data
val response = api.getData()

val model = cache.save(response.payload)

// Sending back the data
model?.let {
emit(Success(it))
} ?: emit(Error("Error caching the remote data"))
}
}

// Shifted all caching logic to another class
class MyCachingService(
private val local: MyLocalDatabase
) {
suspend fun save(payload: Payload): Model? {
var model = Model.parse(payload)
val success = local.addModel(model)
return if (success)
local.find(model.key)
else
null
}
}

请注意 how 只负责将传入的有效负载保存到本地数据库中,而 repository 只负责获取发送上述模型的数据。这样做是一种很好的做法,因为有一种叫做关注点分离的东西,它可以提高调试和可测试性。MyCachingService

O — 打开/关闭

软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。

这个原则基本上意味着不要编写在将来更改时会破坏客户端代码的软件代码。假设您正在使用 Kotlin 构建 Web 开发 API。您已经设计了 ParagraphTag、AnchorTag 和 ImageTag。在您的代码中,系统会要求您比较两个元素的高度。

class ParagraphTag(
val width: Int,
val height: Int
)

class AnchorTag(
val width: Int,
val height: Int
)

class ImageTag(
val width: Int,
val height: Int
)

// Client-code
infix fun ParagraphTag.tallerThan(anchor: AnchorTag): Boolean {
return this.height > anchor.height
}

infix fun AnchorTag.tallerThan(anchor: ParagraphTag): Boolean {
return this.height > anchor.height
}

infix fun ParagraphTag.tallerThan(anchor: ImageTag): Boolean {
return this.height > anchor.height
}

// ... more functions

叹息!那是很多工作。现在,您有新的要求,要求您也包含 Heading 标签。您必须在客户端再添加 6 个函数。这不仅很乏味,而且还需要修改客户端代码以满足程序的需要。

相反,声明一个接口 —PageTag

interface PageTag {
val width: Int
val height: Int
}

class ParagraphTag(
override val width: Int,
override val height: Int
) : PageTag

class AnchorTag(
override val width: Int,
override val height: Int
) : PageTag

class ImageTag(
override val width: Int,
override val height: Int
) : PageTag


// Client Code
infix fun PageTag.tallerThan(other: PageTag): Boolean {
return this.height > other.height
}

现在,您已经关闭了客户端代码,以便进一步修改它。为了扩展您的功能,可以创建一个新类并实现 ,一切都会完美运行。PageTag

L — Liskov 换人

如果 S 是 T 的子类型,那么任何可由 T 证明的属性也必须由 S 证明。

哦。数学?嗯,这并不好。相比之下,这是一个很容易理解的原则。让我们考虑一个新例子。

open class Bird {
open fun fly() {
// ... performs code to fly
}

open fun eat() {
// ...
}
}

class Penguin : Bird() {
override fun fly() {
throw UnsupportedOperationException("Penguins cannot fly")
}
}

请注意上面的 class 不会抛出任何异常,而 class 会抛出。您不能在客户端代码中将 Penguin 替换为 Bird,而不会破坏或修改它。这违反了 Liskov 替代原则。扩展会破坏客户端代码,因此也违反了 open/closed 原则。BirdPenguinPenguinBird

解决此问题的一种方法是更改 design implementation。

open class FlightlessBird {
open fun eat() {
// ...
}
}

open class Bird : FlightlessBird() {
open fun fly() {
// ...
}
}

class Penguin : FlightlessBird() {
// ...
}

class Eagle : Bird() {
// ...
}

上面的代码解释了如果 a 可以吃,那么 的所有子类也可以吃。同样,如果 可以飞行,那么 的所有子类也必须飞行。FlightlessBirdFlightlessBirdBirdBird

I — 接口隔离

接口不应强制其 Client 端依赖它不使用的方法。

这个定义看起来并不可怕。实际上,这并不可怕。假设您正在建造一辆汽车、一架飞机和一辆自行车。由于它们都是车辆,因此您正在实现 Vehicle 接口。

interface Vehicle {
fun turnOn()
fun turnOff()
fun drive()
fun fly()
fun pedal()
}

class Car : Vehicle {
override fun turnOn() { /* Implementation */ }
override fun turnOff() { /* Implementation */ }
override fun drive() { /* Implementation */ }
override fun fly() = Unit
override fun pedal() = Unit
}

class Aeroplane : Vehicle {
override fun turnOn() { /* Implementation */ }
override fun turnOff() { /* Implementation */ }
override fun drive() = Unit
override fun fly() { /* Implementation */ }
override fun pedal() = Unit
}

class Bicycle : Vehicle {
override fun turnOn() = Unit
override fun turnOff() = Unit
override fun drive() = Unit
override fun fly() = Unit
override fun pedal() { /* Implementation */ }
}

呸!看到类是如何被迫实现它不需要的方法的吗?我也不能将类声明为 abstract。根据界面分离原则,我们应该有这个设计。

interface SystemRunnable {
fun turnOn()
fun turnOff()
}

interface Drivable() {
fun drive()
}

interface Flyable() {
fun fly()
}

interface Pedalable() {
fun pedal()
}

class Car : SystemRunnable, Drivable {
override fun turnOn() { /* Implementation */ }
override fun turnOff() { /* Implementation */ }
override fun drive() { /* Implementation */ }
}

class Aeroplane : SystemRunnable, Flyable {
override fun turnOn() { /* Implementation */ }
override fun turnOff() { /* Implementation */ }
override fun fly() { /* Implementation */ }
}

class Bicycle : Pedalable {
override fun pedal() { /* Implementation */ }
}

现在,这看起来更简洁了,并且也更容易通过它们的接口引用不同的功能。

D — 依赖关系反转

1. 高级模块不应依赖低级模块;两者都应该依赖于抽象。

2. 抽象不应依赖于细节。细节应该取决于抽象。

这到底意味着什么?高级模块是业务或 UI 看到的那些模块。低级模块是那些处理应用程序复杂性的模块。回想一下我在 Solid Responsibility Principle 中的例子:

class Repository(
private val api: MyRemoteDatabase,
private val cache: MyCachingService
) {
fun fetchRemoteData() = flow {
// Fetching API data
val response = api.getData()

val model = cache.save(response.payload)

// Sending back the data
model?.let {
emit(Success(it))
} ?: emit(Error("Error caching the remote data"))
}
}

class MyRemoteDatabase {
suspend fun getData(): Response { /* ... */ }
}

class MyCachingService(
private val local: MyLocalDatabase
) {
suspend fun save(): Model? { /* ... */ }
}

class MyLocalDatabase {
suspend fun add(model: Model): Boolean { /* ... */ }
suspend fun find(key: Model.Key): Model { /* ... */ }
}

它看起来还不错,而且会完美运行。但是,将来,如果我决定将我的本地数据库从 PostgreSql 更改为 MongoDB;或者,如果我决定完全更改我的缓存机制,则还必须更改整个实现细节和客户端代码。高级模块依赖于低级 concrete 模块。

这是不对的。相反,您必须将功能抽象为接口,并让具体实现对其进行扩展。

interface CachingService {
suspend fun save(): Model?
}

interface SomeLocalDb() {
suspend fun add(model: Model): Boolean
suspend fun find(key: Model.Key): Model
}

class Repository(
private val api: SomeRemoteDb,
private val cache: CachingService
) { /* Implementation */ }

class MyCachingService(
private val local: SomeLocalDb
) : CachingService { /* Implement methods */ }

class MyAltCachingService(
private val local: SomeLocalDb
) : CachingService { /* Implement methods */ }

class PostgreSQLLocalDb : SomeLocalDb { /* Implement methods */ }
class MongoLocalDb : SomeLocalDb { /* Implement methods */ }

通过更改一个单词,您可以轻松地在整个应用程序中存储库的不同实现之间进行更改。每次我听它时,这都会让我感到不寒而栗。

我花了相当长的时间来阐明这篇文章中的所有信息。我希望您喜欢阅读它并学到了一些东西。谢谢!

© 版权声明
THE END
喜欢就支持一下吧
点赞0赞赏 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容