热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:iOS书写高质量代码之耦合的处理

篇首语:本文由编程笔记#小编为大家整理,主要介绍了iOS书写高质量代码之耦合的处理相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了iOS书写高质量代码之耦合的处理相关的知识,希望对你有一定的参考价值。



 



耦合是每个程序员都必须面对的话题,也是容易被忽视的存在,怎么处理耦合关系到我们最后的代码质量。今天Peak君和大家聊聊耦合这个基本功话题,一起捋一捋ios代码中处理耦合的种种方式及差异。


简化场景

耦合的话题可大可小,但原理都是相通的。为了方便讨论,我们先将场景进行抽象和简化,只讨论两个类之间的耦合。

假设我们有个类Person,需要喝水,根据职责划分,我们需要另一个类Cup来完成喝水的动作,代码如下:


//Person.h
@interface Person : NSObject
- (void)drink;
@end
//Cup.h
@interface Cup : NSObject
- (id)provideWater;
@end

 

很明显,Person和Cup之间要配合完成喝水的动作,是无论如何都会产生耦合的,我们来看看在Objective C下都有哪些耦合的方式,以及不同耦合方式对以后代码质量变化的影响。


方式一:.m引用

这种方式直接在.m文件中导入Cup.h,同时生成临时的Cup对象来调用Cup中的方法。代码如下:


#import "Person.h"
#import "Cup.h"
@implementation Person
- (void)drink {
Cup
* c = [Cup new];
id water = [c provideWater];
[self sip:water];
}
- (void)sip:(id)water {
//sip water
}
@end

 

这应该是不少同学会选择的做法,要用到某个类的功能,就import该类,再调用方法,功能完成提交测试一气呵成。

这种方式初看起来没什么毛病,但有个弊端:Person与Cup的耦合被埋进了Person.m文件的方法实现中,而.m文件一般都是业务逻辑代码的重灾区,当Person.m的代码量膨胀之后,如果Person类交由另一位工程师来维护,那这位新接手的同学无法从Person.h中一眼看出Person类和哪些类之间有交互,即使在Person.m中看drink的声明也没有任何线索,要理清楚的话,只能把Person.m文件从头到尾读一遍,对团队效率的影响可想而知。


方式二:.h Property

既然直接在.m中引用会导致耦合不清晰,我们可以将耦合的部分放入Property中,代码如下:


//Person.h
@interface Person : NSObject
@property (nonatomic, strong) Cup
* cup;
- (void)drink;
@end
//Person.m
@implementation Person
- (void)drink {
id water = [self.cup provideWater];
[self sip:water];
}
- (void)sip:(id)water
{
//sip water
}
@end

 

这样,我们只需要扫一眼Person.h就能明白,Person类对哪些类产生了依赖,比直接在.m中引用清晰多了。

不知道大家有没有好奇过,为什么在Objective C中会有.h文件的存在,为什么不像Java,Swift一样一个文件代表一个类?使用.h文件有利有弊。

.h文件最大的意义在于将声明实现相隔离。声明是告诉外部我支持哪些功能,实现是支撑这些功能背后的代码逻辑。在我们阅读一个类的.h文件的时候,它最主要的作用是透露两个信息:



  • 我(Person类)依赖了哪些外部元素



  • 我(Person类)提供哪些接口供外部调用



所以.h文件应该是我们代码耦合的关键所在,当我们犹豫一个类的Property要不要放到.h文件中去声明时,要思考这个Property是不是必须暴露给外部。一旦暴露到.h文件中,就增加了依赖和耦合的几率。有时候Review代码,只要看.h文件是否清晰,就大概能猜测这个类设计者的水平。

当我们把Cup类做为Person的Property声明时,就表明Person与Cup之间存在必要的依赖,我们把这种依赖放到头文件中来,起到一目了然的效果。这比方式一清晰了不少,但有另一个问题,Cup暴露出去以后,外部元素可以随意修改,当内部执行drink的时候,可能另一个线程将cup置空了,影响正常的业务流程。


方式三:.h ReadOnly Property

