The Programming Language Idioms

好久不见. 提交于 2020-03-12 13:01:29

The Programming Language Idioms


Click below to go directly to a specific section:


重载与重写 | 静态构造函数 | 只读代理 | 同步代理 | 资源管理 | 构造函数中的虚函数

 强制针对接口编程 | 抗变与协变 | friend interface | ctor vs. setter | 杂项


重载与重写

问题

日常讨论中,术语的不统一带来些许混乱

惯用的表达

overload

重载

函数名称相同,参数不同(严格的定义还有其它一些限制)

静态决议

override

重写(覆写,覆盖,改写)

子类重新定义父类定义过的虚函数(个别语言允许返回值,访问级别可以不同)

动态决议

示例

     class Base {

     }

     class Derived:Base {

     }

     class Client {

         void Test(Base obj){

              Console.WriteLine("base");

         }

         void Test(Derived obj){

              Console.WriteLine("derived");

         }

         static void Main(string[] args) {

              Base obj = new Derived();

              new Client().Test(obj);    //输出“base”

         }

     }

 


静态构造函数

问题

1,在工具类中,通常有一些初始化需要在任何静态方法被调用前进行,如配置信息的读取

2,普通类中的复杂的静态信息,需要在任何实例方法被调用前初始化

我见过的解决方法

1,在每个静态方法中都调用必需的初始化步骤

     public class SomeUtilClass {

         private SomeUtilClass(){

         }

         private static void Init(){

              //....

         }

         public static string GetUID(){

              Init();

              return uid;

         }

          public static string GetConnectionString(){

              Init();

              return connString;

         }

     }

2,在普通构造函数中初始化

     public class SomeMapperClass{

         private static Hashtable types;

         public SomeMapperClass(){

              if(types == null){

                   types = new Hashtable();

                   types.Add("RED", Color.Red);

                   types.Add("GREEN", Color.Green);

                   types.Add("BLUE", Color.Blue);

              }

         }

         public Color GetColor(string color){

              return (Color)types[color];

         }

     }

 

我推荐的解决方法

