决定是否一般化一段代码以适应多个用例有时候会非常棘手。调整函数或类型以在代码库的多个部分中使用可能是避免代码重复的好方法,使得过于通用的东西通常会导致难以理解和维护的代码 - 因为它最终需要做太多了。
本周,我们来看看几个关键因素,它们可以帮助我们在尽可能多地重用代码之间取得良好的平衡,同时避免在过程中使事情过于复杂或模糊。
小编这里有大量的书籍和面试资料哦(点击下载)

从具体实现开始
通常,避免过度泛化代码的一个好方法是构建一个初始版本,其中考虑了一个非常具体的特定用例。通常可以更容易地使新代码完成一件事,而不是专注于优化它以便立即重用 - 只要我们确保分离关注点并设计清晰的API,我们就可以随时重构代码一旦需要可重复使用。
假设我们正在研究某种形式的电子商务应用程序,并且我们已经构建了一个类来让我们Product
根据其标识符加载- 看起来像这样:
class ProductLoader {
typealias Handler = (Result<Product, Error>) -> Void
private let networking: Networking
private var cache = [UUID : Product]()
init(networking: Networking) {
self.networking = networking
}
func loadProduct(withID id: UUID,
then handler: @escaping Handler) {
// If a cached product exists, then return it directly instead
// of performing a network request.
if let product = cache[id] {
return handler(.success(product))
}
// Load the product over the network, by requesting the
// product endpoint with the given ID.
networking.request(.product(id: id)) { [weak self] result in
self?.handle(result, using: handler)
}
}
}
查看上面的代码示例,我们可以看到我们的主要职责ProductLoader
是检查所请求的产品是否已被缓存,如果没有,则启动网络请求以加载它。一旦收到响应,它就会将其结果解码为Product
模型,然后使用如下所示的私有handle
方法对其进行缓存:
private extension ProductLoader {
func handle(_ result: Result<Data, Error>,
using handler: Handler) {
do {
let product = try JSONDecoder().decode(
Product.self,
from: result.get()
)
cache[product.id] = product
handler(.success(product))
} catch {
handler(.failure(error))
}
}
}
虽然上面的课程目前是完全Product
特定的 - 但它所做的工作并不是产品所独有的。事实上,我们ProductLoader
需要能够做到的只有三件事:
- 检查给定的缓存条目是否存在。
- 请求注入的
Networking
实例请求端点。 - 将网络响应数据解码为模型。
看着上面的列表中,没有什么,在什么站出来,我们就会只需要为产品做-我们实际上需要执行完全相同的一组任务加载任何我们的应用程序中的模型-诸如用户,广告商,销售商, 等等。因此,让我们ProductLoader
通过启用相同的代码来加载任何模型来研究我们如何推广。
概括核心逻辑
ProductLoader
除了我们在代码库的多个部分中需要完全相同的逻辑这一事实之外,是什么使我们这样一个很好的推广候选者,它的实现只包含非常通用的任务 - 例如缓存,网络和JSON解码。这应该让我们保持或多或少相同的实现,同时仍然打开我们的API以更多的用例。
让我们首先将我们的产品加载器重命名为ModelLoader
,并使其成为可以使用任何Model
符合的类型的通用Decodable
。我们将保留相同的属性和初始化程序,除了我们现在还需要一个函数生成一个Endpoint
作为初始化程序的一部分注入的事实- 因为不同的模型可能从不同的服务器端点加载:
class ModelLoader<Model: Decodable> {
typealias Handler = (Result<Model, Error>) -> Void
private let networking: Networking
private let endpoint: (UUID) -> Endpoint
private var cache = [UUID : Model]()
init(networking: Networking,
endpoint: @escaping (UUID) -> Endpoint) {
self.networking = networking
self.endpoint = endpoint
}
}
当涉及到我们的主加载方法时,我们将其重命名为loadModel
,并使其在执行网络请求时使用注入的endpoint
函数来产生一个Endpoint
调用 - 如下所示:
extension ModelLoader {
func loadModel(withID id: UUID,
then handler: @escaping Handler) {
if let model = cache[id] {
return handler(.success(model))
}
networking.request(endpoint(id)) { [weak self] result in
self?.handle(result, using: handler, modelID: id)
}
}
}
最后,我们将更新我们的私有handle
方法来解码其泛型Model
类型的实例,而不仅仅是Product
值。由于我们不再依赖解码产品的ID进行缓存,因此我们还必须从顶级loadModel
方法中传递所请求模型的ID :
private extension ModelLoader {
func handle(_ result: Result<Data, Error>,
using handler: Handler,
modelID: UUID) {
do {
let model = try JSONDecoder().decode(
Model.self,
from: result.get()
)
cache[modelID] = model
handler(.success(model))
} catch {
handler(.failure(error))
}
}
}
有了上述内容,我们现在已经成功地将我们的上ProductLoader
一个扩展为可用于加载任何可解码模型的泛型类型 - 所有这些都不会大幅改变其实现或API。关于调用站点的唯一区别是,我们现在将调用loadModel
而不是loadProduct
,并且我们还需要Endpoint
在初始化加载器实例时传递生成函数:
let productLoader = ModelLoader<Product>(
networking: networking,
endpoint: Endpoint.product
)
let userLoader = ModelLoader<User>(
networking: networking,
endpoint: Endpoint.user
)
由于我们的endpoint
参数需要一个Endpoint
为给定ID 生成一个函数的函数,因此我们将两个端点product
和user
端点作为上面的第一类函数传递- 无论是使用枚举描述我们的服务器端点,还是使用静态工厂,这都很有效方法,例如“在Swift中构造URL”。
特定领域的便利
通用代码使其可以用于多个不同的模型 - 或者换句话说,在多个域中 - 可以是减少代码重复并使系统架构更加一致的好方法。然而,这样做也可能使得更难以弄清楚给定类型如何适应更大的图景。
当使用一个被调用的类型时ProductLoader
,它很明显它的作用以及它所属的代码库的哪一部分 - 虽然ModelLoader
听起来更加含糊不清。但是,有几种方法可以缓解这个问题。一种方法是使用类型别名来恢复我们的特定于模型的类型名称,而不必实际维护重复的实现:
typealias ProductLoader = ModelLoader<Product>
typealias UserLoader = ModelLoader<User>
我们可以调整ModelLoader
以使其感觉与任何给定模型更加连接的另一种方法是创建特定于域的便利API - 例如,通过让我们跳过端点参数,而不是使用便利初始化器来内联它:
// Note how we can extend our type alias directly, which is
// equivalent to extending ModelLoader where Model == Product.
extension ProductLoader {
convenience init(networking: Networking) {
self.init(networking: networking,
endpoint: Endpoint.product)
}
}
做上面这样的事情似乎是不必要的,但它会对我们的新ModelLoader
用户的使用有多大影响- 特别是如果我们在整个代码库中创建它的多个实例。它也可以是一种很好的方式,使它向后兼容我们的旧版本ProductLoader
,因为如果我们添加便利,使我们的新API完全匹配我们的旧API,则不需要更新调用站点。
共享抽象的力量
泛化代码的好处不仅限于减少代码重复 - 概括核心逻辑集也可以是创建共同基础的好方法,在此基础上我们可以构建强大的共享抽象。
例如,假设我们继续在我们的应用程序上进行迭代,并且在某些时候我们发现自己需要在几个不同的地方一次性加载多个模型。不必为每种模型编写重复的逻辑,因为我们现在拥有通用的ModelLoader
,我们可以简单地扩展它以添加我们需要的API - 这允许我们加载任何类型的模型的数组,给定任何ID序列:
extension ModelLoader {
typealias MultiHandler = (Result<[Model], Error>) -> Void
// We let any sequence be passed here, since some parts of
// our code base might be storing IDs using an Array, while
// others might be using a Dictionary, or a Set.
func loadModels<S: Sequence>(
withIDs ids: S,
then handler: @escaping MultiHandler
) where S.Element == UUID {
var iterator = ids.makeIterator()
var models = [Model]()
func loadNext() {
guard let nextID = iterator.next() else {
return handler(.success(models))
}
loadModel(withID: nextID) { result in
do {
try models.append(result.get())
loadNext()
} catch {
handler(.failure(error))
}
}
}
loadNext()
}
}
请注意上面的示例如何不是一次加载多个模型的最有效方法 - 因为它是完全顺序的。有关并行执行一组任务的更全面实现 - 请参阅Swift中基于任务的并发。
就像我们之前在我们的通用核心API之上添加特定于域的便利一样,我们可以做同样的事情来包装上面的loadModels
方法来创建它的特定于模型的版本 - 比如这个,它允许我们加载所有的产品在给定的包中,然后对它们应用折扣:
extension ModelLoader where Model == Product {
func loadProducts(in bundle: Product.Bundle,
then handler: @escaping MultiHandler) {
loadModels(withIDs: bundle.productIDs) { result in
do {
let products = try result.get().map {
$0.applying(bundle.discount)
}
handler(.success(products))
} catch {
handler(.failure(error))
}
}
}
}
使用上面的设置 - 底部的通用核心逻辑和顶部的特定于域的API - 可以是在代码重用和保持顶级API之间实现“两全其美”的好方法。他们可能会。
我们仍然需要特定于域的类型
但是,并非所有代码都应该被推广 - 即使我们可以使用类型别名和扩展来扩充我们的泛型类型,有时我们只需要一个好的旧式特定于域的API。当一个类型执行的任务在给定的域中真正有意义时,最好是硬连接它以很好地执行该单个任务,而不是过度抽象。
下面是这种类型的一个例子,一个处理购买的控制器类 - 目前,这是我们只需要在产品领域内执行的逻辑:
class ProductPurchasingController {
typealias Handler = (Result<Void, Error>) -> Void
private let loader: ProductLoader
private let paymentController: PaymentController
init(loader: ProductLoader,
paymentController: PaymentController) {
self.loader = loader
self.paymentController = paymentController
}
func purchaseProduct(with id: UUID,
then handler: @escaping Handler) {
loader.loadModel(withID: id) { result in
// Perform purchase
...
}
}
}
请注意以上内容如何通过类型别名ProductPurchasingController
使用我们的新ModelLoader
API ProductLoader
。除了它调用的事实loadModel
,而不是loadProduct
真的没有告诉它实际上使用完全通用的类型 - 它适合在我们目前正在工作的域内的家中。
结论
在处理逻辑实际上并未与任何特定域相关联的类型或函数时 - 将这些逻辑概括为可在多个不同场景中重用可能是统一代码库核心,避免代码重复的一种很好的方法,并且使强大的共享抽象能够构建在该逻辑之上。
然而,虽然尝试概括我们所有的低级逻辑可能很诱人,但有时这样做可能会造成不必要的并发症而没有任何实际好处。关键在于找到足够通用的逻辑,使其适用于泛化,并且可以应用多个具体的用例。
扫码进交流群 有技术的来闲聊 没技术的来学习

网友评论