方式二中,Person类在对Cup产生依赖的同时,也承担了cup随时被外部修改的风险。当然做直观的做法是将Cup类作为ReadOnly的property,同时提供一个对外的setter:


//Person.h
@interface Person : NSObject
@property (nonatomic, strong,
readonly) Cup* cup;
- (void)setPersonCup:(Cup*)cup;
- (void)drink;
@end

有同学可能会问,这和上面的做法有什么区别,不一样都有读写的接口吗?最大的区别是增加了检查和干扰的入口。

当我Debug的时候,经常需要检查某个Propery到底是被谁修改了,Setter中设置一个断点调试起来方便不少。同时,我们还可以使用Xcode的Caller机制,查看当前Setter都被那些外部类调用了,分析类与类之间的关联是很有帮助。

Person.m中Setter方法还提供了我们拓展功能的入口,比如我们需要在Setter中增加多线程同步Lock,当Person.m中的其他方法在使用Cup时,Setter必须等待完成才能执行。又比如我们可以在Setter中实现Copy On Write机制:


//Person.m
- (void)setPersonCup:(Cup*)cup {
Cup
* anotherCup = [cup copy];
_cup
= anotherCup;
}

这样,Person类就可以避免和外部类共享同一个Cup,杜绝使用同一个水杯的卫生问题 ;)

总之,单独的Setter方法让我们对代码有更大的掌控能力,也为后续接手维护你代码的同学带来了方便,利己利人。


方式四:init 注入

使用带Setter的Property虽然看上去好了不少,但Setter方法可以被任意外部类随时随刻调用,对于Person.m中使用Cup的方法来说,多少有些不安心,万一用着用着被别人改了呢?

为了避免被随意修改,我们可以采用init注入的方式,Objective C中的designated initializer正是为此而生:


//Person.h
@interface Person : NSObject
- (instancetype)initWithCup:(Cup*)cup;
- (void)drink;
@end

去掉Property,将Cup的设置放入init方法中,这样Person类对外就只提供一次机会来设置Cup,init之后,外部类就没有其他机会来修改Cup了。

这是使用最多,也是比较推荐的方式。只在对象被创建的时候,去建立与其他对象的关系,把可变性降低到一定程度。那这种方式是否也有什么缺点呢?

通过init的方式设置cup,杜绝了外部因素的影响,但如果内部持有了cup对象,那么内部的函数调用依然可以通过各种姿势与Cup类产生耦合,比如:


//Person.m
@interface Person ()
@property (nonatomic, strong) Cup
* myCup;
@end
@implementation Person
- (instancetype)initWithCup:(Cup*)cup {
self
= [super init];
if (self) {
self.myCup
= cup;
}
return self;
}
- (void)drinkWater {
id water = [self.myCup provideWater];
[self sip:water];
}
- (void)drinkMilk {
id milk = [self.myCup provideMilk];
[self sip:milk];
}
@end

Person内部的方法可以通过Cup所有对外的接口来产生耦合,此时我们对于两个类之间的耦合,就主要靠对Cup.h头文件来解读了。如果Cup类设计合理,头文件结构清晰的话,这其实不算太糟糕的场景。那还有没有其他方式呢?


方式五:parameter 注入

用Property持有的方式,在Person对象的整个生命周期内,耦合的可能性一直存在,原因在于Property对于.m文件来说是全局可见的。我们可以用另一种方式让耦合只发生在单个方法内部,即parameter injection:


//Person.h
@interface Person : NSObject
- (void)drink:(Cup*)cup;
@end
//Person.m
- (void)drink:(Cup*)cup {
id water = [cup provideWater];
[self sip:water];
}

这种方式的好处在于:Person和Cup的耦合只发生在drink函数的内部,一旦函数调用结束,Person和Cup之间就结束了依赖关系。从时间和空间的跨度上来说,这种方式比持有Property风险更小。

可要是在Person中存在多处Cup的依赖,比如有drinkWater,drinkMilk,drinkCoffee等等,反而又不如Property直观方便了。


方式六:单例引用