使用静态构造函数(C#),或静态初始化块(Java)

[C#]

     public class SomeClass {

         static SomeClass(){

              Init();

              types = new Hashtable();

              types.Add(...);

              types.Add(...);

         }

     }

[Java]

     public class SomeClass {

         static{

              Init();

              types = new HashMap();

              types.put("", "");

              types.put("", "");

         }

     }

效果

1,Once,only once

2,定义中对异常处理等有要求,可参考规范

2,多线程时是否有问题,我不清楚,讨论一下


只读代理

问题

对象内部有一个集合,由这个对象来控制其元素的增加删除,但客户需要访问该集合取得自己想要的信息,而对象不可能为所有的客户都提供对应的方法,因此需要返回内部的这个集合,但不允许客户增加或删除其元素

我见过的解决方法

直接返回代表集合的成员引用,仅在文档中要求客户不能增删集合中的元素

     public class SomeClass {

         private List attrs;

         public List GetAttributes(){

              return attrs;

         }

     }

我推荐的解决方法

1,首选语言提供的功能

2,次选类库提供的功能

3,自己包装代理类,或返回深度拷贝,或使用AOP

[C++]

class config

{

public:

     const list & get_attributes(){

         return attrs;

     }

private:

     list attrs;

};

[C#]

     public class SomeClass {

         private IList attrs;

         public IList GetAttributes(){

              return ArrayList.ReadOnly(attrs);

         }

     }

[Java]

     public class SomeClass {

         private List attrs;

         public List getAttributes(){

              return Collections.unmodifiableList(attrs);

         }

     }

效果

1,语言提供的功能可帮助在编译期进行检查,确保程序中连试图增删元素的代码都不存在;但对有意无意的const转型无能为力

2,类库提供的功能可帮助在运行期进行检查,确保程序中试图增删元素的操作都抛出异常


同步代理

问题

为了对象的线程安全引入了同步机制,却使对象在单线程环境下付出了不必要的性能上的代价,曾经的例子如写时拷贝COW

我见过的解决方法

就是视而不见,不做任何处理,使用同步原语

[C#]

     public class SomeClass {

         [MethodImplAttribute(MethodImplOptions.Synchronized)]

         public void Add(string name){

              attrs.Add(name);

         }

     }

[Java]

     public class SomeClass {

         public synchronized void Add(string name){

              attrs.add(name);

         }

     }

 

我推荐的解决方法

参考类库的实现,提供没有同步的原始类,及有同步的代理类;早期的JDK中Vector及HashTable都是同步的类,新的ArrayList及HashMap都不是同步的,Collections提供了静态方法返回同步代理;当在多线程环境中需要更改集合时,使用代理类

[C#,多线程环境中使用同步代理的客户类代码]

     public class SomeClass {

         public SomeClass(IList source){

              attrs = ArrayList.Synchronized(source);

         }

         public void Add(string name){

              attrs.Add(name);

         }

         public void Remove(string name){

              attrs.Remove(name);

         }

     }

[C#,单线程环境中使用同步代理的客户类代码]

     public class OtherClass{

         public OtherClass(IList source){

              attrs = source;

         }

         public void Add(string name){

              attrs.Add(name);

         }

         public void Remove(string name){

              attrs.Remove(name);

         }

     }

[Java,多线程环境中使用同步代理的客户类代码]

     public class SomeClass {

         public SomeClass (List source){

              attrs = Collections.synchronizedList(source);

         }

         public void add(string name){

              attrs.add(name);

         }

     }

[Java,单线程环境中使用同步代理的客户类代码]

     public class OtherClass{

         public OtherClass(List source){

              attrs = source;

         }

         public void add(string name){

              attrs.add(name);

         }

     }

效果

不必为不需要的功能付出额外的代价


资源管理

问题

有时需要精确的控制资源分配和释放的时机,保证资源的异常安全,避免资源泄漏,导致死锁,文件丢失,数据库连接过多等

我见过的解决方法

在缺乏真正的局部对象和析构函数的语言中,try/catch/finally充斥在代码中

使用中间件可帮助解决部分资源管理,如数据库连接等

可能会出现基于AOP的资源管理框架

我推荐的解决方法

在C++中,自动化的资源管理是与生俱来的,即B.S.提出的“资源管理即初始化”(RAII)

在C#中,可使用using+IDispose取得近似RAII的效果

在Java中,我不知道,讨论一下

[C++,RAII,仅仅示例,操作文件应首选std::fstream等]

class File

{

public:

     explicit File(string path){

         pf = fopen(path.c_str(), "rwb");

     }

     ~File(){

         fclose(pf);

     }

     operator FILE* (){

         return pf;

     }

private:

     FILE* pf;

};

[C++,RAII的客户代码,仅仅示例,操作文件应首选std::fstream等]

void test()

{

     File file("auto.txt");

     char buf[256];

     fread(buf, 0, 256, file);//即使这个操作会抛出异常,文件依然会被关闭

}

[C#,仅仅示例]

     public class File:IDisposable {

         private FileStream fs;

         public File(string path){

              fs = new FileStream(path, FileMode.Create);

         }

         public static implicit operator FileStream(File file) {

              return file.fs;

         }

         public void Dispose() {

              fs.Close();

         }

     }

[C#,仅仅示例]

     public class Test{

         void test(){

              using(File file = new File("auto.txt")){

                   //some read, write, etc.

              }

              //文件已经被关闭,即使某步操作抛出异常

         }

     }

效果

1,资源管理自动化,不局限于内存

2,C++中使用模板,可统一定义大部分资源的包装类,目前的C#只能为每种资源定义单独的类,或者使用AOP


构造函数中的虚函数

语言特性

[C++]

虚函数与对象状态有关,与访问权限(public/protected/private)无关

只要子类对象构造出来了,就可以调用重写的方法,不管访问权限

[Java, C#]

虚函数与对象状态无关,与访问权限(public/protected/private/default/internal)有关

只要访问权限允许,就可以调用重写的方法,不管子类对象构造出来没有

后果

[C++]

在基类构造函数/析构函数里调用的方法永远都是基类的实现,不会调到子类;在其它方法里面虚函数永远都是调到子类的覆写实现,不管是不是private

[Java, C#]

在基类构造函数里调用方法,只要子类覆写了该方法,就会调到子类的实现

解决方法

慎重的在构造函数中调用虚函数,尤其是在Java和C#中,至少应该在注释中说明理由


强制针对接口编程

问题

尽管“针对接口编程”做为一条原则已经广为流传,但实际应用中仍然随处可见HashMap,Vector等做为接口参数、返回值传来传去

我见过的解决方法

使用Factory Method返回接口,并最小化具体类构造函数的访问权限,或类本身的访问权限

我推荐的解决方法

Factory Method依然值得推荐,另外可以利用语言本身的特性来避免多写一个Factory Method

在C++中,override一个虚函数时可以任意改变它的访问权限,包括将它由public变为private;有人说这样会破坏封装,但只要语义正确,有意为之,也没什么问题

在C#中,可使用“显式接口成员实现”

在Java中,我不知道,讨论一下,或者用Spring吧

[C++]

class ISomeInterface

{

public:

     virtual void SomeMethod() = 0;

};

 

class SomeClass : public ISomeInterface

{

private:

     void SomeMethod(){

         std::cout << "Subclass\n";

     }

};

 

int main(int argc, _TCHAR* argv[])

{

     SomeClass obj;

     obj.SomeMethod();    //Error

     ISomeInterface& iobj = obj;

     iobj.SomeMethod();   //Ok

     return 0;

}

[C#]

     public interface ISomeInterface {

         void SomeMethod();

     }

     public class SomeClass:ISomeInterface {

         //1,不要写访问修饰符;2,使用方法全名

         void ISomeInterface.SomeMethod(){

              System.Console.WriteLine("Subclass");

         }

}

     public class Test{

         void test(){

              SomeClass obj = new SomeClass();

              obj.SomeMethod();    //Error;

              ISomeInterface iobj = obj;

              iobj.SomeMethod();   //Ok

         }

     }

效果

1,少写一个Factory Method

2,不需要控制构造函数的访问权限


抗变与协变

问题

在override虚函数时,子类有时想要返回或处理与父类函数参数和返回值略微不同的类型,比如假设“动物类”有一个“伴侣”的虚函数,其返回值类型为“动物类”,但子类“兔子”override“伴侣”时,需要把返回值改为“兔子”;假设“鸟类”有一个“进食”的虚函数,其参数类型为“谷类”,但子类“食铁鸟”override“进食”时,需要把参数改为“碱性食物”;这时,除了使用泛型可以解决外,就需要用到抗变与协变

定义

抗变:向父类的方向变化
协变:向子类的方向变化

语言支持

返回值抗变与参数协变会带来明显的类型安全问题,因此,常用的基本是返回值协变与参数抗变;对抗变与协变支持的最全面的是Eiffel,它同时提供了受束泛型来解决返回值抗变与参数协变带来的类型安全问题

[C++]

只支持返回值协变

class Animal{

public:

     virtual Animal const& Spouse() = 0;

}; 

class Rabbit : public Animal{

public:

     Rabbit const& Spouse(){

         return Rabbit(); //return local reference, don't follow me.

     }

};

[C#]

不支持

[Java]

1.5之前不支持,1.5有限度的支持返回值协变;

另外迫于checked exception的蹩脚实现,Java支持异常声明的协变

 

 


friend interface

问题

C#和Java取消了friend关键字,增加了“assembly/package”访问权限;然而,出于代码树合理组织的需要,整个project常常根据功能模块被分解成一些小project,而一些较大的功能模块,通常包含几个相互协作的project,导致了不同的assembly或package;有一些操作,这些相互协作的assemly或package需要能够访问,而不允许其它的assembly或package访问,如各种setter;但现在的assembly/package访问权限无法做到这一点,如果使用public,则所有客户代码均可访问,丧失了安全性

解决方案

根据接口细分的原则,增加一个friend interface,将部分操作转移到friend interface中,对内聚的几个project/assembly/package,使用friend interface作为接口参数或返回值,对其它的project/assembly/package则使用安全的接口

1), 安全接口,供所有客户使用

public interface ActivityInstanceSafeInterface {

    /**

     * 获取分配时间

     */

    Date getAssignTime(); 

    /**

     * 获取提交时间

     */

    Date getCommitTime(); 

    /**

     * 获取激活时间

     */

    Date getActivatedTime();   

    /**

     * 获取最后一次更新时间

     */

    Date getUpdateTime();   

}

2),friend interface,供朋友使用(注意,继承了安全接口)

public interface ActivityInstanceFriendInterface extends ActivityInstanceSafeInterface {

    /**

     * 设置激活时间

     */

    void setActivatedTime(Date date);   

    /**

     * 设置提交时间

     */

    void setCommitTime(Date date);   

    /**

     * 设置分配时间

     */    

    void setAssignTime(Date date);   

    /**

     * 设置最后一次更新时间

     */

    void setUpdateTime(Date date); 

    /**

     * 清除收到的token

     */

    void clearTokens();

    boolean canStart();

    boolean canFinish();

}

3),实现类,实现所有接口

public class ActivityInstance implements ActivityInstanceFriendInterface {

//……

}

 


ctor vs. setter

二者之间的使用场景基本已有定论,这里概述一下

01#

决定对象的有效性(或合法性)的资源(即任何业务逻辑方法第一次调用前必须初始化的属性),应该使用ctor获取

02#

对象生命期内不变的属性,应该使用ctor获取

03#

实际应用中,推荐尽量使用上述两条规则,除非方便性比正确性重要的场合

 


杂项

问题

在代码中发现了一些书籍中曾经提到的小问题,列在这里,提醒一下

条款

01#

请私有化singleton类的构造函数

02#

如果不用子类化代替flag,至少使用enum代替接口常量来表示flag

03#

访问控制(public,private等)是针对类型的,不是针对对象的,同一种类型的对象可以任意访问彼此的私有成员

 


 

【推荐参考资料】

1.C#标准:ECMA-334 : C# Language Specification

2.Java标准:The Java™ Language Specification Second Edition

3.C++标准:ISO/IEC 14882:2003 Programming Languages - C++

4.The C# Programming Language

5.The Java Programming Language

6.The C++ Programming Language

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!