DSL ,领域专用语言, Domain Specific Language
一门编程语言,图灵完备,功能有,性能也有。譬如 Swift
DSL 基于一门语言,专门解决某一个问题。适合声明式,规则明确的场景
该问题上,语法简练,处理方便。譬如 SnapKit
DSL,写起来简练,提升开发效率。建立上下文 domain,隐藏大量的实现细节。这样代码少,不冗长。一般,代码的编译时间会增加
Swift 有类型推导功能 type refer、协议化编程 POP、操作符重载等优势,开发其 DSL 比较方便。
命名空间,放在了最后
本文以视图布局 layout 为例子:
原生布局,使用 LayoutAnchor
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// label 的顶部,距离 button 的底部, 20 pt
label.topAnchor.constraint(
equalTo: button.bottomAnchor,
constant: 20
),
// label 的左边,对齐 button 的左边
label.leadingAnchor.constraint(
equalTo: button.leadingAnchor
),
// label 的宽度,不超过 button 的宽度 - 40 pt
label.widthAnchor.constraint(
lessThanOrEqualTo: view.widthAnchor,
constant: -40
)
])
使用本文造的 DSL 后, 布局代码少了很多,符号更加直观
// put , 有放置的意思
label.put.layout {
$0.top == button.put.bottom + 20
$0.leading == button.put.leading
$0.width <= view.put.width - 40
}
第一步,封装原生的布局功能, LayoutAnchor
需要建立功能协议 LayoutAnchor
, 把 iOS 系统有 6 个布局方法,抽离合并成 3 个。
NSLayoutAnchor 是一个泛型类。每一个具体的约束锚点,搭配具体的 NSLayoutAnchor 类,自带相关的协议。实现细节比较复杂。
建立功能协议 LayoutAnchor
,把繁琐的细节,屏蔽掉
protocol LayoutAnchor {
func constraint(equalTo anchor: Self,
constant: CGFloat) -> NSLayoutConstraint
func constraint(greaterThanOrEqualTo anchor: Self,
constant: CGFloat) -> NSLayoutConstraint
func constraint(lessThanOrEqualTo anchor: Self,
constant: CGFloat) -> NSLayoutConstraint
}
extension NSLayoutAnchor: LayoutAnchor {}
建立一个上层类 LayoutProxy
, 在原生布局方法上,包裹一层。这样调用语法少一点
先拿到属性,
class LayoutProxy {
lazy var leading = property(with: view.leadingAnchor)
lazy var trailing = property(with: view.trailingAnchor)
lazy var top = property(with: view.topAnchor)
lazy var bottom = property(with: view.bottomAnchor)
lazy var width = property(with: view.widthAnchor)
lazy var height = property(with: view.heightAnchor)
private let view: UIView
fileprivate init(view: UIView) {
self.view = view
}
private func property<A: LayoutAnchor>(with anchor: A) -> LayoutProperty<A> {
return LayoutProperty(anchor: anchor)
}
}
再调用布局方法
封装一层,把原生的方法名,给改了
增加一个结构体 LayoutProperty
, 他包了个遵守 LayoutAnchor 的属性 anchor. 这样可以不用直接操作 NSLayoutAnchor ,直接给 NSLayoutAnchor 增加方法,优雅一些
struct LayoutProperty<Anchor: LayoutAnchor> {
fileprivate let anchor: Anchor
}
extension LayoutProperty {
func equal(to otherAnchor: Anchor, offsetBy constant: CGFloat = 0) {
anchor.constraint(equalTo: otherAnchor,
constant: constant).isActive = true
}
func greaterThanOrEqual(to otherAnchor: Anchor,
offsetBy constant: CGFloat = 0) {
anchor.constraint(greaterThanOrEqualTo: otherAnchor,
constant: constant).isActive = true
}
func lessThanOrEqual(to otherAnchor: Anchor,
offsetBy constant: CGFloat = 0) {
anchor.constraint(lessThanOrEqualTo: otherAnchor,
constant: constant).isActive = true
}
}
第一步后的效果:
调用语法,略微精炼
label.translatesAutoresizingMaskIntoConstraints = false
let proxy = LayoutProxy(view: label)
proxy.top.equal(to: button.bottomAnchor, offsetBy: 20)
proxy.leading.equal(to: button.leadingAnchor)
proxy.width.lessThanOrEqual(to: view.widthAnchor, offsetBy: -40)
第二步: 采用闭包,建立布局上下文环境, 封装布局调用的代码
上下文环境, 说明了这里是干什么的。方便理解
手动建立布局对象,let proxy = LayoutProxy(view: label)
,再具体布局
薄板代码 boiler plate,还是多了一些。每次都要重复这个套路,不怎么优雅。
采用 Swift 的闭包 closure,建立执行上下文环境,更加 DSL 一些
上下文环境,譬如 SnapKit
.
看见 .snp{}
, 就知道这里面是干什么的。在这里,只会布局相关,不会干其他
给 UIView
添加扩展方法,配置 UIView
后,执行 LayoutProxy
的闭包
extension UIView {
func layout(using closure: (LayoutProxy) -> Void) {
translatesAutoresizingMaskIntoConstraints = false
closure(LayoutProxy(view: self))
}
}
第 2 步后的效果:比较 DSL 了
看起来像动画调用 UIView.animate
label.layout {
$0.top.equal(to: button.bottomAnchor, offsetBy: 20)
$0.leading.equal(to: button.leadingAnchor)
$0.width.lessThanOrEqual(to: view.widthAnchor, offsetBy: -40)
}
第 3 步: 操作符重载,进一步简化语法
将第 2 步的调用方法,用操作符号替换
加和减,把约束和偏移,结合成元组 tuple
// 加
func +<A: LayoutAnchor>(lhs: A, rhs: CGFloat) -> (A, CGFloat) {
return (lhs, rhs)
}
// 减
func -<A: LayoutAnchor>(lhs: A, rhs: CGFloat) -> (A, CGFloat) {
return (lhs, -rhs)
}
约束生效的三种情况 X 要不要偏移
3 种情况 X 2 种条件
// 等于, 使用 == ,当作 =
// 右边参数,含偏移
func ==<A: LayoutAnchor>(lhs: LayoutProperty<A>,
rhs: (A, CGFloat)) {
lhs.equal(to: rhs.0, offsetBy: rhs.1)
}
// 等于, 使用 == ,当作 =
func ==<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: A) {
lhs.equal(to: rhs)
}
// 不小于,
// 右边参数,含偏移
func >=<A: LayoutAnchor>(lhs: LayoutProperty<A>,
rhs: (A, CGFloat)) {
lhs.greaterThanOrEqual(to: rhs.0, offsetBy: rhs.1)
}
// 不小于
func >=<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: A) {
lhs.greaterThanOrEqual(to: rhs)
}
// 不大于,
// 右边参数,含偏移
func <=<A: LayoutAnchor>(lhs: LayoutProperty<A>,
rhs: (A, CGFloat)) {
lhs.lessThanOrEqual(to: rhs.0, offsetBy: rhs.1)
}
// 不大于
func <=<A: LayoutAnchor>(lhs: LayoutProperty<A>, rhs: A) {
lhs.lessThanOrEqual(to: rhs)
}
第 3 步后的效果: DSL 了
label.layout {
$0.top == button.bottomAnchor + 20
$0.leading == button.leadingAnchor
$0.width <= view.widthAnchor - 40
}
第 4 步: 增加命名空间
命名空间,看起来很高很大,实际上就封装了一层
命名空间可以长这个样子,NamespaceWrapper(val: view)
封装结构体,
public protocol TypeWrapper{
associatedtype WrappedType
var wrapped: WrappedType { get }
init(val: WrappedType)
}
public struct NamespaceWrapper<T>: TypeWrapper{
public let wrapped: T
public init(val: T) {
self.wrapped = val
}
}
给结构体添加功能
extension TypeWrapper where WrappedType: UIView {
func layout(using closure: (LayoutProxy) -> Void) {
wrapped.translatesAutoresizingMaskIntoConstraints = false
closure(LayoutProxy(view: wrapped))
}
var bottom: NSLayoutYAxisAnchor{
wrapped.bottomAnchor
}
var leading: NSLayoutXAxisAnchor{
wrapped.leadingAnchor
}
var width: NSLayoutDimension{
wrapped.widthAnchor
}
var centerX: NSLayoutXAxisAnchor{
wrapped.centerXAnchor
}
var centerY: NSLayoutYAxisAnchor{
wrapped.centerYAnchor
}
}
调用效果长这样,平常见不到的
NamespaceWrapper(val: label).layout {
$0.top == NamespaceWrapper(val: button).bottom + 20
// ...
}
把 NamespaceWrapper(val: view)
变成我们常见的 view.put
( 视图布局有放置的含义,这里用 put )
弄一胶水协议 NamespaceWrap
完成这个转换,UIView 遵守这个协议。
public protocol NamespaceWrap{
associatedtype WrapperType
var put: WrapperType { get }
}
public extension NamespaceWrap{
var put: NamespaceWrapper<Self> {
return NamespaceWrapper(val: self)
}
}
extension UIView: NamespaceWrap{ }
第 4 步后的效果: DSL
label.put.layout {
$0.top == button.put.bottom + 20
$0.leading == button.put.leading
$0.width <= view.put.width - 40
}