1、架构模式 MVC/MVP/MVVM:一切不结合项目和实际问题空谈架构的行为都是耍流氓。
**1.1 MVC-**低耦合,可复用
Model(模型) :负责业务数据管理和处理,包括增删改查。Model必须提供外部可以操作模型数据的接口,同时在数据发生变化后能够通知外部。
View(视图) :用户界面。View需要感知Model的变化,数据变化时,更新用户界面
Controller(控制器) :业务逻辑层。Controller需要感知View的事件,调用Model提供的接口对数据进行操作
MVC的工作流程
1、用户对界面进行操作,触发View的相关事件;
2、View感知这些事件,通知Controller进行处理;
3、Controller处理相关业务,并通过Model的接口对业务数据进行更新;
4、Model的业务数据改变触发相应事件,通知View业务数据已经发生改变;
5、View捕获到Model发出的数据改变事件,重新获取数据并更新用户界面。
MVC的优缺点
优点:
降低用户界面与业务逻辑之间的耦合性。大大降低了用户界面和业务逻辑的耦合性,可以隔离模块间的变化,有利于维护和扩展。
复用性高。多个视图可以共享一个模型。MVC可以将一个项目分为三个子项目分别进行开发和管理,有利于软件工程化,也可实现独立部署。
因为业务逻辑和界面的分离,为业务模块的单元测试创造了条件。
缺点:
不适合中小规模的软件项目。MVC引入了更多概念,增加了项目结构和实现的复杂性,将其应用到中小规模项目通常得不偿失。
更多的内存消耗。Model中的内容都可以从界面获取到,因此,Model的引入增加了内存消耗。
降低了程序性能。因为实现的复杂性,势必会导致性能降低。
注意:
MVC具有一定的分层,model彻底解耦,controller和view并没有解耦
层与层之间的交互尽量使用回调或者去使用消息机制去完成,尽量避免直接持有。 controller和view在android中无法做到彻底分离,但在代码逻辑层面一定要分清
业务逻辑被放置在model层,能够更好的复用和修改增加业务。
1.2 MVP:Model-View-Presenter
从MVC演变而来的。在MVC的基础上,MVP强调Model与View之间的隔离,两者互不感知。
MVP的架构图如下:
1.3 MVVM:Model-View-ViewModel-数据绑定
-
模型(Model):负责数据和业务逻辑。
-
视图(View):用户界面,通常指代UIViewController及其视图。
-
视图模型(ViewModel):表示视图的状态和行为,它封装了模型数据的转换逻辑,使得数据可以更容易地显示在视图上。视图通过数据绑定与视图模型通信。
应用场景:MVVM模式适用于视图逻辑较为复杂,需要频繁更新UI的应用程序。通过数据绑定,可以减少视图和视图模型之间的通信代码,使得代码更加简洁。在Objective-C中,虽然没有原生支持数据绑定,但可以通过KVO(键值观察)或者使用第三方框架来实现。
1.4 VIPER是iOS应用的一种架构模式,旨在解决传统MVC架构中的一些问题,如控制器过于庞大等。
它将应用分为以下几个部分:
-
View: 负责展示用户界面,接收用户的输入。
-
Interactor: 包含应用的业务逻辑,用于处理应用的数据。
-
Presenter: 作为View和Interactor之间的桥梁,从Interactor获取数据后处理,然后将结果展示到View上。
-
Entity: 包含基本的模型对象。
-
Router: 处理导航逻辑,负责在不同的屏幕(View)之间的转换。
每个部分都有其明确的职责,有助于更好地组织代码,使得各部分之间的耦合度更低,更易于测试。
1.5 在Objective-C中的应用
- MVC:由于UIKit的设计,iOS开发者自然而然地使用MVC。控制器(UIViewController)通常承担了过多的职责,这被称为“Massive View Controller”问题。为了解决这个问题,开发者可能会考虑使用MVP或MVVM来更好地分离关注点。
- MVP:在Objective-C中实现MVP,通常需要定义一个协议(Protocol)来规定Presenter和View之间的交互。View通过持有Presenter的引用并调用其方法来处理用户输入,Presenter则通过协议方法更新View。
- MVVM:虽然Objective-C不直接支持数据绑定,但可以通过KVO来实现ViewModel到View的自动更新。此外,也可以使用像ReactiveCocoa这样的响应式编程框架来更方便地实现MVVM模式。
总的来说,选择哪种架构模式取决于具体项目的需求、团队的熟悉度以及项目的复杂度。在Objective-C开发中,理解这些模式的优缺点可以帮助开发者写出更清晰、更易维护的代码。
🌟2、KVC/KVO--iOS底层原理总结 - 探寻KVO本质
KVC (Key-Value Coding键值编码) 和 KVO (Key-Value Observing,键值观察) 是两个非常重要的概念,它们提供了一种机制,允许开发者通过字符串键来访问和观察对象的属性。这些特性极大地增强了代码的灵活性和动态性。
2.1 键值编码Key-Value Coding
KVC 是一种可以通过对象的字符串键来访问、修改对象属性的机制。它允许开发者直接通过属性名称的字符串来读取或修改对象的属性(包括私有属性),而不需要通过显式的调用 getter 或 setter 方法。 这是通过在 NSObject 上实现的一些方法来支持的。实践:juejin.cn/post/725751…
KVC的操作方法由NSKeyValueCoding协议提供,而NSObject就实现了这个协议,也就是说Object-C中几乎所有的对象都支持KVC操作,常用的KVC操作方法如下:
KVC 取值有一点需要我们注意。若实例变量不是 OC 对象,KVC 会自动帮我们转换,把值类型封装成 NSNumber,把结构体封装成 NSValue。keyPath 使用🌟
crash问题 🌟
基本方法:
-
(nullable id)valueForKey:(NSString *)key; //直接通过属性名来取值
-
(void)setValue:(nullable id)value forKey:(NSString *)key; //通过属性名来设值
-
(nullable id)valueForKeyPath:(NSString *)keyPath; //通过属性路径来取值
-
(void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过属性路径来设值
示例:
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation Person
@end
Person *person = [[Person alloc] init];
[person setValue:@"John" forKey:@"name"];
[person setValue:@25 forKey:@"age"];
NSString *name = [person valueForKey:@"name"];
NSInteger age = [[person valueForKey:@"age"] integerValue];
如果不正确地使用,可能会导致崩溃(crash)。以下是一些常见的导致KVC操作崩溃的情况:
1. **键(Key)不存在**:当尝试通过KVC访问一个对象的属性,但是该属性的键不存在时,会抛出`NSUndefinedKeyException`异常,导致应用崩溃。
例如,尝试访问一个对象的`name`属性,但是该对象没有定义`name`属性。
[object setValue:@"value" forKey:@"nonexistentKey"]; // 如果nonexistentKey不存在,会导致崩溃
2. **键路径(Key Path)不正确**:键路径是由多个键通过点(`.`)连接起来的字符串,用于访问对象的嵌套属性。如果键路径中的某个键不存在,同样会抛出`NSUndefinedKeyException`异常。
[object setValue:@"value" forKeyPath:@"person.nonexistentKey"]; // 如果person存在但nonexistentKey不存在,会导致崩溃
3. **设置不兼容的值类型**:当通过KVC设置属性值时,如果提供的值类型与属性的期望类型不匹配,可能会导致崩溃。例如,尝试将一个字符串赋值给一个整型属性。
[object setValue:@"stringValue" forKey:@"integerProperty"]; // 如果integerProperty是整型,尝试赋值字符串会导致崩溃
4. **集合操作中的问题**:KVC还支持对集合进行操作,如`@count`、`@avg`等。如果在进行这些操作时,键路径不正确或者操作的对象不支持这类操作,也可能导致崩溃。
5. **只读属性**:尝试通过KVC设置一个只读属性的值,可能会导致崩溃,因为只读属性没有相应的setter方法。
为了避免这些崩溃,可以采取以下措施:
- 在使用KVC之前,确保键或键路径正确且存在。
- **使用`@try-@catch`块捕获可能抛出的`NSUndefinedKeyException`异常**。
-**在设置属性值之前,检查值的类型是否与属性类型匹配**。
- 对于只读属性,避免尝试通过KVC设置其值。
正确地使用KVC不仅可以提高代码的灵活性和表达力,还可以避免因错误使用而导致的崩溃问题。
## **2.2 KVO:键值观察**
是一种**允许对象观察其它对象某一属性变化的机制**。当被观察对象的属性发生变化时,观察者对象会接收到一个通知,从而可以响应这一变化。
**KVO 依赖于 KVC,因为它使用 KVC 来获取被观察属性的值。KVO**是一种**观察者模式的衍生,用于监听某个对象属性值的改变。** 当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个**全新的通过Runtime动态创建的子类** **NSKVONotifying_对象的类。比如**KVO 会在运行时动态生成个名为 **NSKVONotifying_Person** 的类,该类继承自 Person 类,并重写了 setValue:forKey: 和 valueForKey: 方法;当我们为 Person 对象的 name 属性注册观察者后,K**VO 会将 name 属性的 setter 方法替换成新的 setter,从而实现属性变化时通知**;顺序:willChangeValueForKey:方法;原来的setter方法实现。didChangeValueForKey:方法
- ### **Q1:iOS用什么方式实现对一个对象的KVO?** (KVO的本质是什么?) --**Runtime**
A. 当一个对象使用了KVO监听,**iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类**,子类拥有自己的set方法实现,set方法实现内部会顺序调用**willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法.**
**而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。**
- ### Q2 : 如何手动触发KVO?--<https://juejin.cn/post/6844903593925935117>
<!---->
- A:被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO **,则需要我们自己调用willChangeValueForKey:和didChangeValueForKey:方法,即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可.--TODO:不去调用**willChangeValueForKey会怎么样
- 点击展开内容
setage-->_NSsetIntValueAndNotify内部做的操作相当于,首先调用willChangeValueForKey 将要改变方法,之后调用父类的setage方法对成员变量赋值,最后调用didChangeValueForKey已经改变方法。**didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法中.**
NSKVONotifyin_Person重写class方法是为了隐藏NSKVONotifyin_Person。不被外界所看到。我们在p1添加过KVO监听之后,分别打印p1和p2对象的class可以发现他们都返回Person。NSLog(@"%@,%@",[p1 class],[p2 class]);
**如果NSKVONotifyin_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到nsobject**,而nsobect的class的实现大致为返回自己isa指向的类,返回p1的isa指向的类那么打印出来的类就是NSKVONotifyin_Person,但是apple不希望将NSKVONotifyin_Person类暴露出来,并且不希望我们知道NSKVONotifyin_Person内部实现,所以在内部重写了class类,直接返回Person类,所以外界在调用p1的class对象方法时,是Person类。这样p1给外界的感觉p1还是Person类,并不知道NSKVONotifyin_Person子类的存在。
- (Class) class {
// 得到类对象,在找到类对象父类
return class_getSuperclass(object_getClass(self));
}
didChangeValueForKey方法内部已经调用了observer的observeValueForKeyPath:ofObject:change:context:方法。
![image.jpeg]()
- ![image.jpeg]()
当我们为 Person 对象注册观察者后,KVO 会将该对象的 isa 指针指向 **NSKVONotifying_Person** 类,从而使得该对象的所有方法的调用都会被转发到 NSKVONotifying_Person 类中.所以当我们调用 Person 对象 name 属性的 setter 方法,实际上调用的是 NSKVONotifying_Person 类的 setter 方法,该方法会先调用父类的 setter 方法,然后再发送通知,通知所有观察者属性发生了变化。**observeValueForKeyPath**
简单来说**KVO**可以通过监听对象属性的key,来获得value的变化,利用**它可以在对象之间监听值**的变化。在**Objective-C**中要实现**KVO**则必须实现**NSKeyValueObServing**协议,而**NSObject**已经实现了改协议,因此对于所有继承了**NSObject**的类型,也就是说**Object-C**中几乎所有的对象都支持**KVO**操作,常用的**KVO**操作方法如下:
## 2.3 使用**KVO**流程
注册观察者 addObserver: forKeyPath: options: context:
实现回调方法 observeValueForKeyPath: ofObject: change: context:
在合适的时机,移除观察者 removeObserver: forKeyPath、removeObserver: forKeyPath: context:
/** 注册观察者 observer:观察者,也就是KVO通知的订阅者。 keyPath:描述将要观察的属性,相当于被观察者。 options:KVO的一些属性配置。 NSKeyValueObservingOptionNew:change字典包括改变后的值 NSKeyValueObservingOptionOld:change字典包括改变前的值 NSKeyValueObservingOptionInitial:注册后立刻触发KVO通知 NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次)
context: 上下文,这个会传递到订阅着的函数中,用来区分消息。 */
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; // 移除观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context
// 移除观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; // 监听回调方法, change 这个字典保存了变更信息,具体是哪些信息取决于注册观察者时的options
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
示例:
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation Person
@end
Person *person = [[Person alloc] init];
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
// 在适当的时候,移除观察者
[person removeObserver:self forKeyPath:@"name"];
// 当 `person` 的 `name` 属性发生变化时,观察者(这里是 `self`)会收到一个通知,可以通过重写 `observeValueForKeyPath:ofObject:change:context:` 方法来处理这个通知。
注意🌟:KVO 只能观察对象的属性,不能观察成员变量,因为 KVO 是基于 setter 方法实现的,而成员变量的赋值不会触发 setter 方法的调用,无法观察到成员变量的变化。
注意事项
-
使用 KVC 时,如果键名错误或对象不支持 KVC,会抛出异常。
-
使用 KVO 时,必须确保在观察者销毁前移除它,否则可能会导致崩溃。
-
KVO 通常用于模型和视图之间的同步,或者在某些属性变化时需要执行特定操作的场景。
KVC 和 KVO 是 Cocoa 编程中非常强大的特性,它们提供了一种高度动态的方式来交互对象的状态,但同时也需要谨慎使用,以避免潜在的运行时错误。
2.4 实践
键值观察(KVO)是Cocoa编程中的一个重要特性,允许对象监听另一个对象属性的变化。正确使用KVO可以让你的应用更加响应式,但如果使用不当,也可能导致难以追踪的bug。以下是一些KVO的最佳实践:
- 始终在addObserver之后成对调用removeObserver:这是防止观察者在被观察的对象被释放后仍然存在,从而尝试访问已经不存在的对象,导致崩溃。
- 使用@objc dynamic修饰需要被观察的属性:在Swift中,只有标记为@objc dynamic的属性才能使用KVO,因为KVO是基于Objective-C运行时的。
- 在deinit中移除观察者:确保在对象销毁前移除所有观察者,这是防止内存泄漏和野指针访问的关键。
- 处理好观察回调:在观察者的回调方法中,确保你的代码能够处理好各种边界情况,避免因为未预料到的属性变化导致的崩溃。
- 使用context参数区分观察者:当多个观察者观察同一个对象时,使用context参数可以帮助区分不同的观察者,避免混淆。
- 优先考虑使用属性观察器:在Swift中,如果你只是想要在一个类内部监听某个属性的变化,使用属性观察器(willSet和didSet)通常是更简单、更安全的选择。
- 小心使用KVO:虽然KVO是一个强大的工具,但它也可能导致代码难以理解和维护。在使用KVO之前,考虑是否有更简单的方法可以达到同样的目的,比如使用代理模式、闭包或者使用框架提供的数据绑定功能。
class MyObject: NSObject {
@objc dynamic var myProperty: String
init(myProperty: String) {
self.myProperty = myProperty
}
}
class MyObserver: NSObject {
var objectToObserve: MyObject
init(object: MyObject) {
self.objectToObserve = object
super.init()
object.addObserver(self, forKeyPath: "myProperty", options: [.old, .new], context: nil)
}
deinit {
objectToObserve.removeObserver(self, forKeyPath: "myProperty")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "myProperty" {
print("myProperty changed to: (change?[.newKey] ?? "")")
}
}
}
2.5 坑点
Key-Value Observing(KVO)是Objective-C中用于实现对象之间通信的一种机制,允许对象监听另一个对象属性值的变化。尽管KVO是一个强大的特性,但在使用过程中也存在一些“坑点”或需要注意的地方:
- performselector移除观察者:最常见的问题之一是忘记移除观察者。如果观察者对象在被观察的对象销毁前没有被正确移除,当被观察对象的属性发生变化时,尝试通知已经不存在的观察者,这将导致崩溃。因此,确保在观察者对象销毁前移除观察者是非常重要的。
- 重复添加或移除观察者:重复添加观察者会导致观察者收到多次通知,而重复移除观察者则会抛出异常。开发者需要确保添加和移除观察者的操作是成对出现的。
- 自动通知和手动通知:默认情况下,KVO是自动通知的,即当属性值发生变化时,观察者会自动收到通知。但在某些情况下,如果需要更精细的控制通知的发送,可以关闭自动通知,并手动发送通知。这种情况下,需要特别注意手动通知的正确性和及时性。
- 观察的属性必须是KVO兼容的:并不是所有的属性变化都能被KVO自动检测到。为了使属性变化能够被KVO检测到,属性的修改必须通过setter方法,或者直接修改后调用willChangeValueForKey:和didChangeValueForKey:方法。直接修改内部实例变量的值不会触发KVO通知。
- 线程安全问题:KVO的回调默认是在属性被修改的那个线程中执行的,这可能会导致线程安全问题。如果需要在特定线程(如主线程)处理KVO回调,需要开发者自己做相应的线程调度。
- 性能问题:虽然在大多数情况下,KVO对性能的影响可以忽略不计,但在极端情况下(如大量对象观察同一个属性),KVO可能会成为性能瓶颈。在性能敏感的场景下,需要谨慎使用KVO。
- 继承问题:当子类和父类同时观察同一个对象的同一个属性时,需要确保观察者的添加和移除逻辑正确,避免重复通知或者遗漏通知的问题。
总之,虽然KVO是Objective-C中一个强大的特性,能够有效地实现对象间的通信,但在使用时也需要注意以上提到的一些问题和“坑点”,以避免潜在的错误和崩溃。正确地使用KVO需要对其工作原理有深入的理解,并在实践中不断积累经验。
3、八条基础原则
- 单一职责原则(Single Responsibility Principle):一个类或模块只负责完成一个职责。
- 开闭原则(Open Closed Principle):软件实体应该“对扩展开放、对修改关闭”。
- 里式替换原则(Liskov Substitution Principle):子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
- 接口隔离原则(Interface Segregation Principle):客户端不应该强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。
- 依赖反转原则(Dependency Inversion Principle):高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。
- 迪米特法则(最小知识原则):每个模块只应该了解那些与它关系密切的模块的有限知识。或者说,每个模块只和自己 的朋友“说话”,不和陌生人“说话”(不该依赖的不依赖,该依赖的少依赖)。
- DRY原则(Don’t Repeat Yourself):不要重复自己。 重复的代码、重复的轮子、重复的调用。
- 三次原则(Rule of three):指的是当某个功能第三次出现时,才进行“抽象化”。它的含义是:当第一次用到某个功能时,写一个特定的解决方法;第二次又用到的时候,拷贝上一次的代码;第三次出现的时候,才着手“抽象化”,写出通用的解决方法。
- KISS原则(Keep It Simple and Stupid):尽量保持简单。不要使用一些“奇淫巧技”优化代码而失去了可读性与可维护性。
设计是在质量、成本、时间等因素之间做出权衡的艺术。
“为什么” 比 “怎么做” 更重要
功夫在日常,请持续重构。历史已经证明,越是那些为了实现远大的技术目标而对眼前混乱不管不顾的人,频频让我们的工程陷入大火。
做出好的设计除了自身要有深厚的内力外,还要能明确现有的痛点与瓶颈、考虑是否满足业务中长期的扩展、项目与人力成本等,一个好的工程师除了要自身技术过硬外,还要是好的产品和PMO,我想这也是码农与工程师的区别。
juejin.cn/post/734995… 按照对应的说法对理一下视频页 TODO
4、设计模式
4.1 设计模式分类:创建型模式、结构型模式和行为型模式。
每一种类型下又包含了多种具体的设计模式,以下是一些常见的设计模式:
复杂-变化-设计- 分析-文档-重构-勇者-恶龙
- 创建型模式:关注对象的创建过程。通过封装复杂的创建过程,解耦对象的创建代码与使用代码,如我们常用的单例模式、工厂模式等。
- 单例模式(Singleton) :确保一个类只有一个实例,并提供一个全局访问点。应用场景:全局状态管理、日志记录器等。
- 原型模式(Prototype):通过复制现有的实例来创建新的实例。
- 工厂方法模式(Factory Method):定义一个用于创建对象的接口,让子类决定实例化哪一个类。应用场景:组件或对象的创建。
点击展开内容
工厂模式是一种常用的设计模式,用于创建对象,而不需要指定将要创建的对象的具体类。这种模式通过定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法让类的实例化推迟到子类中进行。
工厂模式的分类
-
简单工厂模式:不直接使用 new 来实例化对象,而是通过一个工厂类来负责创建产品类的实例。
-
工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
-
抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
示例:工厂方法模式
这里我们将使用 Swift 来实现一个简单的工厂方法模式示例。假设我们有一个应用,需要根据不同的配置或条件来创建不同类型的日志记录器。
首先,定义一个表示日志记录器的协议(Logger)和几个具体的日志记录器类:
protocol Logger {
func log(message: String)
}
class FileLogger: Logger {
func log(message: String) {
print("Log '\(message)' to a file")
}
}
class ConsoleLogger: Logger {
func log(message: String) {
print("Log '\(message)' to console")
}
}
接下来,创建一个抽象的日志工厂类,以及具体的工厂类来决定实例化哪一个日志记录器:
protocol LoggerFactory {
func createLogger() -> Logger
}
class FileLoggerFactory: LoggerFactory {
func createLogger() -> Logger {
return FileLogger()
}
}
class ConsoleLoggerFactory: LoggerFactory {
func createLogger() -> Logger {
return ConsoleLogger()
}
}
现在,我们可以根据需要使用不同的工厂来创建日志记录器:
func useLogger() {
let loggerFactory: LoggerFactory = ConsoleLoggerFactory()
let logger = loggerFactory.createLogger()
logger.log(message: "This is an information.")
}
useLogger()
总结
在这个示例中,LoggerFactory 是一个工厂协议,它定义了一个方法 createLogger,该方法用于创建遵循 Logger 协议的对象。FileLoggerFactory 和 ConsoleLoggerFactory 是具体的工厂,它们实现了 LoggerFactory 协议,分别用于创建 FileLogger 和 ConsoleLogger 对象。
这种方式的好处是,如果你需要引入新的日志记录方式,只需添加一个新的具体日志记录器类和对应的工厂类,而无需修改现有代码。这样做提高了代码的可扩展性和可维护性。
- 抽象工厂模式(Abstract Factory):提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
- 建造者模式(Builder):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
- 结构型模式:关注类和对象的组合。主要总结了对象在一起的一些经典结构,这些经典结构可以解决特定应用场景的问题,如:代理模式、适配器模式等。
- 适配器模式(Adapter):允许将一个类的接口转换成客户期望的另一个接口。
- 桥接模式(Bridge):将抽象部分与实现部分分离,使它们可以独立变化。
- 组合模式(Composite):将对象组合成树形结构以表示“部分-整体”的层次结构。
- 装饰器模式(Decorator):动态地给一个对象添加一些额外的职责。
- 外观模式(Facade):提供了一个统一的接口,用来访问子系统中的一群接口。
- 享元模式(Flyweight):运用共享技术有效地支持大量细粒度的对象。
- 代理模式(Proxy):为其他对象提供一种代理以控制对这个对象的访问。
- 行为型模式:关注对象之间的通信。主要用于描述类或对象的交互,以及职责的分配,对类之间交互,以及分配责任的方式提供指南,如:观察者模式、策略模式等。
- 观察者模式(Observer):定义对象间的一种一对多的依赖关系,当一个对象状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。应用场景:事件监听、数据绑定等。
- 模板方法模式(Template Method):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
- 命令模式(Command):将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化。
- 状态模式(State):允许一个对象在其内部状态改变时改变它的行为。
- 策略模式(Strategy):定义一系列的算法,把它们一个个封装起来,并使它们可相互替换。
命令模式和策略模式的区别
命令模式(Command Pattern)和策略模式(Strategy Pattern)都是软件设计中常用的设计模式,它们都允许通过对象来封装行为,但它们的用途、实现方式和目的存在一些关键的区别。
命令模式
命令模式的核心在于将一个请求或操作封装为一个对象。这样,使用这些操作的代码和实现这些操作的代码之间的耦合度降低,增加了代码的灵活性。命令模式通常用于以下场景:
-
需要对操作进行记录、撤销/重做、事务等处理时。
-
系统需要将请求调用者和请求接收者解耦时。
-
需要把请求排队处理,或者记录请求日志,以及支持可撤销操作时。
命令模式涉及到的角色包括:
- Command:命令接口,声明执行操作的接口。
- ConcreteCommand:具体命令,实现Command接口,定义绑定在接收者上的动作。
- Client:客户端,创建一个具体命令对象并设定它的接收者。
- Invoker:请求者,负责调用命令对象执行请求。
- Receiver:接收者,知道如何实施与执行一个请求相关的操作。
策略模式
策略模式的核心在于定义一系列的算法,把它们一个个封装起来,并使它们可以相互替换。策略模式让算法独立于使用它的客户端而变化,即使算法变化,使用算法的客户端也不需要变化。策略模式通常用于:
-
多个类只有在算法或行为上稍有不同的场景。
-
需要安全地封装多种同一类型的操作时。
-
出现同一抽象类有多个子类,而又需要使用if-else或者switch-case来选择具体子类时。
策略模式涉及到的角色包括:
- Strategy:策略接口,定义了一个算法族,它们都实现了相同的操作。
- ConcreteStrategy:具体策略,实现了策略接口的具体算法。
- Context:上下文,用一个ConcreteStrategy来配置,维护一个对Strategy对象的引用。
区别
-
目的不同:命令模式主要用于将行为请求者和行为实现者解耦,常用于命令的排队执行、记录或撤销等操作。策略模式主要用于定义一组算法,让算法独立于使用它的客户端而变化,主要用于算法的选择和切换。
-
应用场景不同:命令模式适用于需要对操作进行更复杂控制的场景,如撤销/重做、事务等;而策略模式适用于在不同情况下需要使用不同算法的场景。
-
实现方式不同:在命令模式中,请求以命令的形式包裹在对象中,并传给调用对象;调用对象寻找可以处理该命令的合适的对象。而策略模式则是将算法��装在策略对象中,由上下文来管理这些策略对象,使得它们可以互相替换。
总的来说,命令模式强调的是对操作的封装,策略模式强调的是对算法的封装。两者虽然都涉及到行为的封装,但应用的侧重点和场景不同。
点击展开内容
策略模式(Strategy Pattern)是一种对象行为型模式,它定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。在策略模式中,算法的使用者(通常是一个上下文对象)拥有一个策略接口的引用,通过这个引用来使用具体的策略实现,策略实现的切换可以动态地在运行时发生。
以下是策略模式的基本组成部分:
-
策略接口(Strategy):这是一个接口,定义了算法族中每个算法所必须遵守的约定或者是一个抽象类,定义了算法的公共接口。具体的算法需要实现这个接口。
-
具体策略(Concrete Strategy):实现了策略接口的类,提供具体的算法实现。策略模式支持任意数量的具体策略实现,且可以动态地在运行时切换。
-
上下文(Context):使用策略的角色。上下文持有一个策略接口的引用,可以是设置好的一个具体策略对象,上下文不负责策略算法的逻辑,只负责调用策略接口中定义的方法。
策略模式的优点包括:
-
封装性好:每个策略都被封装成了一个类,易于维护和扩展。
-
避免使用多重条件转移语句:如if-else或switch-case。使用策略模式可以避免这种情况,代码更加清晰。
-
策略类之间可以自由切换:由于策略类实现了相同的接口,所以它们之间可以自由切换。
策略模式的缺点包括:
-
客户端必须知道所有的策略类:并自行决定使用哪一个策略类。
-
产生很多策略类:每一个策略都是一个类,随着策略的增加,类的数量也会增加。
在iOS开发中,策略模式可以用于多种场景,比如表单验证、网络请求处理等,通过定义一系列的算法或行为,然后在运行时选择使用哪一个,从而使得算法可以独立于使用它们的客户端变化。
- 职责链模式(Chain of Responsibility):为请求创建一个接收者对象的链。
- 迭代器模式(Iterator):提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。
- 中介者模式(Mediator):用一个中介对象来封装一系列的对象交互。
中介者模式和代理模式区别
中介者模式和代理模式都是软件设计中常用的设计模式,它们在结构上有一定的相似性,但用途、目的和实现方式有所不同。
中介者模式
中介者模式(Mediator Pattern)用于减少多个对象或类之间的通信复杂性。这种模式提供了一个中介对象,该对象通常封装了一组对象之间的交互方式,使得这些对象不需要显式地相互引用,从而使其耦合松散,可以独立地改变它们之间的交互。
特点:
-
集中控制交互:所有的组件交互都通过中介者进行,中介者知道每个组件和它们的交互细节。
-
减少类间依赖:组件只依赖中介者,减少了组件间的直接通信,降低了耦合度。
-
方便修改和扩展:交互逻辑集中在中介者中,修改和扩展交互逻辑时,只需修改中介者即可。
应用场景:
-
当一组对象之间的通信方式非常复杂且难以理解时。
-
当需要实现一个对象集合之间的通信协议,而又不希望这些对象直接相互引用时。
代理模式
代理模式(Proxy Pattern)为其他对象提供一种代理以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介的作用,并且可以在不改变目标对象的前提下增加一些额外的处理,如访问控制、缓存、智能引用等。
特点:
-
控制对象访问:代理可以控制对目标对象的访问,实现权限控制、延迟初始化等。
-
增加额外处理:在访问目标对象时,代理可以添加一些额外的操作,如日志记录、性能监控等。
-
不改变目标对象:代理模式允许在不修改目标对象代码的情况下工作。
应用场景:
-
当需要为一个对象提供不同的访问控制时。
-
当需要为访问一个重量级对象提供一个轻量级的代理对象,以减少系统资源的消耗时。
区别
-
目的不同:中介者模式主要用于简化对象间的通信,将多对多的通信转化为一对多的通信,减少对象间的依赖。代理模式主要用于控制对对象的访问,可以在不改变对象接口的前提下增加一些额外的功能。
-
应用场景不同:中介者模式适用于对象间存在复杂的直接交互的场景,代理模式适用于需要间接访问对象或需要在访问对象时执行额外操作的场景。
-
实现方式不同:中介者模式通过一个中介者对象来封装一系列对象之间的交互逻辑。代理模式通过创建一个代理对象来代表实际对象,代理对象对实际对象的访问进行控制和增强。
总的来说,中介者模式和代理模式虽然在某些方面相似,但它们解决的问题和应用的场景有很大的不同。在实际的软件设计中,应根据具体需求选择合适的模式。
- 访问者模式(Visitor):表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
- 备忘录模式(Memento):在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
发布订阅模式允许一个对象(发布者或者称为主题)发布事件,而其他对象(订阅者或者称为观察者)订阅这些事件,当事件发生时,发布者会通知所有订阅者进行相应的处理。 这种模式常被用于事件驱动的架构中,如前端开发中的事件处理、消息队列等。
该模式包含三个核心组件:发布者、订阅者、事件。
- 发布者:当发布者发布事件时,会通知所有订阅者,并调用订阅者的处理方法。
- 订阅者:订阅者负责订阅事件或者消息,并提供处理事件的方法。当发布者发布相关事件时,订阅者会接收到通知并执行相应的处理逻辑。
- 事件:事件是发布者和订阅者之间通信的载体,包含了事件类型和相关的数据
观察者模式和发布订阅模式有什么区别么
观察者模式和发布-订阅模式都是行为设计模式,用于在对象之间定义一种依赖关系,使得当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并被自动更新。尽管这两种模式在目的上相似,它们在实现上有一些关键的区别。
观察者模式
观察者模式定义了对象之间的一对多依赖关系,这样一来,当一个对象改变状态时,它的所有依赖者(观察者)都会收到通知并自动更新。在观察者模式中,被观察的对象直接维护一份观察者列表,并负责通知它们。
-
直接的通信:观察者模式中,被观察者直接通知观察者,没有第三方介入。
-
紧密耦合:观察者和被观察者之间的耦合度相对较高。
发布-订阅模式
发布-订阅模式使用了一个事件通道,这个通道作为调度中心,当发布者发布事件时,不直接通知订阅者,而是通过调度中心来传递消息。订阅者订阅感兴趣的事件并从调度中心接收事件通知。
-
间接的通信:发布者和订阅者之间不直接通信,而是通过消息队列或事件通道进行通信。
-
解耦合:发布者和订阅者之间的耦合度更低,它们之间不需要知道对方的存在。
主要区别
-
通信方式:观察者模式中,被观察者直接通知观察者;而发布-订阅模式中,发布者和订阅者通过一个中间件(事件通道)进行通信。
-
耦合度:观察者模式中,观察者和被观察者之间耦合度较高;发布-订阅模式通过引入第三方调度中心,降低了发布者和订阅者之间的耦合度。
-
使用场景:观察者模式适用于对象之间的直接关系,特别是当被观察对象需要直接通知观察者时;发布-订阅模式适用于跨系统或模块的消息通信,特别是在分布式系统中,可以实现更灵活的消息通信机制。
总的来说,发布-订阅模式可以看作是观察者模式的一种扩展,它通过引入一个调度中心来降低对象之间的耦合度,使得系统更加灵活和可扩展。
4.2 单例模式
在 Objective-C中编写一个单例模式的类通常涉及以下几个步骤:
1. **声明一个静态实例**:这个静态实例用于保持类的唯一实例。
2. **提供一个类方法**:这个方法用于访问这个唯一实例。
3. **重写`allocWithZone:`方法**:确保通过使用`new`、`alloc`或`copy`等方法不能创建新的实例。
4. **线程安全**:确保在多线程环境下,这个类的实例化是安全的。
### 示例
以下是一个实现单例模式的 Objective-C 类的示例:
```objective-c
#import <Foundation/Foundation.h>
@interface MySingleton : NSObject
// 提供一个类方法来获取这个单例
+ (instancetype)sharedInstance;
@end
@implementation MySingleton
// 静态变量_instance,用于保存类的唯一实例
static MySingleton *_instance = nil;
// 类方法实现
+ (instancetype)sharedInstance {
// 使用GCD的dispatch_once来确保这个初始化块只被执行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 这里调用super的alloc方法,避免调用下面重写的allocWithZone:
_instance = [[super allocWithZone:NULL] init];
});
return _instance;
}
// 重写allocWithZone:方法
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self sharedInstance];
}
// 为了完整性,通常还需要重写copyWithZone:方法
- (id)copyWithZone:(NSZone *)zone {
return self;
}
// 如果使用mutableCopy,也需要重写mutableCopyWithZone:
- (id)mutableCopyWithZone:(NSZone *)zone {
return self;
}
@end
```
### 使用单例
```objective-c
MySingleton *singletonInstance = [MySingleton sharedInstance];
```
### 注意事项
- 单例模式在某些情况下是非常有用的,比如当你需要一个全局可访问的资源,或者需要确保某个类只有一个实例时。但是,过度使用单例模式可能会导致代码之间的耦合度增加,使得单元测试变得困难。
- 在设计单例时,应该考虑到线程安全问题。上面的示例中使用了`dispatch_once`来确保初始化代码块在多线程环境下只被执行一次,这是一种线程安全的实现方式。
4.3 代理模式
delegate代理模式是一种常见的设计模式,它允许一个对象(称为代理对象)来代表另一个对象(称为委托对象)执行操作,用于实现对象间的通信和回调机制。
在这种模式下,一个对象(我们称之为委托者)将某些任务或事件的处理委托给另一个对象(称为代理)。为了实现这种委托关系,委托者会持有一个指向代理的引用。
### 定义代理协议 首先,定义一个协议(Protocol),在协议中声明代理对象需要执行的方法。这些方法通常由委托对象来调用,以响应特定的事件或执行特定的任务。
// Objective-C
@protocol MyDelegate <NSObject>
- (void)taskDidFinishWithResult:(NSString *)result;
@end
### 委托对象持有代理:委托对象需要持有一个对代理对象的引用。通常,这个引用被声明为`weak`属性,以避免循环引用。
// Objective-C
@interface MyObject : NSObject
@property (weak, nonatomic) id<MyDelegate> delegate;
### 实现代理方法:代理对象需要遵循上面定义的协议,并实现协议中的方法。
// Objective-C
@interface MyDelegateObject : NSObject <MyDelegate>
@end
@implementation MyDelegateObject
- (void)taskDidFinishWithResult:(NSString *)result {
NSLog(@"Task finished with result: %@", result);
}
@end
### 使用代理:委托对象在适当的时候调用代理对象的方法。这通常是在某个事件发生或任务完成时。
// Objective-C
@implementation MyObject
- (void)completeTask {
// 任务完成后,通知代理
[self.delegate taskDidFinishWithResult:@"Success"];
}
@end
### 设置代理:需要在某个地方设置委托对象的代理为代理对象的实例。
MyObject *myObject = [[MyObject alloc] init];
MyDelegateObject *delegateObject = [[MyDelegateObject alloc] init];
myObject.delegate = delegateObject;
通过以上步骤,我们实现了一个基本的代理模式,其中`MyObject`是委托对象,`MyDelegateObject`是代理对象。
当`MyObject`完成任务时,它会通过代理协议通知`MyDelegateObject`。这种模式使得对象间的通信更加灵活和解耦。
调用代理的过程通常发生在委托对象(即拥有代理引用的对象)内部,当某个特定事件发生或某个条件满足时,委托对象会检查其代理是否实现了相应的协议方法。
如果实现了,则调用之。以下是如何在委托对象中调用代理方法的步骤:
### 1. 检查代理是否存在并响应特定的方法
在调用代理方法之前,应该先检查代理是否存在(即不为`nil`),并且是否响应(即实现了)你打算调用的方法。
这可以通过使用`respondsToSelector:`方法(Objective-C)或者`responds(to:)`方法(Swift)来完成。
// 假设有一个代理方法叫做taskDidFinishWithResult:
if ([self.delegate respondsToSelector:@selector(taskDidFinishWithResult:)]) {
[self.delegate taskDidFinishWithResult:@"任务完成"];
}
### 2. 直接调用代理方法:如果你的代理协议方法是可选的(在Objective-C中使用`@optional`关键字,在Swift中使用`@objc optional`),
你需要按照上面的方式检查代理是否实现了这个方法。如果你的代理协议方法是必须实现的(即非可选的),那么你可以直接调用这个方法,因为你可以确信代理对象实现了这个方法。
// 直接调用非可选的代理方法
[self.delegate taskDidFinishWithResult:@"任务完成"];
### 3. 使用代理模式的注意事项
- **线程安全**:当你在后台线程中调用代理方法时,需要确保这些方法的实现是线程安全的,或者将调用操作切换到主线程上。
- **避免循环引用**:确保代理属性是`weak`,特别是在代理和委托对象之间可能形成循环引用的情况下。
- **清理代理**:在代理对象即将被销毁时,记得将委托对象中的代理引用设置为`nil`,以避免野指针错误。
通过遵循上述步骤和注意事项,你可以有效地在你的应用中实现和使用代理模式,以实现对象间的通信。
delegate的好处
代理(Delegate)模式是一种常用的设计模式,广泛应用于 iOS 开发中,用于在不同的对象之间传递消息或数据。使用代理模式可以带来以下好处:
1. 解耦合
代理可以减少对象之间的直接依赖,使得代码更加模块化。代理对象只需要知道代理协议(Protocol),而不需要知道具体实现细节,这有助于降低代码之间的耦合度。
2. 明确职责分离
通过定义代理协议,可以明确地分离对象的职责,使得每个对象都专注于自己的任务。代理对象负责实现协议中定义的方法,而委托对象则负责在适当的时候调用这些方法。这样可以使代码结构更清晰,职责更明确。
3. 灵活扩展
使用代理模式可以使得代码更容易扩展。当需要修改或添加新的行为时,只需创建一个新的代理对象并实现相应的代理方法,而无需修改原有的代码。这样可以在不影响现有功能的前提下,灵活地扩展应用的功能。
4. 促进复用
代理模式可以促进代码的复用。通过定义通用的代理协议,不同的对象可以共享相同的代理实现,从而避免了代码的重复。
5. 控制对象的访问
代理可以作为一个中间层来控制对对象的访问。例如,可以通过代理来实现懒加载、访问控制、日志记录等功能。这样可以在不修改原有对象代码的基础上,灵活地增加额外的行为。
6. 改善测试和调试
使用代理模式可以更容易地对代码进行测试和调试。可以通过替换代理对象来模拟不同的行为,从而测试应用在不同条件下的表现。这对于单元测试和集成测试尤其有用。
示例
在 iOS 开发中,代理模式经常被用于处理用户交互、数据传递等场景。例如,UITableView 通过 UITableViewDelegate 和 UITableViewDataSource 两个代理协议来分别处理表格的视图展示和数据管理,这样就将表格的展示逻辑和数据逻辑分离开来,提高了代码的可维护性和复用性。
总之,代理模式是一种强大而灵活的设计模式,它通过提供清晰的接口定义和分离对象之间的直接依赖,帮助开发者构建出结构清晰、易于维护和扩展的应用程序。
delegate属性为什么必须要用weak?
如果这个引用是strong的,那么委托者会强引用代理,如果代理同时也强引用委托者(这在实际开发中是很常见的),就会形成一个循环强引用。循环强引用会阻止彼此参与的对象被ARC(自动引用计数)正常释放,导致内存泄漏。
使用weak声明代理属性可以避免这种循环强引用的问题,因为weak引用不会增加对象的引用计数,当代理对象被释放时,weak引用会自动置为nil,从而避免了内存泄漏。
主要是为了避免循环引用导致的内存泄漏,以及确保对象能够在不再需要时被正确释放。这是一种良好的编程实践,有助于提高应用程序的性能和稳定性。
5、函数式编程
如果说面向对象是对数据的抽象,那么函数式编程就是对行为的抽象。
就是允许把函数本身作为参数传入另一个函数,还允许返回一个函数!它的核心是函数。
在解决问题时,使用不可变值和函数,函数对一个值进行处理,映射成另一个值。
关键点: 参数是个函数 返回值也是函数
6、编程范式
面向切面编程(Aspect-Oriented Programming,AOP)是一种编程范式,旨在通过将横切关注点(cross-cutting concerns)从业务逻辑中分离出来,提高代码的模块化程度。横切关注点是指那些散布在代码多个部分,但又不属于主业务逻辑的部分,例如日志记录、权限检查、事务处理等。
AOP通过在运行时动态地将代码注入到特定的方法执行点(称为切点,Join Point),来实现对主业务逻辑的增强或修改,而不需要修改实际的业务代码。这种方式使得开发者可以更加关注于业务逻辑本身,而将一些通用功能(如日志、安全等)模块化和重用。
AOP的关键概念
-
切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。切面可以包括异常处理、日志记录等。
-
连接点(Join Point):程序执行过程中的某个特定点,比如方法的调用或异常的抛出。在AOP术语中,一个连接点总是表示一个方法的执行。
-
通知(Advice):在切面的某个特定的连接点上所采取的行动。通知类型包括“前置通知”、“后置通知”、“环绕通知”等。
-
切点(Pointcut):匹配连接点的断言,在AOP中,通知和一个切点表达式关联,并在满足切点的连接点上运行。
-
引入(Introduction):添加方法或字段到被通知的类。也称为内部类型声明(inter-type declaration)。
-
目标对象(Target Object):被一个或多个切面所通知的对象。
-
织入(Weaving):把切面连接到其他的应用程序类型或对象上,并创建一个被通知的对象。这个过程可以在编译时(如AspectJ)、加载时或运行时进行。
AOP在Objective-C中的实现
在Objective-C中,AOP可以通过几种方式实现,包括使用运行时(Runtime)特性、第三方库(如Aspects)或者手动插入代码。
使用Runtime实现AOP
Objective-C的Runtime系统提供了强大的动态特性,可以在运行时修改类的行为。通过方法交换(Method Swizzling),可以在运行时将一个方法的实现替换为另一个方法的实现,这为AOP提供了可能。
使用第三方库
Aspects是一个流行的Objective-C库,它简化了AOP的实现。通过Aspects,你可以轻松地在任何方法执行前、执行后或替换方法执行来插入自定义的代码。
#import "Aspects.h"
[UIViewController aspect_hookSelector:@selector(viewWillAppear:)
withOptions:AspectPositionBefore
usingBlock:^(id<AspectInfo> aspectInfo) {
NSLog(@"ViewController will appear");
} error:NULL];
总结
面向切面编程提供了一种强大的方式来增强和修改程序的行为,特别是对于那些横切多个模块的功能,如日志、事务管理等。在Objective-C中,可以利用Runtime特性或第三方库来实现AOP,从而提高代码的重用性和模块化。