单例的优劣有很多优秀的技术文章分析过了,Peak君只强调其中一点,也是平时review代码和Debug发现最多的问题缘由:单例中的状态共享

上面的例子中,我们可以把Cup做成单例,代码如下:


//Person.m
- (void)drink {
id water = [[Cup sharedInstance] provideWater];
[self sip:water];
}

这种方式产生的耦合不但和方式一同样隐蔽,而且是最容易导致代码降级的,随着版本的不停迭代,我们很有可能会得到下面的一个类关联图:

技术分享
所有的对象都依赖于同一个对象的状态,所有的对象都对这个对象的状态拥有读写权限,最后的结果很有可能是到处打补丁修Bug,按下葫芦浮起瓢。

使用单例类似的场景很常见,比如我们在单例中持有某个用户的信息,在用户登出之后,忘记清除之前用户的信息就会导致奇怪的bug,而且单例一旦零散的分布在项目的各个角落,要逐一处理十分困难。


方式七:继承

继承是一种强耦合关系,网络上有不少关于继承(inheritance)和组合(compoisition)之间优劣的对比文章了,这里不做赘述。继承确实能在初期很方便的建立清晰的对象模型,重用和多态看着也很美妙,问题在于这种强耦合关系在理解上很容易产生分歧,比如什么样对象之间可以被确立为父子关系,哪些子类的行为可以放到父类中给其他子类使用,在多层继承的时候这些问题会变得更加复杂。所以Peak君建议尽可能的少用继承关系来描述对象,除非是一目了然毫无异议的父子关系。

我就不强行来一波父类定义来举例了,比如什么ObjectWithCup这类。


方式八:runtime依赖

使用runtime来处理耦合是Objective C独特的方式,而且耦合度非常之低,甚至可以说感觉不到耦合的存在,比如:


//Person.m
- (void)drink:(id)obj
{
id water = nil;
SEL sel
= NSSelectorFromString(@"provideWater");
if ([obj respondsToSelector:sel]) {
water
= [obj performSelector:sel];
}
if (water) {
[self sip:water];
}
}

既不需要导入Cup的头文件,也不需要知道Cup到底支持哪些方法。这种方式的问题也正是由于耦合度太低了,让开发者感知不到耦合的存在,感知不到类之间的关系。如果哪天有人把provideWater改写成getWater,drink方法如果没有同步到,Xcode编译时不会提示你,runtime也不会crash,但是业务流程却没有正常往下走了。

这也是为什么我们不推荐用Objective-C runtime的黑魔法去做业务,只是在无副作用的场景下去完成一些数据的获取操作,比如使用AOP去log日志。


方式九:protocol依赖

这并不是一种独立的耦合方式,protocol可以结合上述各种耦合方式来进一步降低耦合,也是在复杂类关系设计中推荐的方式,比如我们可以定义这样一个protocol:


@protocol LiquidContainer
- (id)provideWater;
- (id)provideCoffee;
@end
//Person.h
@interface Person : NSObject
- (void)drink:(id)container;
@end

上述的方式中,无论是Property持有还是parameter注入,都可以使用protocol来降低依赖,protocol的好处在于他只规定了方法的声明,并不限定具体是那个类来实现它,给后期的维护留下更大的空间和可能性。


更复杂的场景

以上是一些常见的类耦合方式,描述的两个类A,B之间的耦合方式。从上面的描述中,我们可以大致感知到两个类使用不同的方式所导致的耦合的深浅,这种耦合深浅度说白了就是:互相调用函数和访问状态的频次。理解这种耦的深浅可以帮助我们大致去量化两个对象之间的耦合度,从而在更复杂的场景中去分析一个模块或者一种架构方式的耦合度。

在更复杂的场景中,比如A,B,C三个类之间也可以采用类似的方法去分析,A,B,C三者可以是如下关系:技术分享
分析三个类或者更多类之间的耦合关系的时候,也是先拆解成若干个两个类分析,比如左边我们分析AB,BC,AC三组耦合,进而去感知ABC作为一个整体的耦合度。很显然,右边的方式看着比左边的好,因为只需要分析AB和BC。在我们选用设计模式重构代码的时候,也可以依照类似的方式来分析,从而选择耦合度最低,最贴合我们业务场景的模式。

