前言:Jakob Nielsen(雅各布·尼尔森)的十大交互设计原则被称为“启发式”,因为它们是广泛的经验法则,而不是特定的可用性指导原则。因此,我们不能把它上升为一种标准,而是应该当做一种经验来学习。
接下来,会通过一些具体的实例来跟大家深度解析尼尔森十大交互设计原则在设计中的用法。
“雅各布·尼尔森(Jakob Nielsen)简介
雅各布·尼尔森(Jakob Nielsen)是毕业于哥本哈根的丹麦技术大学的人机交互博士, 被纽约时报称为“Web 易用性大师”,被 Internet Magazine 称为 “易用之王”。”
原则一:状态可见原则
原则二:环境贴切原则
原则三:用户可控原则(User control and freedom)
原则四:一致性原则(Consistency and standards)
*结构一致性
*色彩一致性
*操作一致性
*反馈一致性
*文字一致性
原则五:防错原则(Error prevention)
原则六:易取原则(Recognition rather than recall)
原则七:灵活高效原则(Flexibility and efficiency of use)
原则八:优美且简约原则(Aesthetic and minimalist design)
原则九:容错原则(Help users recognize, diagnose, and recover from errors)
原则十:人性化帮助原则(Help and documentation)
参考链接:
https://www.uisdc.com/21-app-design-principles
http://www.sohu.com/a/225263949_208871
1.标签导航
2.舵式导航
3.抽屉导航
4.宫格导航
5.组合导航
6.列表导航
7. tab导航
8.轮播导航
9.点聚导航
10.瀑布导航
参考链接:https://www.sohu.com/a/113725930_108633
参考链接:
https://www.jianshu.com/p/52ab0373a1ed
https://www.jianshu.com/p/121ccf669029
无论是 MVC/MVP 还是MVVM,其终极目的都是去耦合。
MVC虽然可以实现功能的分离,但是在模块化越来越细致的今天,由于其耦合性太高,所以催生出了MVCP和MVCVM。
其模式可看成是:MVC+ 模式,及实现Controller内功能的拆分,去耦合化。
相信随着软件的扩展,以后还将有更多的模式出现,所以不必拘泥于形式,适用的就是最好的。
参考链接:
https://www.cnblogs.com/Jax/p/6791886.html
https://www.cnblogs.com/geek6/p/3951677.html
https://www.jianshu.com/p/763278ee047f
一、程序方法设计模式的分类
总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
其实还有两类:并发型模式和线程池模式。用一个图片来整体描述一下:
开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等,后面的具体设计中我们会提到这点。
“个人理解:
一个类只做一类事情。为此,我们应该多用类的引用,而不是类的继承。
最重要的设计原则。Activity和ViewController动辄几千行代码,又难读懂又难修改,就是因为没有遵守这个原则。最著名的例子,就是把switch语句改为简单工厂。
其实就是多态。
依赖于抽象编程。所以你看到所有的设计模式都有抽象类和接口的存在。
有多大胃口吃几碗饭。宽窄接口就是基于此,备忘录模式也是基于此。
也就是不要和陌生人说话。一个类不要引用太多的类。
”
官方详解:
1、单一职责原则
不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,如若不然,就应该把类拆分。
2、里氏替换原则(Liskov Substitution Principle)
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。—— From Baidu 百科
历史替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。
3、依赖倒转原则(Dependence Inversion Principle)
这个是开闭原则的基础,具体内容:面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。
4、接口隔离原则(Interface Segregation Principle)
这个原则的意思是:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。
5、迪米特法则(最少知道原则)(Demeter Principle)
就是说:一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。
最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。
6、合成复用原则(Composite Reuse Principle)
原则是尽量首先使用合成/聚合的方式,而不是使用继承。
工厂模式是创建型模式,使我们常用/常见的模式之一。多用于需要生成复杂对象的地方。用new就可以完成创建的对象就无需使用。工厂模式降低了对象之间的耦合度,由于工厂模式依赖抽象的架构,实例化的任务交由子类去完成,所以有很好的扩展性。
定义:一个用于创建对象的接口,让子类决定实例化哪个类
工厂模式一般也就两大类:
*普通工厂模式:生产具体的产品,创建的产品是类(Class)。
*抽象工厂模式:生产抽象的产品,创建的产品是接口(Interface)。
一开始你可能理解不上来,当你看完这篇文章,你理解了,其实他们并不复杂,
我们先来看一个普通工厂的例子:
public abstract class NokiaPhone {
public abstract void powerOnPhone();
}
先试定义了一个抽象类,抽象出方法powerOnPhone(),模拟手机开机的动作。(ps:抽象类作用简单点说就是抽象出一些方法,需要子类去实现,自己不能实现父类定义方法,子类实现方法。起到一个---把象抽出来---即抽象的作用)
然后我们定义具体的手机:
public class Nokia5200 extends NokiaPhone {
@Override
public void powerOnPhone() {
Log.d("Factory","Nokia5200 power on");
}
}
public class NokiaN97 extends NokiaPhone{
@Override
public void powerOnPhone() {
Log.d("Factory","NokiaN97 power on");
}
}
我们定义了具体的手机Nokia5200和NokiaN97两款手机。并实现了抽象方法powerOnPhone。现在产品定义好了,我们就要定义工厂了,首先我们也抽象出工厂的方法:
public abstract class Factory {
public abstract T createNokia(Class clz);
}
工厂的方法无非就是生产手机,所以我们抽象出来了createNokia方法,现在我们来定义工厂:
public class NokiaFactory extends Factory {
@Override
public T createNokia(Class clz) {
NokiaPhone nokiaPhone = null;
try {
nokiaPhone = (NokiaPhone) Class.forName(clz.getName()).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return (T) nokiaPhone;
}
}
NokiaFactory工厂也很简单就实现了抽象方法createNokia,来生产不同的手机。这里我们使用了反射方法:
nokiaPhone = (NokiaPhone) Class.forName(clz.getName()).newInstance();
这句话的意思是通过类名(ClassName)来实例化具体的类,用的是反射机制来实现。
然后我们来看看我们怎么用工厂生产手机:
NokiaFactory nokiaFactory = new NokiaFactory();
Nokia5200 nokia5200 = nokiaFactory.createNokia(Nokia5200.class);
NokiaN97 nokiaN97 = nokiaFactory.createNokia(NokiaN97.class);
我们用工厂创建了两个手机,一个nokia5200,一个nokiaN97。然后我们开机试试:
nokia5200.powerOnPhone();
nokiaN97.powerOnPhone();
log 输出:
D/Factory: Nokia5200 power on
D/Factory: NokiaN97 power on
至此,一个工厂模式就写完了,可以看到工厂模式的代码结构其实很简单。有的读者可能会想为啥NokiaFactory为啥要用反射呢,其实用反射主要是为了代码简洁,如果不这么写,你可能像下面的代码这样写:
// 方案一
public class NokiaFactoryNokia5200 extends Factory {
@Override
public T createNokia() {
Nokia5200 nokia5200 = new Nokia5200();
return (T) nokia5200;
}
}
public class NokiaFactoryNokiaN97 extends Factory {
@Override
public T createNokia() {
NokiaN97 nokiaN97 = new NokiaN97();
return (T) nokiaN97;
}
}
// 方案二
public class NokiaFactory extends Factory {
@Override
public T createNokia(Class clz) {
Log.d("Factory",clz.getSimpleName());
if (clz.getSimpleName().equals("Nokia5200")) {
Nokia5200 nokia5200 = new Nokia5200();
return (T) nokia5200;
} else if (clz.getSimpleName().equals("NokiaN97")) {
NokiaN97 nokiaN97 = new NokiaN97();
return (T) nokiaN97;
}
return null;
}
}
普通工厂模式小结
1、上面两种方案,一是为每个手机单独创建一个工厂,或者通过带入的class来选择创建都能实现,但是如果手机型号过多,代码就显得很长,当然最好还是用反射的方法,这里只是为了进行一个说明。
2、上面NokiaFactoryNokia5200、NokiaFactoryNokiaN97这种情况也有适合用这种方式的地方。我们下面讲解抽象工厂的时候就会用不同工厂对应不同产品的方式来创建。并非一定是反射的方法。
3、最开始的例子还可以省略抽象方法,抽象方法只是为了更具体化,不过不建议这么做,抽象方法使我们的NokiaPhone更规范。代码可读性也更好。
4、普通工厂模的创建的产品是具体的类,这个例子的产品是NokiaPhone.class,虽然它是一个抽象类,但是它是一个具体的产品。
抽象工厂我们举例一个生产Iphone零件的例子。
我们先定义产品,这里是生产零件,我们定义两个抽象产品,一个CPU,一个电池。这里我把两个接口写在了一起,当然你也可以分开写成两个:
public interface component {
public interface CPU {
void showCpuName();
}
public interface Battery {
void showBatteryCapacity();
}
}
然后我们定义 CPU 的具体产品,一个A9,一个A10:
public class A9 implements component.CPU {
@Override
public void showCpuName() {
Log.d("AbstractFactory","A9");
}
}
public class A10 implements component.CPU {
@Override
public void showCpuName() {
Log.d("AbstractFactory","A10");
}
}
然后是两种电池产品,一个1000ma,一个1200ma:
public class Battery1000ma implements component.Battery {
@Override
public void showBatteryCapacity() {
Log.d("AbstractFactory","battery is 1000ma");
}
}
public class Battery1200ma implements component.Battery {
@Override
public void showBatteryCapacity() {
Log.d("AbstractFactory","battery is 1200ma");
}
}
产品定义好了,我们来定义工厂了,依旧先用抽象类,抽象出工厂类的方法:
public abstract class Factory {
public abstract component.CPU createCPU();
public abstract component.Battery createBattery();
}
(注意:
这里的抽象方法跟抽象工厂模式并无关系,不是因为这里使用了抽象类 而因此叫 抽象工厂模式;而是因为工厂模式生产的产品:一个是component.CPU,一个是component.Battery, 他们两个都是 抽象出来的接口,抽象工厂模式之名 因此而来。
另外说一下Factory是一个抽象类,我们用接口一样可以实现工厂该有的功能,为啥不用接口呢。因为抽象类不能实例化,接口是可以实例化的。我们实例化Factory,再来操作createCPU/createBattery没有实际意义,直接用抽象类就杜绝了这种情况的发生。)
接着我们看具体工厂的实现,这里我们将用不同的工厂对应不同的产品来举例:
public class IPhone6Factory extends Factory {
@Override
public component.CPU createCPU() {
return new A9();
}
@Override
public component.Battery createBattery() {
return new Battery1000ma();
}
}
public class Iphone7Factory extends Factory {
@Override
public component.CPU createCPU() {
return new A10();
}
@Override
public component.Battery createBattery() {
return new Battery1200ma();
}
}
从上面 我们可以看到,IPhone6Factory和Iphone7Factory两个工厂模式,他们创建的产品相同,都是创建CPU和Battery这两个抽象产品。而这两个抽象产品又可以是同接口不同子类实例。
抽象工厂模式小结
1、抽象工厂模式创建的产品是接口,抽象出来的。
2、上面的例子其实跟普通工厂模式例子没太大的差别,除了产品不同,实现的思想都是一样的,只是这里用了不同的工厂对应不同的产品。普通工厂模式也可以这样用。
3、抽象工厂有一个显著的优点是分离接口与实现,用户根本不知道具体的实现是谁,客户仅仅是面向接口编程,使其从产品实现解耦,抽象工厂模式在切换产品类的时候更加灵活容易。
1、现在 文章最开始的那句话 是不是很好理解了
普通工厂模式:生产具体的产品,创建的产品是类(Class)
抽象工厂模式:生产抽象的产品,创建的产品是接口(Interface)
2、工厂模式的优点在上述两个例子的小结中已经阐述,工厂模式的缺点也比较明显,就是不太容易扩展新的产品类,需要去改具体的产品类和工厂类。
3、虽然美中不足,但工厂模式是运用非常广泛的一种模式。值得大家学习使用。
4、工厂模式细分出来还有:“简单工厂模式”“多工厂方法模式”“静态工厂方法模式”,可参考:https://blog.csdn.net/llussize/article/details/80276627
其他参考链接:https://www.jianshu.com/p/13f80d27b7f2
其定义及使用方法同Android一致。另外补充一个简单工厂模式的例子:
简单工厂模式:
简单工厂的生活场景,卖早点的小摊贩,他给你提供包子,馒头,地沟油烙的煎饼等,小贩是一个工厂,它生产包子,馒头,地沟油烙的煎饼。该场景对应的UML图如下所示:
简单工厂模式的参与者:
工厂(Factory)角色:接受客户端的请求,通过请求负责创建相应的产品对象。
抽象产品(Abstract Product)角色:是工厂模式所创建对象的父类或是共同拥有的接口。可是抽象类或接口。
具体产品(ConcreteProduct)对象:工厂模式所创建的对象都是这个角色的实例。
简单工厂模式的演变:
简单工厂模式的优缺点:
参考链接:https://www.jianshu.com/p/847af218b1f0
简单的来说,一个单例类,在整个程序中只有一个实例,并且提供一个类方法供全局调用,在编译时初始化这个类,然后一直保存在内存中,到程序(APP)退出时由系统自动释放这部分内存。
iOS 系统提供的单例类:
UIApplication(应用程序实例类)
NSNotificationCenter(消息中心类)
NSFileManager(文件管理类)
NSUserDefaults(应用程序设置)
NSURLCache(请求缓存类)
NSHTTPCookieStorage(应用程序cookies池)
在哪些地方会用到单例模式?
一般在我的程序中,经常调用的类,如工具类、公共跳转类等,我都会采用单例模式:
重复初始化单例类会怎样?
[[UIApplication alloc]init];
最后运行的结果是,程序直接崩溃,并报了下面的错:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'There can only be one UIApplication instance.'
所以,由此可以确定,一个单例类只能初始化一次。
单例实例在存储器中的位置:
下面的表格展示了程序中中不同的变量在手机存储器中的存储位置:
在程序中,一个单例类在程序中只能初始化一次,为了保证在使用中始终都是存在的,所以单例是在存储器的全局区域
,在编译时分配内存,只要程序还在运行就会一直占用内存,在APP结束后由系统释放这部分内存内存。
(1)单例模式的创建方式:
考虑数据和线程问题,苹果官方推荐开发者使用dispatch_once_t
来创建单例,那么我就采用dispatch_once_t
方法来创建一个单例,类名为OneTimeClass 。
static OneTimeClass *__onetimeClass;
+ (OneTimeClass *)sharedOneTimeClass {
static dispatch_once_t oneToken;
dispatch_once(&oneToken, ^{
__onetimeClass = [[OneTimeClass alloc]init];
});
return __onetimeClass;
}
先说优点:
(1)在整个程序中只会实例化一次,所以在程序如果出了问题,可以快速的定位问题所在;
(2)由于在整个程序中只存在一个对象,节省了系统内存资源,提高了程序的运行效率;
再说缺点:
(1)不能被继承,不能有子类;
(2)不易被重写或扩展(可以使用分类);
(3)同时,由于单例对象只要程序在运行中就会一直占用系统内存,该对象在闲置时并不能销毁,在闲置时也消耗了系统内存资源;
(1)重写单例类的alloc
方法保证这个类只会被初始化一次
我在viewDidLoad
方法中调用单例类的alloc
和init
方法:
[[OneTimeClass alloc]init];
此时只是报黄点,但是并没有报错,Run
程序也可以成功,这样的话,就不符合我们最开始使用单例模式的初衷来,这个类也可以随便初始化类,为什么呢?因为我们并没有获取OneTimeClass
类的使用实例,改进代码:
[OneTimeClass sharedOneTimeClass];
[[OneTimeClass alloc]init];
这是改进后的,但是在多人开发时,还是没办法保证,我们会先调用alloc
方法,这样我们就没办法控制了,但是我们控制OneTimeClass
类,此时我们可以重写OneTimeClass
类的alloc
方法,此处在重写alloc
方法的处理可以采用断言或者系统为开发者提供的NSException类来告诉其他的同事这个类是单例类,不能多次初始化。
//断言
+ (instancetype)alloc {
NSCAssert(!__onetimeClass, @"OneTimeClass类只能初始化一次");
return [super alloc];
}
//NSException
+ (instancetype)alloc {
//如果已经初始化了
if (__onetimeClass) {
NSException *exception = [NSException exceptionWithName:@"提示" reason:@"OneTimeClass类只能初始化一次" userInfo:nil];
[exception raise];
}
return [super alloc];
}
此时在run一次,可以看到程序直接崩到main函数上了,并按照我之前给的提示报错:
但是,如果我们的程序直接就崩溃了,这样的做法与开发者开发APP的初衷是不是又相悖了,作为一个程序员的目的要给用户一个交互友好的APP,而不是一点小问题就崩溃,当然咯,如果想和测试的妹纸多交流交流,那就。。。。。
对于这种情况,可以用到NSObect
类提供的load
方法和initialize
方法来控制,
这两个方法的调用时机:
*load
方法是在整个文件被加载到运行时,在main函数调用之前调用;
*initialize
方法是在该类第一次调用该类时调用;
为了验证load
方法和initialize
方法的调用时机,我们在 Main
函数中打印:
printf("\n\n\n\nmain()");
在OneTimeClass
类的load
方法中打印:
+ (void)load {
printf("\n\nOneTimeClass load()");
}
在OneTimeClass
类的initialize
方法中打印:
+ (void)initialize {
printf("\n\nOneTimeClass initialize()");
}
运行程序,最后的结果是,load
方法先打印出来,所以可以确定的是load
的确是在在main函数调用之前调用的:
这样的话,如果我在单例类的load
方法或者initialize
方法中初始化这个类,是不是就保证了这个类在整个程序中调用一次呢?
+ (void)load {
printf("\n\nOneTimeClass load()");
}
+ (void)initialize {
printf("\nOneTimeClass initialize()\n\n\n");
[OneTimeClass sharedOneTimeClass];
}
这样就可以保证sharedOneTimeClass
方法是最早调用的。同时,再次对alloc
方法修改,无论在何时调用OneTimeClass
已经初始化了,如果再次调用alloc
可直接返回__onetimeClass
实例:
+ (instancetype)alloc {
if (__onetimeClass) {
return __onetimeClass;
}
return [super alloc];
}
最后在ViewController
中打印调用OneTimeClass
的sharedOneTimeClass
和alloc
方法,可以看到Log出来的内存地址是相同的,这就说明此时我的OneTimeClass
类就只初始化了一次。
(2)、对new
、copy
、mutableCopy
的处理
方案一:重写这几个方法,当调用时提示或者返回
OneTimeClass
类实例,请参考alloc
方法的处理;
方案二:直接禁用这个方法,禁止调用这几个方法,否则就报错,编译不过;
+(instancetype) new __attribute__((unavailable("OneTimeClass类只能初始化一次")));
-(instancetype) copy __attribute__((unavailable("OneTimeClass类只能初始化一次")));
-(instancetype) mutableCopy __attribute__((unavailable("OneTimeClass类只能初始化一次")));
此时我在viewDidLoad
中调用new
,然后Build
,编译器会直接给出错误警告,如下图:
这样就解决了单例类被多次初始化的问题;
*注意:单例模式的静态特性导致它的对象的生命周期是和应用一样的,如果不注意这一点就可能导致内存泄漏。
(3)分类Category
的使用
如果在程序中某个模块的业务逻辑比较多,此时可以选择分类Category
的方式,这样做的好处是:
(1)、减少Controller
代码行数,使代码逻辑更清晰;
(2)、把同一个功能业务区分开,利于后期的维护;
(3)、遇到BUG
能快速定位到相关代码;
原则上分类Category
只能增加和实现方法,而不能增加属性,此处请参考美团技术团队的博客:深入理解Objective-C:Category
例如,在我们的APP中,用到了Socket
技术,我在客户端Socket
部分的代码使用了单例模式。由于和服务器的交互比较多,此时采用分类Category
的方式,把Socket
异常处理,给服务器发送的协议,和接受到服务器的协议 用三个分类Category
来实现。在以后的维护中如果业务复杂度增加,或者加了新的业务或功能,可继续新建一个分类。这样既不影响之前的代码,同时又可以保证新的代码逻辑清晰。
参考链接:https://www.jianshu.com/p/829a523c32aa