面向对象编程,设计原则,设计模式
面向对象编程
面向对象编程与面向过程编程的区别
4、面向接口依然是面向对象编程,但他在分析系统提取对象的过程中,会首先找寻系统间的边界,并为之定下协议。之后再依据协议来实现各个子系统。用户不需要与他无关的东西,所以接口要细化,粒度要小,但又要把握尺度。
5、面向过程是以代码为主,用代码控制数据,描述解决问题的方法。面向对象是以抽象数据为主,所有处理都围绕数据展开,用数据控制代码,依赖抽象编程。
6、在面向对象编程中,也会局部地运用其他的编程思想,如具体的软件设计过程中以及代码实现里,依然会需要深入某业务的细节,去理解它的执行过程。还有为了更好地发挥多核的优势,已经在普遍使用的函数式编程。
面向对象软件开发的优点
- 代码开发模块化(将大问题拆成小问题,从而可以解决更大的难题),更易维护和修改
- 代码复用
- 增强代码的可靠性和灵活性
- 增强代码的可理解性
面向对象编程语言
程序语言是为了寻求便捷而诞生的,正是因为对便捷的不同需求。 才促成了各种程序语言的发明。
语言设计的取舍:易用性与功能,安全性与效率,稳定性与可扩展性。
C,结构化编程 + 高效 + 功能强大 + 易于学习,诞生于1972年
the c programming language 1978由美国电话电报公司(AT&T)贝尔实验室正式发表了C语言
1989.12 第一个完整的C语言标准,简称“C89”,不过人们也习惯称其为“ANSI C”。
最新标准在2011年12月8日发布,称为ISO/IEC9899: 2011,简称为“C11”。
C++,1979年发明,最初称“带类的C”,1983年正式更名为C++
重视代码执行效率,语言规范相应的非常复杂。
JAVA,1991年构想,92年初步实现,95年公开发布。
目标是实现体系结构中立,而万维网的发展成就了JAVA,因为万维网需要可移植程序。JAVA对网络编程的意义就如同C对系统编程的意义一样:都是改变世界的革命性力量。
C#,1998年12月项目启动,2000年6月发布
早期的C#与JAVA极其相似,但从3.0开始,与java的差异越来越大。
面向对象编程语言知识点
流程控制
使用if...else , while, break,continue 代替 过于强大、结构差、不便阅读的goto。
同时增加 for, switch,以及后续的foreach 让代码更简洁、清晰。
函数
因为随着程序代码变得越来越庞大,把握全局逐渐变得困难起来,同时有可能需要多次用到非常相似的操作,所以促成了函数 的诞生。
将代码的一部分视作一个有机整体,切分出去并为之命名的程序设计机制(便于理解和可重复使用,早期有子程序的称谓)。函数的实现需要在函数调用完成后返回原来的位置,同时函数还会嵌套乃至递归调用,目前普遍使用栈结构来实现。
只有代码行数少了,才能很容易就把握到每一行执行什么功能。减小出错概率。同时有助于理清思路,把握全局。
优秀的代码书写应该是每个函数仅实现一个功能,就如同类的单一职能原则一样,这是一个度的把握。一般要求一个函数的代码行数绝不能超过100行。一般不宜超过一个屏幕高。就如横向上一行代码不宜太长一样(不要产生滚动条)。同时参数列表也不易太长,否则可考虑通过某种数据组织形式整合一下。
以上两项是面向结构编程的基础
结构化程序设计的初衷正是通过导入这些功能从而使代码结构的理解变得简单。
错误处理
常见的处理方法有两种:使用返回值及引用参数传达出错信息。
- C,GO采用这一模式
使用可追加错误类型和可主动触发错误的异常处理机制。
- C++ ,JAVA,C# 使用throw , try ... catch
- JAVA,C# 中 可以使用try ... catch ... finally。使用finally,出口只有一个,实现成对操作的无遗漏执行。
- JAVA 采用检查型异常,要明确声明可能抛出的异常,因为"The Trouble with Checked Exceptions",所以C#未引人。
- swift此前采用c的模式,2.0后引入异常处理机制。
命名和作用域(动态作用域、全局作用域、静态作用域),命名空间
类型
数据如何读取,需要依赖人们为数据附加的类型信息。
基本数据类型:
- CPU对操作数的存放方式:Little_endian(从低字节到高字节)和Big_endian(从高字节到低字节)。
- 以2进制表示10进制,约3.32(log10 / log2)个bit位表示一位。
- 整形:最高位为符号位。负数采用补码的方式,从而将减法转变为加法。(补码:除符号位外 按位取反 再 加 1 )
- 浮点:
以32位为例:
最高位为符号位,
之后8位是表示位数的指数部分(0~255,减去127,得到-127~128,-127表下溢出:0,128表上溢出:无穷小或无穷大,剩下的为位数)
剩下23位为尾数部分,表示小数点以下的部分(通过移动小数点使整数部分变成1后的小数部分)。
如: 1.75 二进制:1.11 ,无需移位, 符号位:0,指数部分 0111,1111(127),尾数部分:110,0000,0000,0000,0000(0.5 + 0.25)
3.875 二进制:1.1111 * 2, 符号位:0,指数部分 1000,0000(128右移一位),尾数部分:111,1000,0000,0000,0000
范围 :2-126 / 3.32 ~ 2127 / 3.32 约10-38~1038
精确度:23/ 3.32 ,6~7位有效数字
64位则指数部分为11位
范围 :-21022 / 3.32 ~ 21023 / 3.32 约10-308~10308
精确度:52/ 3.32 ,15~16位有效数字
自定义类型:结构,联合,枚举,类。使用基本数据类型通过组合定义新的类型。
泛型、模版
动态类型
容器(数据结构):List,Set,Dictionary,……
对象与类
面向对象编程里的 对象是现实世界的模型。归纳并建立模型的方式多种多样,语言不同,选择也不同,类是最方便的,却不是必须的。
JS中通过将函数和变量放入散列,以实现将相关联的函数、变量集中到一起的功能。
函数式语言里,则会采用闭包的形式。
perl 使用包的形式
访问权限:public private protected internal
访问控制的作用:功能上实现封装,代码上解耦
具体:通过隐藏对象的字段来保护其内部的状态
禁止对象之间的不良交互提高模块化
提高代码的可维护性,与外界的交互点越少,越好管理
继承
采用封装来建立抽象数据模型,但仅仅只有封装还不够,事物之间是普遍联系的,还需要继承和多态来描述他们之间的关系
继承可以重用基类代码,需要谨慎使用,否则代码可读性降低,而且扩展困难。使用中要遵守里氏替换原则
C++为多重继承,多重继承使用方便,但会产生冲突,JAVA的解决办法就是只容许单继承,用接口实现多重继承的功能。同时单继承便于统一管理内存。
JAVA1.8后允许接口中添加默认方法,但它的优先级低于基类中的同名方法。
抽象类,接口
抽象类可以有一个抽象方法,而接口则所有方法都是抽象方法,java中的接口可以有静态变量,1.8后又为接口增加了默认方法和静态方法。增强了代码的扩展性,简化了代码实现。但接口与类依然有根本性区别存在,那就是接口不能持有状态,因为它不能含有字段。
多态是指 从基类继承并进行扩展的一系列子类,相互之间是不同的,但对于基类而言,他们依然还是基类的一个实例,即是他们都可以赋值给一个基类的引用变量,同理也可以赋值给抽象类和接口,后者是面向接口编程的基础。
软件设计通用原则
可维护代码的基础包含两个原则: 高内聚和低耦合
高内聚和低耦合是为了支持结构化设计而引入的基本设计原则(1976年 Larry Constantine和Edward Yourdon)。但它们的适用范围非常广。好的面向对象设计也具备高内聚和低耦合的特点。
高内聚原则
衡量一个模块(如类)中的各个方法、库中的各个函数以及方法完成的各个动作表达的逻辑间直接的距离。
- 要求软件模块承担的众多职责关联性很强。
- 高内聚类有助于维护,因为它们倾向于没有依赖。而低内聚使类的目的难以理解,表现为类的职责(方法)上的共同点很少,并且应用不同的不相关活动。
- 高内聚原则 建议创建专注的类,有少量的方法表示逻辑上相关的操作。
低耦合原则
衡量两个软件模块(如类)直接存在的依赖关系。 假设有A和B两个类,如果修改A,就不得不修改B,那么就说它们耦合了。
- 低耦合并不是说两个模块完全隔离,它们依然允许通讯,但它们应该通过一组定义明确和稳定的接口来做。每个模块都应该可以在没有了解另一模块的内部实现的情况下工作。
- 高耦合妨碍测试和重用代码,使理解代码变得困难。
- 高耦合是导致设计僵硬和脆弱的主要原因之一。
关注点分离
关注点分离原则(Separation of Concerns SoC) 由Edsger W. Dijkstra于1974年提出。
- 在系统中每个你想要的特性都是系统的一个关注点(一个方面)。特性、关注点、方面等术语一般可看作是同义词。
- SoC要求每次只把注意力放在一个具体的关注点上。
- 关注点分离是实现高内聚低耦合的一个重要途径。
eg:
1、MVC模式的设计理念就是分离业务逻辑、可视化界面的数据呈现、UI处理逻辑这三个关注点。
2、数据库的读写分离
3、查询集合操作
IList<Product> products = Product.GetSampleProducts(); foreach (var product in products) { if(product.Price > 100) { Console.WriteLine(product); } } //可调整为 foreach (var product in products.Where(p => p.Price > 100)) { Console.WriteLine(product); }
隔离
Soc具体通过使用模块化代码依据大量使用信息隐藏来实现。模块化编程鼓励为每个重要特性使用单独模块。模块包含内部信息供自己使用。只有模块的公共接口才是对其他模块开放的,用于与其他模块通讯。内部数据要么不暴露,要么封装起来,有选择地暴露。接口的实现包含模块的行为,但其细节对其他模块来说不可知,也不可访问。
这种封装隔离是保障高可维护性的常见方式。
面向对象设计6大原则
合成复用原则(Composite Reuse Principle)
组合/聚合(has) 和继承(is) 是在面向对象中存在的两种重用已有实现和设计的方法,合成复用原则强调的就是在重用已有设计和实现时要优先考虑组合/聚合。
原因:
- 继承会破坏基类的封装,如果基类发生变更,子类也要随之受到影响,而且继承来的方法是死的,虽然可以重写,但无法在运行时发生改变,不具备灵活性,扩展不便。
- 继承不但会继承基类的代码,也会基础基类的Context。意味着派生类可以在任何接受父类的场景下使用,但是OO语言并不会保证这两个类真的能替换(这是里氏替换原则的目标)。
- 组合/聚合的实现耦合性更低,相对更加灵活。 而且是一种防御性编程,更难引入面向对象中广为人知的一些糟糕实践相关的细微缺陷,如脆弱的基类、虚成员、构造函数等。
选择继承一定要小心慎用,在使用继承时必须严格遵守里氏代换原则。使用继承一般是为了两个目的,一是父类里封装的变量,方法对子类而言确实是基本不可能变更的,使用继承可以减少代码量,也可以在父子之间建立联系。二是提供约束,比如模版方法模式。
//继承 public class User { public virtual object DoWork() { //…… return new object(); } } public class RegisteredUser : User { public override object DoWork() { object o = base.DoWork(); return Process(o); } private object Process(object data) { //…… return data; } }
public interface IUser { object DoWork(); } public class User: IUser { public object DoWork() { //…… return new object(); } } //组合 public class RegisteredUser: IUser { private IUser _User; public RegisteredUser(IUser user) { this._User = user; } public object DoWork() { object o = _User.DoWork(); return Process(o); } private object Process(object data) { //…… return data; } } //聚合 public class RegisteredUser2 : IUser { private IList<User> _Users; public RegisteredUser2(IList<User> user) { this._Users = user; } public object DoWork() { if (_Users != null) { int index = new Random().Next(_Users.Count); object o = _Users[index].DoWork(); return Process(o); } return null; } private object Process(object data) { //…… return data; } } //泛型 public class RegisteredUser<T> : IUser where T : IUser { private T _User; public RegisteredUser(T user) { this._User = user; } public object DoWork() { object o = _User.DoWork(); return Process(o); } private object Process(object data) { //…… return data; } }
迪米特法则(Demeter Principle)
一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。好的类应该是高内聚,低耦合的。
一方面要求自身的知识对方了解越少越好,另一方面要求对方的知识自己知道的越少越好。内部的实现多麽复杂,都和你没关系,我只需要给你几个public方法可以调用就行了。
四层含义:
- 只和亲密的朋友说话,每个对象必然会与其他对象有耦合关系,一个类应该只和与自己有直接联系的其他对象交流。就是不要出现X.getA().getB().getC(),X与C的业务应该委托给A去做,或者X直接与C建立联系,而不是中间中转多次。即 类与类之间的关系是建立在类之间的,而不是方法之间,一个方法尽量不引入一个类中不存在的对象。
- 朋友之间也要有距离,暴露太多方法和变量给对方,二者的关系就太亲密了,耦合关系变得异常牢固,也就把修改变更的风险扩散了。所以需要访问权限的控制。
- 是自己的就是自己的,如果一个方法放入本类中可以,放入其他类中也没错,那么判别标准就是:如果这个类放入本类中,既不增加类间关系,也不会对本类产生负面影响,那就应该放入本类中。
- 谨慎使用Serializable,一端发生变更,另一端必须同步修改。
核心观念:类间解耦,弱耦合。这样类的复用率才可以提高,代价则是会产生大量的中转或跳转类,导致系统复杂度提高。也会为维护带来难度。
需要在高内聚低耦合与结构清晰二者之间的进行权衡。在实际应用中一个类如果需要跳转两次以上才能访问到另一个类,一般就需要想办法重构了。跳转次数越多,系统越复杂,维护越困难。
迪米特法则可以表述为一系列的规则
- 在方法中,一个类的实例可以调用该类的其它方法
- 在方法中,实例可以查询自己的数据,但不能查询数据的数据(译者注:即实例的数据比较复杂时,不能进行嵌套查询)
- 当方法接收参数时,可以调用参数的第一级方法
- 当方法创建了一些局部变量的实例后,这个类的实例可以调用这些局部变量的方法
- 不要调用全局对象的方法。
public class Foo() { public Bar doSomething(Baz aParameter) { Bar bar = null; if (aParameter.getValue().isValid()) { aParameter.getThing().increment(); bar = BarManager.getBar(new Thing()); } return bar; } }
在这个示例方法中调用了方法链:aParameter.getValue().isValid() 和 aParameter.getThing().increment()。为了测试它们,我们需要明确地知道aParameter.getValue() 和 aParameter.getThing() 的返回结果类型,然后才可以在测试中构建恰当的模拟值。
接口隔离原则(Interface Segregation Principle)
- 接口要小,每个接口只负责服务一个子模块或一个业务逻辑
- 要高内聚,减少对外的交互,要求接口尽量少公布public方法,接口是对外的承诺,承诺越少越有利,变更的风险也就越小。
- 要定制服务,有模块就有交互耦合,就需要互相访问的接口,接口设计时需要为各个访问者定制服务,只提供访问者需要的方法,即是要避免使用臃肿接口。
- 已经污染的接口要尽量去修改,若变更风险太大,则采用适配器模式进行转化处理
- 接口需要精心设计,接口粒度太小,导致接口数据剧增,开发人员呛死在接口海洋里;粒度太大,则灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
//门的接口拆分为门和定时门 public interface IDoor { void Lock(); void Unlock(); bool IsDoorOpen { get; } } public interface ITimedDoor { int OpenTimeOut { get; set; } event EventHandler DoorOpenTooLong; } // 电话的接口拆分为 协议管理 和 通话 interface IProtocal { void dial(String number); void hangUp(); } interface IChat { void chat(); }
依赖倒置原则(Dependence Inversion Principle)
程序开发从面向过程到面向对象,已经是从依赖代码控制数据,步入依赖抽象数据控制代码了。而依赖倒转原则则要求我们要更进一步,实现类是对客观事物或概念的抽象,而抽象类,接口都不能被实例化,是更高一层的抽象,称为高级模块。现实生活中,常见事物、理念都是高层依赖低层。而面向接口要求我们低层依赖高层。模块间的依赖通过抽象来发生,实现类 之间不产生直接的依赖关系,而是通过接口、抽象类这样的高层模块来描述。
在swift语言中接口的关键字从interface变更为protocal,更精准地描述了它的新作用,通过抽象描述出来的依赖关系,就像是一份协议,它是对实现的约束。
依赖倒置是开闭原则的基础,依赖倒置没有实现,开闭原则也就是奢望。负面作用是大量应用接口,抽象类会使文件量大大增加,在小项目中它的优点难于体现,反而是增加了工作量,但在大中型项目中却是必须的。
采用依赖倒置原则可以轻松扩展和维护,可以规避一些非技术因素引起的问题,如需求的变更,项目越大,变化的概率也越大。还有人员的变更,如果设计良好,代码结构清晰,人员变化对项目的影响将基本为零,否则很可能是一场灾难。
依赖倒置原则, 对应 IOC原则,对应好莱坞法则。
里氏代换原则(Liskov Substitution Principle)
任何基类可以出现的地方,子类一定可以出现(子类描述的模型集合是父类的一个子集)。只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。
当我们继承一个父类时,可以对这个父类的方法进行重写,也可以重载。
重写时,参数列表与返回值都要与父类一致
重载时,如果要覆盖父类的方法则要求(参数逆变或返回值协变):
1. 方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
2. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
只有满足上面的条件才算是遵循里氏替换原则,调用类时可使用基类或接口,如果不能使用,则违背了该原则。
重写与重载的调用
class Father{ public Collection doSomething(HashMap map){ System.out.println("父类被执行"); return map.values(); } } class Son1 extends Father{ //重写 public Collection doSomething(HashMap map){ System.out.println("子类1被执行"); return map.values(); } } class Son2 extends Father{ //重载,放大父类的输入参数类型 public Collection doSomething(Map map){ System.out.println("子类2被执行"); return map.values(); } } public class MyClass { static void invoker(){ Father f = new Father(); HashMap map =new HashMap(); f.doSomething(map); } static void invoker1(){ Father f = new Son1(); HashMap map = new HashMap(); f.doSomething(map); } static void invoker2(){ Father f = new Son2(); HashMap map = new HashMap(); f.doSomething(map); } public static void main(String[] args) { invoker(); invoker1(); invoker2(); } } // 输出 // 父类被执行 // 子类1被执行 // 父类被执行
重写方法里对参数加了限制条件,也可能产生不符合里氏替换原则的后果
public class User { public virtual void DoSomething(int number) { Console.WriteLine(typeof(User).Name+":"+ number); } } public class RegisteredUser : User { public override void DoSomething(int number) { if (number < 0) { throw new ArgumentException("number < 0"); } base.DoSomething(number); } }
开闭原则(Open Close Principle)
- 一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。从而建立一个稳定,灵活的系统。
- 软件应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。
开闭原则是最基础的一个原则,其他原则都是其的具体形态,也即是其他五个是指导设计的工具和方法,而开闭原则才是精神领袖。开闭原则是抽象类,其他5个是具体实现类。
其重要性表现在4个方面:
- 对测试的影响,已经投产的代码都是有意义的,并且都受系统规则的约束,这样的代码都要经过千锤百炼的测试过程,不仅保证逻辑是正确的,还要保证苛刻条件下(高压,异常,错误)不产生有毒代码。因此有变化提出是,也需要考虑下原有的健壮代码是否可以不修改。仅仅通过扩展来实现变化呢。否则,就需要把原有的测试过程回笼一次。单元测试,功能测试,集成测试,验收测试,太大的人力消耗。扩展增加新的类,只要保证新类正确就可以了。
- 提高复用性:所有逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样的代码才可以复用,粒度越小,被复用的可能性越大。为什么要复用呢?减少代码量,避免相同的逻辑分散在多个角落,避免日后的维护人员为了修改一个微小缺陷或增加新功能而要在整个项目中到处查找相关的代码。怎样提高复用率呢?缩小逻辑粒度,直到一个逻辑不可再拆分你为止。
- 提高可维护性,维护人员的工作不仅仅是修改数据,也包括扩展程序,最方便的是扩展一个类,而最头疼的是修改一个类。不管原有代码写得多么优秀还是多么糟糕。让维护人员读懂原有代码,然后再修改,是一件痛苦的事情。
- 面向对象开发的需求,一切皆对象,但对象是在运动变化的,如何快速响应变化,就需要在设计之初就考虑到所有可能变化的因素,并留下接口,等待可能转变为现实。
具体在实践中的应用:
- 使用抽象约束,抽象没有具体实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化,因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够对扩展开放。包括三层含义:
- 通过接口或抽象类约束扩展,对扩展进行边界限定,不容许出现在接口或抽象类中不存在的public方法。
- 参数类型,引用对象尽量使用接口或者抽象类,而不是实现类。
- 抽象层尽量保持稳定,一但确定即不允许修改。可以扩展。
- 尽量使用元数据来控制程序的行为,减少重复开发。元数据是用来描述环境和数据的数据,就是配置参数,参数可以从文件中读取,也可以从数据库获得。使用最多的是Spring容器。
- 制定项目章程,对项目来说,约定优于配置,就是统一风格,针对某种情况统一用某种实现。
- 封装变化,把相同的变化封装到一个接口或抽象类中,将不同的变化封装到不同的接口或抽象类中。就是要找出预计有变化或不稳定的点,为这些变化创建稳定的接口,准确地讲就是封装可能发生的变化。
在实践中,一旦发现有发生变化的可能或者变化曾经发生过,都需要考虑现有的架构能否可以轻松地实现这一变化。架构师设计一套系统不仅要符合现有的需求,也要适应可能发生的变化,这才是一个优秀的架构。开闭原则是一个终极目标,却也不可能百分百做到。但朝着这个方向努力,可以非常显著地改善一个系统的架构,真正做到“拥抱变化”。
SOLID原则
对象对象的设计原则还有另外一种表述
SRP | The Single Responsibility Principle | 单一责任原则 |
OCP | The Open Closed Principle | 开放封闭原则 |
LSP | The Liskov Substitution Principle | 里氏替换原则 |
DIP | The Dependency Inversion Principle | 依赖倒置原则 |
ISP | The Interface Segregation Principle | 接口分离原则 |
与上一表述的6大原则对比。
多了单一职责原则,少了组合聚合原则和迪米特法则。
单一责任原则(The Single Responsibility Principle)
当需要修改某个类的时候原因有且只能有一个(THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE)。换句话说就是让一个类只做一种责任,当这个类需要承当其他责任的时候,就需要分解这个类。
在类中的一切都与该单一目的有关,即内聚性。这并不是说类只应该含有一个方法或属性。
编码向量
尽量简单、一目了然(Keep it Simple Stupid,KISS 原则)
保持简单化、傻瓜式。但需要的功能都不能缺,简单而不代表简陋。因为人们(包括产品的用户以及服务的用户)通常喜欢简单的,容易学习和使用的东西。
- 不要让人烦,不要让大家做功课。提供一个有用的东西就够了:把你的主要想法用一种方便理解、易于切入的形式呈现出来。如果你的想法很简单,但是描述却弄得十分繁琐、令人失望就不好了。挫折感是吞噬好想法的恶魔。
- 简单很好,但简单并不是少,比如写代码,3行代码能实现的逻辑如果用10行写肯定不好,不够简单,但如果写成了1行,同样会不够简单。因为这行代码包含了太多的内容,因为几个月以后你自己再看,都有想哭的感觉。这是什么意思!这是另外一种复杂,隐藏的复杂,比直观的复杂更害人。
适可而止(You Ain’t Gonna Need It,YAGNI 原则)
YAGNI 原则指的是只需要将应用程序必需的功能包含进来,而不要试图添加任何其他你认为可能需要的功能,因为任何功能的增加和移除都是需要成本的。
在一个软件项目中,往往 80% 的时间花费在 20% 的功能上。
当你准备列出一个项目清单时,试着考虑以下问题:
●通过降低抽象的层级,来实现低复杂度
●根据特性将功能独立出来
●适度接受非功能性需求
●识别耗时的任务,并摆脱它们
不要重复自己(Don’t Repeat Yourself,DRY 原则)
DRY 原则规定,在整个系统中,每一个小的知识块只可能发生一次,且每个知识块必须有一个单一、明确、权威的表征。应该避免保持系统不同部分同步的麻烦。如果要改变任何地方,应该只做一次就够了。用到代码上,就是完成给定操作的代码只写一次。这也是重构的首要目标。
当你正在构建一个大型的软件项目时,你通常会被整体复杂性搞得不知所措。解决复杂性的最基本的策略是将系统分成若干个容易处理的部分。起初,你可能想将系统按组件划分,每个组件代表了一个子系统,其中包含了完成特定功能所需的一切。
组件还可以往下再分,这样复杂性将被降低到单一职责(single responsibility),每个职责可以使用一个类来实现,类包含了方法和属性。方法实现算法,这些算法和算法的子部分是构成软件业务逻辑的最小知识块。你只需要保证这些块不重复即可。
说,别问(Tell,Don't Ask)
软件实体应该包含数据并暴露某些行为。调用的时候只需要跟踪任务是否在允许的时间内完成,事件发生的细节都被委托和封装到接受命令的对象了。
设计模式
创建型
- 单例: 保证一个类只有一个实例,并提供一个访问它的全局访问点。
- 工厂方法:定义一个用于创建对象的接口,但让子类决定实例化哪一个类,Factory Method使一个类的实例化延迟到了子类。
- 抽象工厂: 提供一个接口,用于创建一系列相关或相互依赖的产品家族,而无须指定它们的具体类。抽象工厂的方法可用工厂方法的方式实现。
- 原型: 用原型实例指定创建对象的种类,并且通过拷贝这些原型来创建新的对象。
- 建造者: 将一个复杂对象的构建与他的表示相分离,使得同样的构建过程可以创建不同的表示。
结构型
- 适配器:将一个或多个类的接口转换成客户希望的一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作那些类可以一起工作。
- 装饰器:动态地给一个对象增加一些额外的职责,若要附加扩展功能,Decorator模式提供了比继承更有弹性的替代方案。
- 代理: 为其他对象提供一种代理以控制对这个对象的访问。(就是把对该对象的调用拦截了下来,进行了一些中间操作)。
- 外观: 为子系统中的一组接口提供一致的界面,facade提供了一高层接口,这个接口使得子系统更容易使用。(家庭影院的遥控器)
- 桥接: 将抽象部分与它的实现部分相分离,使他们可以独立的变化。
- 组合: 将对象组合成树形结构以表示部分整体的关系,Composite使得用户对单个对象和组合对象的使用具有一致性。
- 享元: 享元模式以共享的方式高效的支持大量的细粒度对象。享元模式能做到共享的关键是区分内蕴状态和外蕴状态。内蕴状态存储在享元内部,不会随环境的改变而有所不同。外蕴状态是随环境的改变而改变的。
行为型
- 策略:定义一系列的算法,把他们一个个封装起来,并使他们可以互相替换,本模式使得算法可以独立于使用它们的客户。(游戏角色的武器)
- 模板方法:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,Template Method使得子类可以不改变一个算法的结构即可以重定义该算法得某些特定步骤。(具体依赖抽象,高层组件调用低层组件,避免环形依赖)
- 观察者:定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知自动更新。
- 迭代器:提供一个方法顺序访问一个聚合对象的各个元素,而又不需要暴露该对象的内部表示。
- 责任链:使多个对象都有机会处理请求,从而避免请求的送发者和接收者之间的耦合关系。
- 命令:将请求封装为对象,从而使你可以用不同的请求对客户进行参数化,对请求排队和记录请求日志,以及支持可撤销的操作。(队列请求,日志请求)
- 备忘录:在不破坏对象的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
- 状态:允许对象在其内部状态改变时改变他的行为。对象看起来似乎改变了他的类。
- 访问者:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这个元素的新操作。
- 中介者:用一个中介对象封装一些列的对象交互。
- 解释器:给定一个上下文,定义他的配置参数,并定义一个解释器,这个解释器根据上下文的参数数据以特定的程式给出具体的解释呈现。