我们的原则是:类与类之间调用的方法,依赖的状态要越少越好,在Objective C这门语言环境下,书写分类清晰,接口简洁的头文件非常重要。


良性的耦合

前面的分析重在尝试去量化和感知耦合的深浅,但并不是每一次方法调用都是有风险的,有些耦合可以称作是良性的。

如果将我们的代码进行高度抽象,所有的代码都可以被归为两类:Data和Action。一个Class中的Property是Data,而Class中的函数则是Action,我之前写过的一篇关于函数式的文章中提到过,真正让我们代码变得危险的是状态的变化,即改变Data。如果一个函数是纯函数,既不依赖于外部状态,也不修改外部状态,那么这个函数无论被调用多少次都是安全的。如果两个类,比如上面举例的Person和Cup,二者互相调用的都是纯函数,那么二者之间的耦合可以看做是良性的,并不会导致程序的状态维护混乱,只是会让代码的重构变得困难,毕竟耦合的越深,重构改动的代码就越多。

所以我们在做设计的时候,应该尽可能使不同元素之间的耦合是良性的,这就涉及到状态的维护问题,先看下图中两种不同的设计方式:技术分享
图中红色的圆圈代表每个类或者功能单位所持有的状态。依照图中上方的设计方式,每个单位各自处理自己的状态变化,这些状态之间还互相存在依赖的话,耦合越深,开发调试和重构就越难,代码就降级越厉害。如果按照图中下方的方式,将状态变化的部分全部都集中到一起处理,维护起来就轻松很多了,这也是为什么很多App都有model layer这一设计的原因,将App状态(各类model)的变化处理独立出来作为一个layer,上层(业务层)只是作为model layer的展现和交互的外壳。这种设计技巧,大可以应用于一个App架构的处理,小可以到一个小功能模块的设计。


结束语

上面总结了我们常用的一些耦合方式,目的在于分析不同代码的书写方式,对于我们最后耦合所产生的影响。最后值得一提的是,上面有些耦合方式并没有绝对的优劣之分,不同的业务场景下可能选择的方式也不同,比如有些场景确实需要持有Property,有些场景单例更合适,关键在于我们能明白不同方式对于我们代码后期维护所产生的影响,这篇文章有些地方可能比较抽象,其中很多都是个人感悟和总结,或有不妥之处,请阅读之后选择性的吸收,希望能对大家平常写代码处理耦合带来一些帮助。






