关于面向对象编程的一些理解,这本书主要看六大原则的部分,书中关于设计模式的内容由于之前的那本《设计模式与游戏完美开发》已经很好的讲解了游戏开发领域的应用,所以不多关注。
单一职责原则SRP:一个类应该只有一个发生变化的原因,如果你能够想到多余一个的动机去改变一个类,那么这个类就具有多于一个的职责。
public interface Modem {
// 连接管理
public void Dial(string pno);
public void Hangup();
// 数据通信
public void Send(char c);
public char Recv();
}
上述代码一个调制解调器所具有的功能,在接口中显示出连接管理和数据通信两个职责。但这目前还不是需要分离职责的理由,如果应用程序的变化会影响对方,那么是应该被分离的;反之变化总是导致两个职责同时变化,那实际上是不必分离的。
public interface DataChannel {
// 数据通信
public void Send(char c);
public char Recv();
}
public interface Connection {
// 连接管理
public void Dial(string pno);
public void Hangup();
}
public class ModemImplementation {
}
尽管ModemImplementation类会混杂DataChannel和Connection,这并不是我们希望的,但对于其他部分来说,通过接口分离我们已经解耦了概念,而这个类除了Main需要知道,谁也不需要知道它的存在。
开闭原则OCP:软件实体(类、模块、函数等)应该是可以拓展的,但是不可修改。如果正确的应用OCP,那么在进行改动时,只需要添加新的代码,而不是改动已有的代码。
拓展和修改看似是冲突的,如何在不改动模块代码的情况下更改它的行为呢?答案就是抽象。由于模块依赖于一个固定的抽象体,所以它对于更改可以是封闭的。同时,通过从这个抽象体派生,可以拓展次模块的行为。遵循OCP的开发过程是困难的,我们很难保证能够预测到变化的发生,如果我们决定接受重构,那么重构的时间来的越早对项目是越有利的。
里氏替换原则LSP,子类必须能够替换掉他们的基类
public class Shape {
public static void DrawShape(Shape s) {
if (s.type == ShapeType.square)
(s as Square).Draw();
else if (s.type == ShapeType.circle)
(s as Circle).Draw();
}
}
public class Circle: Shape{}
public class Square: Shape{}
上述代码典型的违反了OCP,它必须知道Shape类每个可能的派生类,并且每次创建一个从Shape类派生的新类时都必须要更改它。但是从本质上看,开发者采用重写Draw()的方式,以致于子类无法替换Shape(),违反了LSP,这个违法又迫使DrawShape()违反了OCP。LSP是使得OCP成为可能的主要原则之一,正事子类型的可替换性才使得使用基类型表示的模块在无需修改的情况下就可以拓展。
依赖倒置原则DIP:高层次模块不应该依赖于低层次模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
上述启发规则对于某些稳定的类是不太合理的,有些类直接依赖于它也不会造成损害,比如String。然而开发者编写的大多数具体类都是不稳定的,为了不直接依赖于这些不稳定的具体类,通过隐藏到抽象接口的后面,以隔离它们的不稳定性。
public class Button {
private Lamp lamp;
public void Poll() {
if (/*some condition*/)
lamp.TurnOn();
}
}
上述代码Button类直接依赖于Lamp类,当Lamp类发生改变时,Button类会受到影响,这个方案违反了DIP,高层策略没有和低层分离。在这个例子中,背后的抽象是检测用户的开/关指令传给目标对象。至于用什么机制检测,目标对象是什么,都无关紧要。
public interface ButtonServer {
public virtual void TurnOff();
public virtual void TurnOn();
}
public class Lamp : ButtonServer {
}
public class Button {
private ButtonServer bs;
public void Poll() {
if (/*some condition*/)
bs.TurnOn();
}
}
上述代码就很好的倒置了依赖的方向,Lamp依赖于其他类而不是被其他类依赖,知道如何操纵ButtonServer接口的对象都能够控制Lamp。对于面向过程程序设计所创建出来的依赖关系结构,策略依赖于细节,这是糟糕的,这样策略会受到细节改变的影响。DIP是面向对象设计的标志所在,如果依赖关系不是倒置的,它就是过程化的设计。
接口隔离原则ISP:类与类之间的依赖关系应该建立在最小的接口之上,不应该强迫类依赖并未使用的方法。如果一个客户程序依赖于那些它们不使用的方法,那么这些客户程序就面临着由于这些未使用方法的改变所带来的变更。
迪米特原则LoD:也称为最少知识原则,一个对象应当对其他对象有最少的了解
public class Computer{
public void SaveCurrentTask(){ }
public void CloseService(){ }
public void CloseScreen(){ }
public void ClosePower(){ }
}
public class Person{
private Computer c;
public void ClickCloseButton(){
c.SaveCurrentTask();
c.CloseService();
c.CloseScreen();
c.ClosePower();
}
}
上述代码展示了Person想要通过点击按钮关闭Computer的情况,可是由于Computer所有方法都是公开的,Person并不知道电脑关闭的流程,只有Person对Computer关机的流程全部熟悉才能正确的写出ClickCloseButton()这个方法,这违背了LoD。
public class Computer{
private void SaveCurrentTask(){ }
private void CloseService(){ }
private void CloseScreen(){ }
private void ClosePower(){ }
private void Close(){
SaveCurrentTask();
CloseService();
CloseScreen();
ClosePower();
}
}
public class Person{
private Computer c;
public void ClickCloseButton(){
c.Close();
}
}
改进是明显的,现在Person不需要了解任何Computer的内部逻辑就可以写出ClickCloseButton()方法了。
小结:细心的读者会发现最后一个例子,也是明显的违背DIP,具体类Person依赖于具体类Computer,我们可以想象如果Person还想要通过点击按钮关闭电视、空调等其他机器的时候。但是如果明确了这个项目只需要Person控制电脑,绝不可能出现其他设备呢?难道我们还要再抽象出一个mechine类吗?拒绝不成熟的抽象和抽象本身一样重要。