之前我编写了该文章的第一部分,今天接着写第二部分,希望大家能继续支持。
抽象强调的其实是一种思想和概念,而不依赖于实现的细节。抽象最重要的功能是,可以通过抽象名字去引用对象从而达到隐藏对象中不相关细节的目的。抽象对于建立程序是十分重要的,它说明了一个对象是什么,这个对象能做些什么,而不是解释了这个对象怎么实现了这些内容的,即抽象隐藏了对象实现的具体细节,是对象的代表和说明,因此抽象是管理大型程序复杂性的主要方法。
抽象是通过隐藏不相关的细节从而减少复杂性的,而泛化(Generalization)则是通过将具有相似行为功能的实体组织起来,将这些实体的构造过程进行统一(使用接口)来减少复杂度的。泛化拓宽了程序,它将对象域扩展了,把不同类型的对象组织到了一个对象域下。高级编程语言是通过参数化、多态以及一般化实现泛化的,它强调的是对象之间的相似点。因此,泛化将具有相似行为的实体对象进行分组,并对该组的对象提供一个公共的代表,这个代表将用于识别组中的对象。
抽象和泛化通常一起使用。抽象普遍通过参数化提供高可用性。参数化的意思就是一个实体将被一个名字(接口类型)代替,这个名字起到对象引用参数的作用,通过对这个名字参数的调用,被绑定(binding)的实体对象也将被调用。
抽象类声明的时候添加了abstract关键字,它不能被实例化。它只能是继承了它的类的超类(super -class),抽象类是个概念性的对象,它只能通过子类实现后才能进行完善。一个类只能继承一个抽象类(但可以实现多个接口),抽象类中的抽象方法和属性必须被覆盖和重写(override),可以选择覆盖虚方法或属性(virtual)。
抽象类对于实现框架是十分理想的选择。例如:我们学习下下面的抽象类。请仔细阅读注释,它将帮你了解代码的含义:
public abstract class LoggerBase
{
/// <summary>
/// 字段是private的,所以它只能在类的内部进行使用
/// </summary>
private log4net.ILog logger = null;
/// <summary>
/// 访问级别是protected,所以它只对继承了它的子类可见
/// </summary>
protected LoggerBase()
{
// 私有的对象在构造器中创建
logger = log4net.LogManager.GetLogger(this.LogPrefix);
// 附加的初始化过程
log4net.Config.DOMConfigurator.Configure();
}
/// <summary>
/// 当你定义属性为abstract时,它强制继承的类去重写LogPrefix
/// </summary>
protected abstract System.Type LogPrefix
{
get;
}
/// <summary>
/// 简单的log方法,只对继承类可见
/// </summary>
/// <param name="message"></param>
protected void LogError(string message)
{
if (this.logger.IsErrorEnabled)
{
this.logger.Error(message);
}
}
/// <summary>
/// Public属性暴露给继承的类,其它的类也可以访问
/// </summary>
public bool IsThisLogError
{
get
{
return this.logger.IsErrorEnabled;
}
}
}
之所以将此类作为抽象类是为了给异常的日志处理定义一个通用的框架。这个类将允许所有的子类访问通用的异常日志记录模块,方便地进行日志记录。当你定义了LoggerBase时,你将不会考虑系统的其它模块。但是在你心中肯定会有个思路,即如果一个类要日志记录异常的信息,那么它将继承LoggerBase这个类。用另一句话说就是,LoggerBase这个类提供一个通用的异常日志记录的框架。
让我们来理解一下上面每一行代码的意思吧,就像普通的类一样,抽象类可以包含字段,因此我使用了一个名字为logger的私有字段声明了ILog接口(log4net类库中)。这将允许LoggerBase类控制日志的记录。
抽象类中的构造器的访问级别设为了protected,因为public修饰的构造器对于抽象类来说是没有意义的,因为抽象类是不允许实例化的,所以在此我使用了protected修饰符。
LogPrefix这个抽象属性是十分重要的,它强制要求和保证子类在调用log异常的方法前LogPrefix是有值的(LogPrefix是用来获得产生异常的目标类的细节的)。
方法LogError是protected的,说明所有的子类在继承了LoggerBase后都能使用。在这里你不应该声明为public的,因为任何的类应该在继承了LoggerBase类后才能使用它,从而符合异常记录的框架。
让我们来看看IsThisLogError这个属性为什么是public的吧,因为对于和它进行通信的相关的类,有必要需要让它们知道自身是否已经记录了异常错误,即它对与外部应该是可见的,所以声明为public的。
除了以上这些内容,你同样可以在抽象类中定义virtual方法,virtual方法在抽象类中可以有个默认的实现,而且子类也可以覆盖重写它。
总之,所有的OOP概念应该合理小心的使用,这点很重要,你应该在逻辑上能解释清楚为什么这么用,为什么属性是public标识的或者字段为什么是private的,类为什么是抽象类。
简要的说接口把结构(struct)的定义和具体的实现(implementation)分离了,这个概念是十分有用的。接口十分有用,尤其是当实现(implimentation)经常变化的时候。在一些公司中,很多开发人员都说你应该为所有的类定义接口,这样做是为了系统今后的扩展性,虽然对于总体扩展性而言使用接口是没错,但是我还是觉得这有些极端。
接口可以用来为类定义通用的模板,在接口定义完成后,应该使用一个或多个抽象类去部分实现接口的内容。接口指定了方法的声明(默认是public和abstract)并且可以包含属性(默认也是public和abstract)。接口是由关键字interface标识的,而且接口和抽象类一样不能被实例化。
如果需要一个类实现了一个接口后而并需要实现它的所有方法,那么这种情况应该使用抽象类而不是接口。因为接口强制实现了它的类实现接口声明的所有方法。还有一点需要提出的是,接口是可以继承其它的接口的。
下面的代码将向抽象类LoggerBase提供接口:
public interface ILogger
{
bool IsThisLogError { get; }
}
在C#.NET中,一个类可以实现一个或者多个接口。当类实现了一个接口后,这个类就可以封装到接口中了。如下例所示:如果MyLogger是一个类,它实现了ILogger接口,则可这么使用,代码如下:
ILogger log = new MyLogger();
类和接口是不同的两种类型(概念上),在面向对象理论中,类强调封装性,而接口则是强调抽象。接口和类之间有很明显的不同,所以不能将它们进行有意义的对比,但是抽象类和接口之间很容易混淆,所以它们之间的对比就显得有意义的多。
接口和抽象类虽然看起来很像,但是它们之间有很大的不同,具体如下:
(1)接口使用关键字interface,而抽象类则使用关键字abstract进行声明;
(2)接口没有方法实现,但是实现它们的类必须要实现接口定义的方法;
(3)抽象类中的方法可以被实现和扩展;
(4)接口中只能有方法声明(默认是public和abstract的)和字段(默认是public static);
(5)抽象类的方法在有abstract关键字声明时在抽象类内部不能有实现。
(6)接口可以继承多个其它接口;
(7)抽象类只能继承一个类,但是可以实现多个接口;
(8)抽象类的子类必须覆盖所有抽象方法(方法中有abstract关键字)和选择覆盖虚(virtual)方法;
(9)接口在实现改变的时候仍然可以使用;
(10)抽象类在为子类提供些默认的行为时可以使用;
(11)接口通过隐藏实现的过程从而增加了安全性;
抽象类为子类定义了一些公共的行为;它可以强制使子类实现抽象方法,例如:你有一个应用程序框架,抽象类可以去提供一些默认的服务(例如:日志记录和异常处理),这样的设计可以使得开发人员通过抽象类获得实现的一些指导性帮助。
然而,在实践中,如果你的程序包含一些独有的功能时(例如:任务的startup和shutdown),这样,抽象的基类可以把startup和shutdown方法声明为virtual的,基类只是知道子类需要这些方法,而不知道子类如何实现它们,子类通过实现startup和shutdown方法可以进行初始化和结束任务,当程序运行时,抽象类可以通过调用startup和shutdown方法,执行子类定义的方法内容。
.NET中,一个类可以实现多个接口,而当一个类实现了多个接口时,隐式和显式实现则为安全接口的实现提供了方式。让我们来考虑下,如果不同的接口中存在相同的方法需要被实现时,我们怎么做,如下为定义的接口:
interface IDisposable
{
void Dispose();
}
在下面的代码中我们可以看到Student类通过隐式和显式实现Dispose方法的方式,它们分别为:Dispose和IDisposable.Dispose,这样当两个接口中存在相同方法名时就可以区分了。
class Student : IDisposable
{
public void Dispose()
{
Console.WriteLine("Student.Dispose");
}
void IDisposable.Dispose()
{
Console.WriteLine("IDisposable.Dispose");
}
}
通过从现有的类中扩展而创建新的类的能力叫做继承,继承的UML图如下:
通过上面的UML图可以得出继承的代码如下所示:
public class Exception
{
}
public class IOException : Exception
{
}
上面的例子中,新类(IOException)叫做子类,它将继承现有类(Exception)的成员和方法,现有类被称为基类或超类。子类IOException可以通过添加新的类型和新的方法或者重写父类的方法来实现对父类的功能扩展。
就像抽象和泛化的紧密关系一样,继承和特殊化(specialization)也紧密地关联。把特殊化和泛化一起对比讨论将会对概念的掌握十分有帮助。
特殊化是对象之间一个非常重要的关系,它可以说是一个“is a”的关系。当你说狗狗是哺乳动物,我们指狗狗是(is a)哺乳动物的一种特殊化,狗狗拥有哺乳动物的通用特点,但要与其它哺乳动物区分,狗狗也拥有自身特殊的特点,这就是特殊化。例如:猫咪也是哺乳动物,它和狗狗有一些哺乳动物共通的特点,但它自身也有猫咪特殊的特点,这些特点将帮助它与狗狗区分。如果用面向对象的方式说就是狗狗类和猫咪类都继承自哺乳动物类,它们包含了哺乳动物的公有特点和行为,而狗狗类和猫咪类各自的实现互相有区别,这就是特殊化。
特殊化和泛化的关系就像硬币的两面一样:哺乳动物泛化了狗狗和猫咪之间的共通特点,而狗狗和猫咪通过自身私有的特点特殊化了哺乳动物的类型。
相似的,例如:你可以说IOException和SecurityException 都是Exception的一种类型。它们都有Exception的所有特点和行为,这就是说IOException是一种特殊化的Exception,对于SecurityException同理。我们希望SecurityException和IOException分享一些共通的特点,这些共通的特点就泛化在Exception中,但是为了区分这两个类型,IOException和SecurityException 都拥有一些自身特殊化的特点。用另一句话说就是Exception泛化了IOException和SecurityException 的共通点,而IOException和SecurityException 则特殊化了它们各自的特点和行为。
在OOP中,特殊化通过继承实现,这也是最通用、自然和被广泛接受的实现特殊化的方式。
上面的章节介绍了面向对象里的一些重要的概念,在后续的第三篇文章中,我将继续总结面向对象的一些知识,对于上文一些枯燥乏味以及不是很清晰的解释,由于时间很紧难免存在错误,希望大家能理解和之争,谢谢!