推荐阅读
  • 导读:本文来自SwiftGG翻译组,作者@walkingway基于苹果Swift官方博客中TedKremenek所撰写的“Swift2.2Released!”文章进行了关于Swift2. ... [详细]
  • 【问题】在Android开发中,当为EditText添加TextWatcher并实现onTextChanged方法时,会遇到一个问题:即使只对EditText进行一次修改(例如使用删除键删除一个字符),该方法也会被频繁触发。这不仅影响性能,还可能导致逻辑错误。本文将探讨这一问题的原因,并提供有效的解决方案,包括使用Handler或计时器来限制方法的调用频率,以及通过自定义TextWatcher来优化事件处理,从而提高应用的稳定性和用户体验。 ... [详细]
  • 在Cisco IOS XR系统中,存在提供服务的服务器和使用这些服务的客户端。本文深入探讨了进程与线程状态转换机制,分析了其在系统性能优化中的关键作用,并提出了改进措施,以提高系统的响应速度和资源利用率。通过详细研究状态转换的各个环节,本文为开发人员和系统管理员提供了实用的指导,旨在提升整体系统效率和稳定性。 ... [详细]
  • 在 Linux 环境下,多线程编程是实现高效并发处理的重要技术。本文通过具体的实战案例,详细分析了多线程编程的关键技术和常见问题。文章首先介绍了多线程的基本概念和创建方法,然后通过实例代码展示了如何使用 pthreads 库进行线程同步和通信。此外,还探讨了多线程程序中的性能优化技巧和调试方法,为开发者提供了宝贵的实践经验。 ... [详细]
  • Squaretest:自动生成功能测试代码的高效插件
    本文将介绍一款名为Squaretest的高效插件,该工具能够自动生成功能测试代码。使用这款插件的主要原因是公司近期加强了代码质量的管控,对各项目进行了严格的单元测试评估。Squaretest不仅提高了测试代码的生成效率,还显著提升了代码的质量和可靠性。 ... [详细]
  • 第六章:枚举类型与switch结构的应用分析
    第六章深入探讨了枚举类型与 `switch` 结构在编程中的应用。枚举类型(`enum`)是一种将一组相关常量组织在一起的数据类型,广泛存在于多种编程语言中。例如,在 Cocoa 框架中,处理文本对齐时常用 `NSTextAlignment` 枚举来表示不同的对齐方式。通过结合 `switch` 结构,可以更清晰、高效地实现基于枚举值的逻辑分支,提高代码的可读性和维护性。 ... [详细]
  • 在Kohana 3框架中,实现最优的即时消息显示方法是许多开发者关注的问题。本文将探讨如何高效、优雅地展示flash消息,包括最佳实践和技术细节,以提升用户体验和代码可维护性。 ... [详细]
  • 在 iOS 静态库中使用 Category(分类)时,有时会在集成该静态库的项目中遇到“未识别的选择器发送到实例”的错误。本文深入分析了这一问题的原因,并提供了解决方案,包括确保分类中的方法正确加载和避免命名冲突的方法。通过这些措施,可以有效防止因静态库中的分类导致的运行时错误。 ... [详细]
  • 在Python中,是否可以通过使用Tkinter或ttk库创建一个具有自动换行功能的多行标签,并使其宽度能够随着父容器的变化而动态调整?例如,在调整NotePad窗口宽度时,实现类似记事本的自动换行效果。这种功能在设计需要显示长文本的对话框时非常有用,确保文本内容能够完整且美观地展示。 ... [详细]
  • 本文深入探讨了CGLIB BeanCopier在Bean对象复制中的应用及其优化技巧。相较于Spring的BeanUtils和Apache的BeanUtils,CGLIB BeanCopier在性能上具有显著优势。通过详细分析其内部机制和使用场景,本文提供了多种优化方法,帮助开发者在实际项目中更高效地利用这一工具。此外,文章还讨论了CGLIB BeanCopier在复杂对象结构和大规模数据处理中的表现,为读者提供了实用的参考和建议。 ... [详细]
  • 本文详细探讨了Zebra路由软件中的线程机制及其实际应用。通过对Zebra线程模型的深入分析,揭示了其在高效处理网络路由任务中的关键作用。文章还介绍了线程同步与通信机制,以及如何通过优化线程管理提升系统性能。此外,结合具体应用场景,展示了Zebra线程机制在复杂网络环境下的优势和灵活性。 ... [详细]
  • 在Swift编程中,派生类中重写方法时调用其父类的同名方法是一种常见的做法。然而,如果不正确地处理这种调用,可能会导致程序崩溃。本文详细分析了这一问题的根源,并提供了有效的解决策略,帮助开发者避免此类错误。例如,在一个自定义的`ViewController`类中继承自`UITableViewController`时,如果重写了某个方法而未恰当调用父类的实现,就可能引发运行时异常。通过本文的指导,读者将能够更好地理解Swift中的方法重载机制,确保代码的稳定性和安全性。 ... [详细]
  • [译]  OS X 和 iOS 的测绘框架Core Plot 入门教程
    [译] OS X 和 iOS 的测绘框架Core Plot 入门教程 ... [详细]
  • 我正在尝试使SwiftUI中的按钮在文本旁边显示图像。但是,即使不在按钮中时图像 ... [详细]
  • 项目地址:github.comDanie1sDNS…DNSPageViewDNSPageView一个纯Swift的轻量级、灵活且易于使用的pageView框架 ... [详细]
author-avatar
挽木城祠_
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有