Dubbo之父——梁飞技术博客拾遗(1)
概述
感觉 梁飞 大佬的博客很多意识流的内容很深刻,但是早年看的时候,就是囫囵吞枣的,只觉得很牛很有道理,但是没有特别深入的思考过。
现在从头翻一翻他的博客,会感觉他写出来dubbo不是一蹴而就的,而是长期的思考和积累,厚积薄发的结果。他会认真的推敲、打磨代码细节,吸收、总结自己的代码原则。
其实从06年一路看过来,早在09年去阿里之前,Dubbo的雏形就已经有了。是他基于业务不断思考、总结,为了解决实际问题形成的。 只不过后来到了阿里这个更大的平台后,有了更系统的思考、也放大了影响。从这几篇就可以看到过程:
- https://www.iteye.com/blog/javatar-278790
- https://www.iteye.com/blog/javatar-258066
- https://www.iteye.com/blog/javatar-264509
他的博客也好久没有维护了,06年写到了14年,刚好也是Dubbo2停止维护的时候,应该是Scope变大了没有精力继续维护了。
我感觉那些文章随时会404,最近在整理dubbo源码,顺手把他的文章整理下。一方面现在自己可以跟着想想,一方面后续有时间了也可以再复习复习,毕竟好的内容总是常读常新。
2006到2014,8年的输出,可以看到他在技术上一点一滴的一路走来,看到他思考问题的角度越来越宏观、系统,也可以看到一个自律、勤奋的人物画像。
【梁飞】https://www.iteye.com/blog/user/javatar
整体内容按时间轴,由远及近整理。梁飞大佬的技术栈还是太深了,按个人喜好筛选了Java相关的,没有全部收录。
MeteorTL 是 梁飞 早年造的轮子。一个模板引擎,类似FreeMarker 或者 JSP,后面改名叫:CommonTemplate,再后面又重构改名为:HTTL https://github.com/httl/httl
发布订阅的【监听器】如何设计
2006-12-12 https://www.iteye.com/blog/javatar-38775
在开发桌面程序时,观察者模式(Observer Pattern)是处理 UI 交互的核心。但在实际应用中,关于“感兴趣事件”的订阅粒度控制,往往存在几种方案的博弈。
核心矛盾
在标准观察者模式中,Observer 向 Subject 注册。但当 Subject 存在多个不同事件(如 Click, Move, Close 等),且不同 Observer 的兴趣点存在重叠或部分交叉时,如何管理这种订阅关系?
方案一:分而治之 —— 多聚集订阅(Multiple Collections)
做法: 在 Subject 中维护多个 Observer 列表,每个列表代表一个特定事件。Observer 根据需要注册到不同的列表中。
- 痛点:
- 重复注册: 如果一个 Observer 对两个事件感兴趣,就必须注册两次。
- 管理冗余: Subject 的管理逻辑较繁琐,需要编写多组
addObserver和removeObserver方法。
- 评价: 粒度最细,但维护成本随事件数量线性增长。
方案二:大而全 —— 宽接口模式(Wide Interface)
做法: 将所有可能的事件方法都纳入同一个 Observer 接口。Subject 维护一个统一的订阅者聚集(Collection)。
- 痛点:
- 违背原则: 违反了“最少知道原则(LoD)”。
- 空实现污染: 具体 Observer 实现接口时,必须为那些不感兴趣的事件函数留空。
- 经典案例: Java AWT 事件模型。如
WindowListener接口,即便你只关心窗口关闭,也必须实现激活、最小化等所有方法。 - 评价: 符合 ISP(接口隔离原则)的对立面,导致接口过于臃肿。
方案三:万能接口 —— 参数化模式(Generic Method with State/Type)
做法: 同样只维护一个订阅者聚集,但 Observer 接口简化为只有一个方法。具体事件类型通过参数(如状态位、事件对象)传递。
- 痛点:
- 语义模糊: 单个方法表意不清,丧失了多态的优雅性。
- 灵活性丧失: 传参必须保证所有事件一致,或者使用
Object弱类型传参。 - 逻辑堆砌: 具体 Observer 内部需要大量使用
instanceof判断和if-else分支。
- 经典案例: Eclipse SWT 早期设计。
- 评价: 扩展容易但类型安全性差,逻辑判断的负担从 Subject 转移到了 Observer 内部。
7种坏味道/11种原则/23种模式
2006-12-27 https://www.iteye.com/blog/javatar-41096
每个程序员都应牢记的7种坏味道,11种原则,23种模式
7种设计坏味道
- 僵化性: 很难对系统进行改动,因为每个改动都会迫使许多对系统其他部分的其它改动。
- 脆弱性: 对系统的改动会导致系统中和改动的地方在概念上无关的许多地方出现问题。
- 牢固性: 很难解开系统的纠结,使之成为一些可在其他系统中重用的组件。
- 粘滞性: 做正确的事情比做错误的事情要困难。
- 复杂性(不必要的): 设计中包含有不具任何直接好处的基础结构。
- 重复性(不必要的): 设计中包含有重复的结构,而该重复的结构本可以使用单一的抽象进行统一。
- 晦涩性: 很难阅读、理解。没有很好地表现出意图。
11种原则 - Principle
类原则(SOLID)
【S】单一职责原则 - Single Responsibility Principle(SRP)
就一个类而言,应该仅有一个引起它变化的原因。
职责即为“变化的原因”。
【O】开放-封闭原则 - Open Close Principle(OCP)
软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改。
对于扩展是开放的,对于更改是封闭的.
关键是抽象.将一个功能的通用部分和实现细节部分清晰的分离开来.
开发人员应该仅仅对程序中呈现出频繁变化的那些部分作出抽象.
拒绝不成熟的抽象和抽象本身一样重要.
【L】里氏替换原则 - Liskov Substitution Principle(LSP)
子类型(subclass)必须能够替换掉它们的基类型(superclass)。
【I】接口隔离原则 (Interface Segregation Principle, ISP)
不应该强迫客户依赖于它们不用的方法。 接口属于客户,不属于它所在的类层次结构。
多个面向特定用户的接口胜于一个通用接口。
【D】依赖倒置原则 - Dependence Inversion Principle(DIP)
抽象不应该依赖于细节。细节应该依赖于抽象。
Hollywood原则: “Don’t call us, we’ll call you”.
程序中所有的依赖关系都应该终止于抽象类和接口。
针对接口而非实现编程。
任何变量都不应该持有一个指向具体类的指针或引用。
任何类都不应该从具体类派生。
任何方法都不应该覆写他的任何基类中的已经实现了的方法。
包内聚原则
重用发布等价原则(REP)
重用的粒度就是发布的粒度。
共同封闭原则(CCP)
包中的所有类对于同一类性质的变化应该是共同封闭的。 一个变化若对一个包产生影响, 则将对该包中的所有类产生影响, 而对于其他的包不造成任何影响。
共同重用原则(CRP)
一个包中的所有类应该是共同重用的。 如果重用了包中的一个类, 那么就要重用包中的所有类。
相互之间没有紧密联系的类不应该在同一个包中。
包耦合原则
无环依赖原则(ADP)
在包的依赖关系图中不允许存在环。
稳定依赖原则(SDP)
朝着稳定的方向进行依赖。 应该把封装系统高层设计的软件(比如抽象类)放进稳定的包中, 不稳定的包中应该只包含那些很可能会改变的软件(比如具体类)。
稳定抽象原则(SAP)
包的抽象程度应该和其稳定程度一致。
一个稳定的包应该也是抽象的,一个不稳定的包应该是抽象的.
其它扩展原则
BBP(Black Box Principle)黑盒原则
多用类的聚合,少用类的继承。
DAP(Default Abstraction Principle)缺省抽象原则
在接口和实现接口的类之间引入一个抽象类,这个类实现了接口的大部分操作.
IDP(Interface Design Principle)接口设计原则
规划一个接口而不是实现一个接口。
DCSP(Don’t Concrete Supperclass Principle)不要构造具体的超类原则
避免维护具体的超类。
迪米特法则
一个类只依赖其触手可得的类。
23种设计模式
创建型
- Abstract Factory(抽象工厂模式) -> (简单工厂模式)
- Factory Method(工厂模式)
- Builder(生成器模式)
- Singleton(单件模式) -> (多例模式)
- Prototype(原型模式)
结构型
- Adapter(适配器模式)
- Bridge(桥接模式)
- Composite(组合模式)
- Decorator(装饰模式)
- Facade(外观模式,门面模式)
- Flyweight(享元模式) -> (不变模式)
- Proxy(代理模式)
行为型
- Chain of Responsibility(职责链模式)
- Command(命令模式)
- Interpreter(解释器模式)
- Iteartor(迭代器模式)
- Mediator(中介者模式)
- Memento(备忘录模式)
- Observer(观察者模式)
- State(状态模式)
- Strategy(策略模式)
- TemplateMethod(模板方法模式)
- Visitor(访问者模式)
人生的三十五个好习惯
2006-12-30 https://www.iteye.com/blog/javatar-41667
- 不说”不可能”三个字.
- 凡事第一反应:找方法,而不是找借口.
- 遇到挫折对自己大声说:太棒了!
- 不说消极的话,不落入消极情绪,一旦出现立即正面处理
- 凡事先订立目标,并且尽量制作”梦想版”.
- 凡事预先作计划,尽量将目标视觉化.
- 工作时间.每一分,每一秒都做有利于生产的事情.
- 随时用零碎的时间(如等人、排队等)做零碎的事情.
- 守时.
- 写下来,不要太依靠脑袋记忆.
- 随时记录灵感.
- 把重要的观念,方法写下来,并贴起来,以随时提示自己.
- 走路比平时快30%,走路时脚尖稍用力推进,肢体语言健康有力,不懒散,萎靡.
- 每天出门照镜子,给自己一个自信的微笑.
- 每天自我反省一次.
- 每天坚持一次运动.
- 听心跳一分钟,指在做重要事情前,疲劳时,心情烦躁时,紧张时.
- 开会坐在前排.
- 微笑.
- 用心倾听,不打断对方说话.
- 说话时声音有力.感觉自己声音似乎能产生有感染力的磁场.
- 说话之前,先考虑一下对方的感受.
- 每天有意识,真诚地赞美别人三次以上.
- 及时写感谢卡,哪怕是用便笺写.
- 不用训斥,指责的口吻跟别人说话.
- 控制住不要让自己做出为自己辩护的第一反应.
- 每天做一件”分外事”.
- 不管任何方面,每天必须至少做一次”进步一点点”.
- 每天提前15分钟上班,推迟30分钟下班.
- 每天在下班前用5 分钟的时间做一天的整理工作.
- 定期存钱.
- 节俭.
- 时常运用”头脑风暴”.
- 恪守诚信,说到做到.
- I am the best one!!!
AppFuse改造之Struts框架隔离
2007-01-15 https://www.iteye.com/blog/javatar-47584
进入新的项目组,
checkout项目下来,
看了一下其整体结构与代码,哎,比较乱。
经过我的建议,部门经理让我对该项目进行全面重构。
首先,此项目是以AppFuse作为基础的,还记得robin说它就一toy
选择的模板是iBatis + Spring + Struts
我的第一步工作是要隔离Struts。
Struts是老牌的MVC实现,那个年代IoC和轻量级还没现在流行,框架侵入性也没有得到重视。
所以Struts的实现让应用程序严重依赖它:
- 所有控制器都必须继承Action类
- 所有数据封装类必需继承ActionForm
- 控制器方法execute必需返回ActionForward,与Struts藕合
- 控制器方法execute的参数ActionMapping,与Struts藕合
- 控制器方法execute的参数HttpServletRequest,HttpServletResponse,与Servlet容器藕合
- 由于Action类是Struts管理的,不支持Service类的IoC注入,除非将控制权委托给IoC容器,再配一遍(如:org.springframework.web.struts.DelegatingActionProxy)。
目标:
- 控制器不继承任何类,也不实现任何接口
- 数据封装Form类应为简单的POJO,不要继承ActionForm
- execute返回值可以是任意对象(包括基本类型和void), 标准返回String,即forward的索引值, 如果返回其它类型对象就调用其toString。 如果返回类型为void或返回值为null,forward到默认的”success”
execute只传入POJO的Form, 如果该动作不需要Form数据,也可以保持空的参数列表。 如果有多个参数,第一个参数为Form(作为传入,也作为传出,这个是struts已经实现的规则),后面的都为传出对象,必需保证为POJO,传出对象会根据struts的action配置的scope,放入相应域。
- 支持IoC注入Service,即然IoC,当然不能依赖某个具体IoC容器,没有Spring一样运行。要不然会被ajoo一顿臭骂,什么什么? IoC还:容器类.getXXX()?
- 当然,还要实现一个线程安全的容器类,持有与Servlet相关的信息,
这样,若有特殊要求需要访问HttpServletRequest,HttpServletResponse 则可以通过:容器类.get当前线程容器().getRequest()方式获取。
效率自查
2007-04-13 https://www.iteye.com/blog/javatar-70656
遵循效率自查,问自己几个问题:
我当前的职责是什么?
我现在在干什么?
做的事情符合职责规范吗?
做事遵循的原则是什么?
做事的方法是什么?
方法是最具效率的吗?
需不需要请教他人?
自己是不是在哪里应该提高?
架构师警钟(一)
2007-04-15 https://www.iteye.com/blog/javatar-71290
如果你读到某种东西,它使你突然一惊或心烦意乱,那么说明你的构想和架构都已经落伍了。
如果软件开发周期结束的时候没有一堆需要解决的问题和功能特性,这很清楚地说明你遗漏了某些东西,你的构想没有超越当前的发布。
当发现某个小组极度缺人手时,说明开发节奏被破坏了。
总是应该用架构的半成品来改进架构。
架构师必须不停的考虑架构的客户如何变化,竞争形势如何变化,运行环境是什么样的,且要不断的验证,总结。
见识下用if…else搞定工作流
2007-04-17 https://www.iteye.com/blog/javatar-71978
接手XX移动项目的流程模块,好头痛,哎…
整个工作流没有用JPBM之类的引擎,从头写出来的。
代码乱得足以体现维护者的价值…
流程是出了名的状态多,状态切换多,易变性大的模块
典型的状态模式,状态机,规则引擎用武的地方,
但这位大牛却用近千行的if…elseif…else搞定,
为了取得流程相关状态,用5层子查询,一页半A4纸的SQL语句查询。
怀念阵亡的多态…
漫谈创业和管理-程序员5大思维障碍
2007-04-24 https://www.iteye.com/blog/javatar-73920
程序员是最容易创业的,或者说是创业成本最低的职业。只要有一台电脑和投入自己的时间,就可以写出畅销天下的软件,这是每个程序员的梦想。更何况世界首富常年以来就是程序员出身的比尔盖茨,这也刺激了更多的程序员走上创业之路。
可是等到真的开始创业,才发现这条路并不容易.由于创办CSDN网站和《程序员》杂志的原因,接触了大量的技术创业者,或者从技术转向管理的程序员。我发现真正程序员创业成功的例子非常罕见,我自己也曾经创业三次,经历了很多的挫折和失败。
我总结了一下,由于程序员的思维习惯给创业或者管理带来的障碍:
为什么要谈管理,因为真正创业做企业,靠一个人是不行的,必须有团队,团队如何管理就是第一步创业的挑战
程序员思维定式:
机器思维 优秀的程序员最擅长和电脑程序打交道,并通过代码去控制反馈。 而管理需要和人打交道,需要收集人的反馈。电脑是按逻辑来执行的,而人却要复杂很多, 特别是团队中有女性成员,挑战难度就更大。 由于长期和电脑接触,很多程序员缺乏和别人沟通的技巧,或者说情商相对较低。 这在管理上是比较致命的缺点。
BUG思维 优秀的程序员追求完美,看自己或者别人代码时第一反应是看什么地方可能有BUG, 管理时如果带着BUG思维,就会只看到别人的不足和错误,而不去表扬其有进步的地方。 (完美思维的坏处还有一个,就是过于关注细节)如果方向和前提有问题,过于关注细节反而会带来延误
工匠思维 程序员靠手艺吃饭,创业总是会碰到各种困难,在碰到困境的时候程序员出身的创业者是有退路的,大不了我再回去写程序搞技术好了。 创业最需要的就是坚持,需要一种永不言弃的精神气,不能坚持到底,也就不能收获果实。
大侠思维 以技术创业起家的容易迷信技术,忽视市场,忽视管理,总以为只有自己的是最好的。遗憾的是技术变迁实在太快,一时的先进不能代表永远的先进。先进的技术也不一定就是致胜的法宝。
边界思维 程序员设计代码和系统时,常常会考虑要处理边界和异常。反映到思维习惯上,就是遇到问题,就会全面的思考各种情况。这是很好的优点,但做事业时,这有时候反而会是缺点。 上面五类有不少具体例子,大家也可以看看自己的思维习惯里面是不是这样?
习惯是很难改变的,最好的处理方式是找到搭档,能弥补自己的不足,这样成功的概率才会加大。HP, Apple Microsoft, Oracle,Adobe, 都是两个主要创始人搭档创业成功的。
API使用接口导出
2007-05-15 https://www.iteye.com/blog/javatar-80191
一般的构件都会围绕一个主体对象开展,
如: Hibernate的Session, Spring的Bean等
通常都需要一个配置文档, 一个建造主体对象的工厂
Hibernate:
1
2
3
Configuration config = new Configuration().config(new File("hib.cfg.xml")); // 配置
SessionFactory sessionFactory = config.buildSessionFactory(); // 创建
Session session = sessionFactory.openSession(); // 使用
Spring:
1
2
3
Resource resource = new ClassPathResource("bean.xml"); // 配置
BeanFactory factory = new XmlBeanFactory(resource); // 创建
MyBean myBean = (MyBean)factory.getBean("myBean"); // 使用
Hibernate 使用建造者模式, 封装了工厂的具体实现,很多框架都使用这种方式导出(如:FreeMarker)。
Spring 直接new工厂的具体实现, 但多态性更强些,便于扩展实现。
MeteorTL当前选择的是建造者模式:
1
2
3
4
Configuration config = new Configuration(); config.loadConfig("template.xml"); // 配置
// config.addDirective("xxx.xxx.XXXDirective"); // 可编程控制配置
TemplateFactory factory = config.buildTemplateFactory(); // 创建
Template template = factory.getTemplate("index.mtl"); // 使用
如果模仿Spring
1
2
3
4
Resource resource = new ClassPathResource("template.xml"); // 配置
TemplateFactory factory = new XmlTemplateFactory(resource); // 创建
// factory.addDirective("xxx.xxx.XXXDirective"); // 可编程控制配置
Template template = factory.getTemplate("index.mtl"); // 使用
哪种API使用接口更合理?
当然还有更多其它方式,欢迎讨论。
FreeMarker在TemplateLoader的设计缺陷
2007-05-16 https://www.iteye.com/blog/javatar-80197
在设计MeteorTL的TemplateLoader时,借鉴了下FreeMarker,
FreeMarker的TemplateLoader:
1
2
3
4
5
6
7
8
9
10
11
public interface TemplateLoader {
public Object findTemplateSource(String name) throws IOException;
public long getLastModified(Object templateSource);
public Reader getReader(Object templateSource, String encoding) throws IOException;
public void closeTemplateSource(Object templateSource) throws IOException;
}
初读此接口很费解,这是一个留给扩展者的SPI,但却让扩展者要看完那长篇的注释才能明白那个Object是干什么的。
TemplateLoader的第一目标是要通过模板name拿到Reader,
最初的想法肯定是:
1
2
3
4
5
public interface TemplateLoader {
public Reader getReader(String name, String encoding) throws IOException;
}
但实现时,会发现Reader无法持有一些meta数据,如:用于热加载时比较是否更新用的lastModified时间等。
所以freemarker才会有上面的写法,用一个Object(大部分时候是File)传递,也就是 findTemplateSource(String name) 的返回值 Object templateSource 会被框架(接口调用者)通过参数回传给下面的三个函数。以至于此接口的契约依赖于框架的具体实现(即所有此接口的调用者,都应该遵守回传templateSource,而这些都是文档约束的)。
重构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.IOException;
public interface TemplateLoader {
public TemplateSource getTemplateSource(String name) throws IOException;
}
import java.io.IOException;
import java.io.Reader;
public interface TemplateSource {
public static final long UNKOWN_TIME = -1;
public long getLastModifiedTime();
public Reader getReader(String encoding) throws IOException;
public void close() throws IOException;
}
思考要不要用JavaCC作为语法解释器
2007-05-22 https://www.iteye.com/blog/javatar-81792
设计之初就想过这个问题,应该怎么处理语法解释。 是否应基于一个语法引擎,如:JavaCC,ANTLR等。 但觉得MeteorTL( http://www.meteortl.org/ )的语法较统一, 比较简单,所以自已实现了DFA自动机解析。 其中,MeteorTL的指令是独特的DSL, 而指令中的表达式则是基于通用MathDSL扩展的。 JavaCC和ANTLR都算是EBNF-AST语法体系定义的经典实现。 Velocity和FreeMarker都使用JavaCC作为语法解释器 Hibernate则使用ANTLR作为HQL的语法解释器
准备留出策略接口,用自己的DFA作为默认实现。
领域专用语言(DSL)
2007-05-24 09:57 https://www.iteye.com/blog/javatar-82514
原文:DomainSpecificLanguage (http://www.martinfowler.com/bliki/DomainSpecificLanguage.html)
所谓领域专用语言(domain specific language / DSL),其基本思想是“求专不求全”,不像通用目的语言那样目标范围涵盖一切软件问题,而是专门针对某一特定问题的计算机语言。几乎自计算机发明伊始,人们就开始谈论DSL使用DSL了。
Unix社群是一个频繁使用DSL的社群,他们通常称之为小语言或迷你语言。(关于这一传统,Eric Raymond的《Unix编程艺术》有上佳探讨。)要构建一种DSL,按最常见的Unix风格的做法,就是先定义它的语法,然后通过代码生成技术把DSL代码转成一种通用语言代码,或者写一个这种DSL的解释器。Unix有很多工具能让这件事做起来轻松些。我为这类DSL定了一个术语:“外部DSL”。XML配置文件是外部DSL的另一种常见形式。
DSL也是Lisp和Smalltalk社群的一项重要传统,但方式不同,他们不是动手新造一套语言,而是让Lisp或Smalltalk这种通用目的语言换个颜面变成DSL。(Paul Graham的文章《自底向上编程》对此有精彩讲述。)利用编程语言自带的语法结构定义出来的DSL,我称之为“内部DSL”,也叫做“内嵌DSL”。这是种通用策略,不仅适用于Lisp和Smalltalk,用任何语言都可以这么做,我面对问题时总是考虑着用这种策略定义出具有DSL功能的东西来解决,不过Lisp和Smalltalk程序员走得要深远得多。
关于这两类DSL,在我最近的文章《语言工作台(Language Workbench)》中有进一步的例子,我希望DSL能使用得更普遍,文中详细讨论了这两种风格各自的优缺点,还介绍了语言工作台工具的最新进展。
直到出现了一位人物,原本内外分流的DSL走向了一个有趣的汇合,他就是PragDave。沿袭Unix的传统,用本主义程序员们(The pragmatic programmers)老早就是DSL粉丝了(《用本主义程序员》(中译本链接)第十二节对这个话题的讨论引人入胜——我干脆把它称作“用本要义12”好了)。Dave在一次富有见地的访谈里说到,尽管代码生成是他的惯用技术,但在用Ruby编程时很少用到。
我做设计时,经常借构建一套DSL的思路来类推——有意把class和方法设计成DSL的样子。不论用什么语言,我都尽量这么做,如果做不到,我就乐得转用代码生成技术了。在我们ThoughtWorks公司的较大型系统上,代码生成以及类似的技术使用得非常普遍。
什么时候需要把DSL和主语言划清界线,我认为这个问题的答案因主语言而异,用Smalltalk时我几乎从没感觉有必要分离出一种DSL来,而这种需求在用C++/Java/C#时则非常常见。
因此,我认为有的语言适合设计内部DSL,有的不适合。适合的是那种“一条道跑到黑”的风格简约的语言,它们在某一方面比其他传统语言走得更远更纯粹(例如Lisp的函数式风格,Smalltalk的“对象-消息”思想),这是我分析Lisp和Smalltalk得出的结论。再看Ruby,它比前两者更常规化一些,也比它们都庞大,但仍不失为一门用来构建内部DSL的好语言。
这么看来,语言设计者需要对语言的精练程度有一个良好的决策,既要保证常规性内容可以轻松地表达,又要为原本费神的复杂东西提供舒适的语法支持。总之,我认为这是非常重要的一点。我喜欢用Smalltalk和Ruby的程度比喜欢用Java或C#的程度高那么多,其原因我总是觉得难以言传,最常听到的解释是静态类型与动态类型的区别所致,但我总觉得这个说法并没有抓住要害,更接近两者区别本质的是它们对构建内部DSL友好程度的差异。
译注:Brian W. Kernighan与Rob Pike合著的《程序设计实践(The Practice of Programming)》第9章也是一份关于DSL的极好的参考资源。
扩展接口的思考
2007-06-06 https://www.iteye.com/blog/javatar-87374
在设计MeteorTL时, 模板指令,表达式操作符的可扩展性,一直没有找到好的方案。 由于扩展是由第三方实现的,所以一般采用SPI(Service Provide Interface)接口的方式。 在留出SPI接口时,引擎需要传递编译期数据及运行时数据给扩展方, 运行时数据肯定是用callback函数的参数列表传递, 但编译期数据是其应持有的状态,所以必需在初始化时给予。 以指令为例:
1
2
3
4
5
6
7
8
9
10
public interface Directive extends Serializable {
public void interpret(TemplateContext templateContext) throws DirectiveException;
public String getName();
public Expression getExpression();
public NestDirective getParentDirective();
}
其中,
templateContext 为运行时数据,在callback函数中传过去。
name, expression, parentDirective 为编译期数据,
是在引擎将模板解析成树时,应该注入的。
这里的问题主要在于编译期数据怎么传给扩展者。
下面是我暂时想到的方案。
方案一:
约定构造函数,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class OutDirective implements Directive {
private String name;
private Expression expression;
private NestDirective parentDirective;
public OutDirective(String name, Expression expression, NestDirective parentDirective) {
this.name = name;
this.expression = expression;
this.parentDirective = parentDirective;
}
public void interpret(TemplateContext templateContext) throws DirectiveException {
......
}
public String getName() {
return name;
}
public Expression getExpression() {
return expression;
}
public NestDirective getParentDirective() {
return parentDirective;
}
}
这种方法契约性太差,实现者可能要在看了文档后才会知道构造函数的约定。
方案二:
指定初始化函数,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public interface Directive extends Serializable {
public void init(String name, Expression expression, NestDirective parentDirective);
public void interpret(TemplateContext templateContext) throws DirectiveException;
public String getName();
public Expression getExpression();
public NestDirective getParentDirective();
}
public class OutDirective implements Directive {
private String name;
private Expression expression;
private NestDirective parentDirective;
public void init(String name, Expression expression, NestDirective parentDirective) {
this.name = name;
this.expression = expression;
this.parentDirective = parentDirective;
}
public void interpret(TemplateContext templateContext) throws DirectiveException {
......
}
public String getName() {
return name;
}
public Expression getExpression() {
return expression;
}
public NestDirective getParentDirective() {
return parentDirective;
}
}
这个方案在方案一的基础上改的,使用init函数初始化,当然还必需有无参构造函数。
init函数在接口中定义,是为了保证明确的契约,
但也因为如此,接口暴露了init函数,init可能被非法调用,
因此引擎在初始化指令时,就需要包一层代理:
public class ProxyDirective implements Directive {
private Directive directive;
public ProxyDirective(Directive directive) {
this.directive = directive;
}
public void init(String name, Expression expression, NestDirective parentDirective) {
throw new UnsupportedOperationException("此方法为初始化setter,只允许引擎调用!");
}
public void interpret(TemplateContext templateContext) throws DirectiveException {
directive.interpret(templateContext);
}
public String getName() {
return directive.getName();
}
public Expression getExpression() {
return directive.getExpression();
}
public NestDirective getParentDirective() {
return directive.getParentDirective();
}
}
上面两个方案都有个缺陷是,初始化工作都由引擎完成。
一般会将OutDirective.class的Class类元传给引擎,
当引擎解析到out指令时,就会用Class.newInstance()创建一个实例并初始化它作为指令树的一个节点。
这样第三方在实现OutDirective就会受到诸多限制。
最大的坏处,就是对IoC的破坏,
假如,第三方在实现OutDirective时,需要一些配置,如:默认格式化串等
由于实现者没有指令对象的生命周期管理权,根本没法注入依赖(不管是构造子注入还是setter注入等)。
这就会迫使实现者直接取配置或其它辅助类。
如:
1
2
3
4
5
public void interpret(TemplateContext templateContext) throws DirectiveException {
MyConfig myConfig = MyConfig.getConfig("config.xml");
ToStringHandler handler = new NotNullToStringHandler();
......
}
方案三:
使用Handler回调方式
鉴于上面的缺陷,可以将Directive实现为通用类,
通过回调一个Handler接口,
将所有编译期数据和运行时数据都通过callback函数的参数列表传递。
如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public interface DirectiveHandler {
public void doInterpret(TemplateContext templateContext,
String name, Expression expression, NestDirective parentDirective)
throws DirectiveException;
}
public final class Directive implements Serializable {
private String name;
private Expression expression;
private NestDirective parentDirective;
private DirectiveHandler directiveHandler;
public Directive(String name, Expression expression, NestDirective parentDirective, DirectiveHandler directiveHandler) {
this.name = name;
this.expression = expression;
this.parentDirective = parentDirective;
}
public void interpret(TemplateContext templateContext) throws DirectiveException {
directiveHandler.doInterpret(templateContext, name, expression, parentDirective);
}
public String getName() {
return name;
}
public Expression getExpression() {
return expression;
}
public NestDirective getParentDirective() {
return parentDirective;
}
}
这样,因为DirectiveHandler不持有状态,
可以只向引擎供应一个实例,而不是类元,对其构造函数也不再有要求。
如:
1
2
3
DirectiveHandler outDirectiveHandler = new OutDirectiveHandler("xxx.xml"); // 构造子注入
outDirectiveHandler.setConfig("xxx.xml"); // setter注入
....
将IoC组装完成后的实例注册到引擎:
1
2
Map handlers = new HashMap();
handlers.put("out", outDirectiveHandler);
引擎在解析到out指令时,就会new Directive(name, expression, parentDirective, handlers.get(name));
这样,Directive就变成一个final的内部class,不再是SPI接口。
而换成DirectiveHandler作为SPI接口,其实例的生命周期可自行管理。
暂时只想到这里,
如果有更好的方案请指教,谢谢。
契约设计的一些想法
2007-06-14 https://www.iteye.com/blog/javatar-90101
契约设计由来已久,各语言的支持方式与级别不尽相同,
契约元素包括:
前验条件(precondition)
后验条件(postcondition)
不变式(invariant)
这里只考虑前验条件的:
需求定义(require)
保证合法性(ensure)
函数的签名算是需求的最基本定义了。
对于合法性的保证,一般采用断言。
Java在1.4以前,大家只能靠简单的函数封装实现。
如,在函数开头:
1
2
if (param < 0)
throw new IllegalArgumentException("param < 0");
或进行简单封装:
1
Assert.notLessThan(param, 0);
从1.4开始,Java对断言提供语言级别的支持。加入了关键字assert。
如:
1
assert(param < 0);
因为是语言级别的支持,断言可在生产环境中被擦拭掉,以保证性能。
废话就说到这了,我想说的是,
是否应该在需求定义时就尽可能的保证合法性。
也就是在函数签名上保证合法性,而不是断言。
断言信息需要通过文档才能被调用者获悉。
而函数签名能够更明确。
如:
1
2
3
4
setSize(Integer size) {
assert(size > 0);
...
}
改为:
1
2
3
setSize(PositiveInteger size) {
...
}
当然,这样会多出很多类型定义。
而这些语言是否应给予更多便利支持?
泛型就是一种方式:
setUsers(Map users) {
for(Iterator iterator = users.entrySet.iterator(); iterator.hasNext();) {
Map.Entry entry = (Map.Entry)iterator.next();
assert(entry.getKey() instanceof String);
assert(entry.getValue() instanceof User);
}
...
}
这个断言是很费时的。
而用泛型:
1
2
3
setUsers(Map<String, User> users) {
...
}
泛型一定程度上减少了类型定义的烦琐。
契约设计
2007-06-14 https://www.iteye.com/blog/javatar-90125
DbC 元素
先验条件。针对方法(method),它规定了在调用该方法之前必须为真的条件。
后验条件。也是针对方法,它规定了方法顺利执行完毕之后必须为真的条件。
不变式。针对整个类,它规定了该类任何实例调用任何方法都必须为真的条件。
DbC 六大原则
区分命令和查询。
将基本查询同派生查询区分开。 针对每个派生查询,设定一个后验条件,使用一个或多个基本查询的结果来定义它。
对于每个命令都撰写一个后验条件,规定每个基本查询的值。
对于每个查询和命令,采用一个合适的先验条件。
撰写不变式来定义对象的恒定特性
DbC 六大准则
在适当的地方添加物理限制。
先验条件中尽可能使用高效的查询。
用不变式限定属性。
为了支持特性的重定义,用相应的先验条件确保每个后验条件。
将预期发生的变化和框定规则这两种不同的限制分别放置在不同的类中。
有保密性要求,则违背保密性的查询可以在契约中使用,然后被设为私有属性。
Eiffel中的”契约”
契约关系的双方是平等的,对整个bussiness的顺利进行负有共同责任,没有哪一方可以只享有权利而不承担义务。
契约关系经常是相互的,权利和义务之间往往是互相捆绑在一起的;
执行契约的义务在我,而核查契约的权力在人;
我的义务保障的是你的利益,而你的义务保障的是我的利益;
包设计原则
2007-06-23 https://www.iteye.com/blog/javatar-93469
粒度:包的内聚性原则
- 重用发布等价原则(The Release Reuse Equivalency Principle (REP))
- 重用的粒度就是发布的粒度
- 一个可重用的包必须为发布跟踪系统所管理,使我们在新版本发布后我们还可以继续使用老版本
- 一个包中的所有类对于同一类用户来讲都应该是可重用的。
- 共同重用原则(The Common Reuse Principle (CRP))
- 一个包中的所有类应该是共同重用的,如果重用了包中的一个类,就应该重用包中的所有类。
- 一般来说,可重用的类需要与作为该可重用抽象一部份的其它类协作,CRP规定了这些类应该属于同一个包。
- 放入同一包中的所有类应该是不可分开的,其它包仅仅依赖于其中一部份情况是不可能的(不允许的),否则,我们将要进行不必要的重新验证与重新发布,并且会白费相当数量的努力。(一个包依赖于另外一个包, 哪怕只是依赖于其中的一个类也不会削弱其依赖关系)
- CRP倾向于把包做的尽可能的小
- 共同封闭原则(The Common Closure Principle (CCP))
- 包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包中所有类产生影响,而对于其他的包不造成任何影响。
- 这是单一职责原则对于包的重新规定。
- CCP鼓励我们把可能由于同样的原因而更改的所有类共同聚集在同一个地方。将变化限制在最小数据的包中。
- CCP倾向于将包做的尽可能的大。
- CCP有益于维护者(包的作者),而REP和CRP有益于重用者(包的使用者)。
稳定性:包的耦合性原则
- 无环依赖原则(The Acyclic Dependencies Principle (ADP))
- 在包的依赖关系图中不允许存在环。
- 包的依赖关系图应该是一个有向无环图(DAG(Directed Acyclic Grphic))
- 存在环的系统,很难确定包的构建顺序,事实上,并不存在恰当的构建顺序。
- 打破环的第一个方法:依赖倒置原则,使一个包不再依赖于另一个包,而只是依赖于其抽象接口。
- 打破环的第二个方法: 创建一个新包来包含公共依赖部份。
- 稳定依赖原则(The Stable Dependencies Principle (SDP))
- 朝着的稳定的方向进行依赖
- 你设计了一个易于更改的包, 其它人只要创建一个对它的依赖就可以使它变的难以更改,这就是软件的反常特性。通过遵循SDP可以避免这种情况。
- 不稳定性度量:I = Ce / (Ca + Ce). Ca: Afferent Coupling. Ce: Efferent Coupling
- SDP规定一个包的I度量值应该大于它所依赖的包的I的度量值,也就是说,I的度量值应该顺着依赖的方向减少。
- 稳定抽象原则(The Stable Abstractions Principle (SAP))
- 包的抽象程度应该和其稳定程度一致。
- 一个稳定的包同时应该是抽象的,这样,其稳定性就不会导致其无法扩展。一个不稳定的包应该是具体的,这样,因为其不稳定性使得其内部的具体代码易于修改。
- 抽象性度量:A = Na / Nc Na: Number of classes. Nc:Number of abstract classes.
- 创建一个以A为纵轴,I为横轴的坐标图,最稳定,最抽象的包位于左上角(0,1)处, 那些最不稳定,最具体的包位于右下角(1,0)处。
代码的味道
2007-06-29 https://www.iteye.com/blog/javatar-95508
概述
代码的味道是高水平程序员对“好程序“的一种感觉,他们具备一种能力,即使不涉及程序代码的具体实现就能看出你的设计是否合理。
如果代码有“异味“,那么你需要进行Refactorying.
重复代码(Duplicate Code)
即使是一两句代码的重复也需要refactoring,有时候重复不是那么明显,你需要首先进行其他的refactoring才能看到代码重复。排除代码重复是OO软件工程最重要的研究课题之一
长方法(Long Method)
来自于面向过程的思路,即使能够在一页内能够显示的方法也可能太长。
大类(Large Class)
一个类含有太多的责任和行为
参数太多(Long Parameter List)
对象含有状态,不再需要太多的参数。
不一致的变化(Divergent Change)
不要把变化速度不同的东西放在一起。不要把一个方法对每个子类的变化的部分和不变化的部分放在一起。不要把对象中每秒都在变化的实例变量和一个月才变化一次的势力变量放在一起…等等。
Shotgun Surgery
改变影响到太多的类和方法
特性羡慕(Feature Envy)
对其他对象中的数据太感兴趣了
数据从(Data Clumps )
一块数据到处一起使用,他们应该有自己的类
原始类型困扰(Primitive Obsession)
用类代替原始数据类型
开关语句(Switch Statement)
面向对象由其他办法来处理这些依赖于类型的方法。
并行继承层次(Parallel Inheritance Hierarchies )
有时候有用但有时候不必要
惰类(Lazy Class)
不足以自己成为一个类,应该排除
投机通则(Speculative Generality )
不要太多考虑为将来而建立的灵活性
消息链(Message Chain )
硬性把客户和导航结构相耦合
中间人(Middle Man )
如果他所有的事情就是在做分派,那么应当删除。
不合适的亲密(Inappropriate Intimacy)
限制对其他类内部结构的知识和了解。
不完整的库类(Incomplete Library Class )
某些时候必须扩展一增加所需的功能
数据类(Data Class )
应当添加任务和行为来处理它的数据
被拒绝的遗产(Refused Bequest )
子类很少利用父类给予它们的东西
注释(Comments )
注释是说明why而不是what的好地方。
OOD 启思录
2007-06-29 https://www.iteye.com/blog/javatar-95509
这是一份整理自《OOD 启思录》(Object-Oriented Design Heuristics)的 Markdown 格式版本。我对文本进行了层级化处理,并保留了原书的页码索引。
作者: Arthur J. Riel
译者: 鲍志云
“你不必严格遵守这些原则,违背它们也不会被处以宗教刑罚。但你应当把这些原则看成警铃,若违背了其中的一条,那么警铃就会响起。”
—— Arthur J. Riel
类与接口的基础设计
- 所有数据都应该隐藏在所在的类的内部。 (p13)
- 类的使用者必须依赖类的共有接口,但类不能依赖它的使用者。 (p15)
- 尽量减少类的协议中的消息。 (p16)
- 实现所有类都理解的最基本公有接口。 (p16) 例如:拷贝操作(深拷贝和浅拷贝)、相等性判断、正确输出内容、从 ASCII 描述解析等。
- 不要把实现细节(例如放置共用代码的私有函数)放到类的公有接口中。 (p17) 注:如果类的两个方法有一段公共代码,应当创建一个私有函数来放置这些公共代码。
- 不要以用户无法使用或不感兴趣的东西扰乱类的公有接口。 (p17)
- 类之间应该零耦合,或者只有导出耦合关系。 (p18) 即:一个类要么同另一个类毫无关系,要么只使用另一个类的公有接口中的操作。
封装与内聚
- 类应该只表示一个关键抽象。 (p19) 注:包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则应影响包内所有类,而对其他包不造成影响。
- 把相关的数据和行为集中放置。 (p19) 警示:设计者应当留意那些通过
get之类操作从别的对象中获取数据的对象,这通常暗示违反了本原则。 - 把不相关的信息放在另一个类中(即:互不沟通的行为)。 (p19) 原则:朝着稳定的方向进行依赖。
- 确保你为之建模的抽象概念是类,而不只是对象扮演的角色。 (p23)
系统架构与功能分布
- 在水平方向上尽可能统一地分布系统功能。 (p30) 即:按照设计,顶层类应当统一地共享工作。
- 在你的系统中不要创建全能类/对象。 (p30) 警示:对名字包含
Driver、Manager、System、Subsystem的类要特别多加小心。 建议:规划一个接口而不是实现一个接口。 - 对公共接口中定义了大量访问方法的类多加小心。 (p30) 这意味着相关数据和行为没有集中存放。
- 对包含太多互不沟通的行为的类多加小心。 (p31) 表现:在应用程序中创建了大量的
get和set函数。 - 在与 UI 交互的模型中,模型不应依赖于界面,界面应当依赖于模型。 (p33)
- 尽可能地按照现实世界建模。 (p36) 注意:为了遵守功能分布原则、避免全能类等,有时需要权衡并违背此条。
类的精简与去重
- 从你的设计中去除不需要的类。 (p38) 通常会将这种类降级为一个属性。
- 去除系统外的类。 (p39) 系统外类的特点:抽象看它们只往系统领域发送消息,但不接受系统领域内其他类发出的消息。
- 不要把操作变成类。 (p40) 警示:质疑名字是动词或派生自动词的类,特别是只有一个行为的类。
- 在设计阶段去除无用的代理类。 (p43)
协作与关联
- 尽量减少类的协作者的数量。 (p52)
- 尽量减少类和协作者之间传递的消息的数量。 (p55)
- 尽量减少类和协作者之间的协作量。 (p55) 即:减少传递的不同消息的数量。
- 尽量减少类的扇出 (Fan-out)。 (p55) 即:减少类定义的消息数和发送的消息数的乘积。
- 如果类包含另一个类的对象,那么包含类应当给被包含的对象发送消息。 (p55) 即:包含关系总是意味着使用关系。
- 类中定义的大多数方法都应当在大多数时间里使用大多数数据成员。 (p57)
- 类包含的对象数目不应当超过开发者短期记忆的容量。 (p57) 建议:数目通常是 6。若超过,应将逻辑相关的成员划分为一组,用新的包含类封装。
约束与语义
- 让系统功能在窄而深的继承体系中垂直分布。 (p58)
- 在实现语义约束时,最好根据类定义来实现。 (p60) 若导致类泛滥,则在行为(如构造函数)中实现。
- 把约束测试放在构造函数允许的尽量深的包含层次中。 (p60)
- 经常改变的语义约束信息,最好放在集中的第三方对象中。 (p60)
- 很少改变的语义约束信息,最好分布在约束所涉及的各个类中。 (p60)
- 类必须知道它包含什么,但是不能知道谁包含它。 (p61)
- 共享字面范围的对象相互之间不应当有使用关系。 (p61)
继承与多态
- 继承只应被用来为特化层次结构建模。 (p74)
- 派生类必须知道基类,基类不应该知道关于派生类的任何信息。 (p74)
- 基类中的所有数据都应当是私有的,不要使用保护 (Protected) 数据。 (p75)
- 理论上,继承层次体系应当越深越好。 (p77)
- 实践中,继承层次的深度不应当超过短期记忆能力(通常值为 6)。 (p77)
- 所有的抽象类都应当是基类。 (p81)
- 所有的基类都应当是抽象类。 (p82)
- 把数据、行为或接口的共性尽可能地放到继承层次体系的高端。 (p85)
- 如果多个类仅共享公共数据(无公共行为),应使用包含关系而非继承。 (p88)
- 如果多个类共享公共数据和行为,应当从公共基类继承。 (p89)
- 如果多个类仅共享公共接口,只有在需要多态使用时才应从公共基类继承。 (p89)
- 对对象类型的显示分情况分析 (Switch/If-Else) 一般是错误的,应使用多态。 (p89)
- 对属性值的显示分情况分析常常是错误的,应将属性值变换为派生类。 (p96)
- 不要通过继承关系来为类的动态语义建模。 (p97) 避免在运行时切换类型。
- 不要把类的对象变成派生类。 (p99) 警示:对任何只有一个实例的派生类都要多加小心。
- 若需要在运行时刻创建新类,认清你创建的是对象,应将其概括为类。 (p103)
- 在派生类中用空方法覆写基类方法应当是非法的。 (p103)
- 不要把可选包含同对继承的需要相混淆。 (p108)
- 在创建继承层次时,试着创建可复用的框架,而不是可复用的组件。 (p112)
多重继承与其它
- 如果你在设计中使用了多重继承,先假设你犯了错误。如果没犯,设法证明它。 (p120)
- 使用继承时问自己: (p121)
- (1) 派生类是否是基类的一个特殊类型?
- (2) 基类是不是派生类的一部分?
- 在多重继承中,确保没有哪个基类实际上是另一个基类的派生类。 (p122)
- 在包含关系和关联关系间作出选择时,请选择包含关系。 (p135)
- 不要把全局数据或全局函数用于类的簿记工作,应使用类变量或类方法。 (p140)
- 不要让物理设计准则破坏逻辑设计,但决策时经常会参考物理准则。 (p149)
- 不要绕开公共接口去修改对象的状态。 (p164)
类的质量核对表
2007-06-29 https://www.iteye.com/blog/javatar-95523
摘自:《代码大全》 —— Steve McConnell
抽象数据类型 (ADT)
- 1.1 是否把程序中的类都看作是抽象数据类型了?是否从这个角度评估它们的接口?
抽象 (Abstraction)
- 2.1 类是否有一个中心目的?
- 2.2 类的命名是否恰当?其名字是否表达了其中心目的?
- 2.3 类的接口是否展现了一致的抽象?
- 2.4 类的接口是否能让人清楚明白地知道该如何使用它?
- 2.5 类的接口是否足够完整,能让其它类无须动用其内部数据?
- 2.6 是否已从类中除去无关信息?
- 2.7 是否考虑过把类进一步分解为组件类?是否已尽可能将其分解?
- 2.8 在修改类时是否维持了其接口的完整性?
封装 (Encapsulation)
- 3.1 是否把类的成员的可访问性降到最小?
- 3.2 是否避免暴露类中的数据成员?
- 3.3 在编程语言许可范围内,类是否已尽可能对其它的类隐藏了实现细节?
- 3.4 类是否避免对使用者(包括派生类)如何使用它做了假设?
- 3.5 类是否不依赖于其它类?它是松散耦合的吗?
继承 (Inheritance)
- 4.1 继承是否用来建立 “是一个 (is a)” 的关系?派生类是否遵循了 LSP (里氏替换原则)?
- LSP: 派生类必须能通过基类的接口而被使用,且使用者无须了解两者之间的差异。
- 4.2 类的文档中是否记述了其继承策略?
- 4.3 派生类是否避免了“覆盖”不可覆盖的方法?
- 4.4 是否把共用的接口、数据和行为都放到尽可能高的继承层次中了?
- 4.5 继承层次是否很浅?
- 4.6 基类中所有的数据成员是否都被定义为 private 而非 protected?
实现相关的其它问题
- 5.1 类中是否只有大约 7 个或更少的数据成员?
- 5.2 是否把类直接或间接调用其它类的子程序的数量减到最少了?
- 5.3 类是否只在绝对必要时才与其它的类相互协作?
- 5.4 是否在构造函数中初始化了所有的数据成员?
- 5.5 除非有经过测量的、创建浅层复本的理由,类是否都被设计为当作深层复本 (Deep Copy) 使用?
与语言相关的问题
- 6.1 是否研究过所有编程语言里和类相关的各种特有问题?
高质量的子程序
2007-06-29 https://www.iteye.com/blog/javatar-95527
摘自:《代码大全》 —— Steve McConnell
创建子程序的理由
创建子程序不仅是为了组织代码,更是为了管理系统的复杂性。主要理由包括:
- 降低复杂度:通过抽取复杂的代码块,简化主程序的逻辑。
- 引入中间的、易懂的抽象:将一段代码命名为一个有意义的子程序。
- 避免代码重复:遵循 DRY(Don’t Repeat Yourself)原则。
- 支持子类化:使派生类能够有针对性地重写某些行为。
- 隐藏顺序:隐藏执行特定任务时所需的具体步骤顺序。
- 隐藏指针操作:提高代码的可读性,避免底层操作干扰业务逻辑。
- 提高可移植性:将平台相关的操作封装在特定的子程序中。
- 简化复杂的逻辑判断:将复杂的布尔表达式提取到命名清晰的函数中。
- 改善性能:便于在单一位置进行优化。
此外,创建类的诸多理由也同样适用:
- 隔离复杂度。
- 隐藏实现细节。
- 限制变更所带来的影响。
- 隐藏全局数据。
- 形成中央控制点。
- 促成可重用的代码。
- 达到特定的重构目的。
子程序的内聚性 (Cohesion)
内聚性是指子程序内部各项任务之间联系的紧密程度。
可接受的内聚性 (高度内聚)
- 功能内聚 (Functional Cohesion):最理想的状态,子程序只执行一项操作。
- 顺序内聚 (Sequential Cohesion):子程序包含需要按特定顺序执行的操作,且前一步的输出是下一步的输入。
- 通信内聚 (Communicational Cohesion):子程序中的不同操作使用了相同的数据,但逻辑上联系不紧密。
- 临时内聚 (Temporal Cohesion):因为需要在同一时间执行而放在一起的操作(如:
Initialize())。
不可取的内聚性 (低度内聚)
- 过程内聚 (Procedural Cohesion):仅仅是因为操作按特定顺序排列。
- 逻辑内聚 (Logical Cohesion):若干个逻辑不同的操作被塞进一个子程序,通过传入的“旗标(Flag)”来决定执行哪一个。
高质量子程序核对表
大局事项
- 创建子程序的理由充分吗?
- 一个子程序中所有适于单独提出的部分,是否已经被提出到单独的子程序中了?
- 过程的名字中是否用了强烈、清晰的“动词+宾语”词组?函数的名字是否描述了其返回值?
- 子程序的名字是否描述了它所做的全部事情?
- 是否给常用的操作建立了统一的命名规则?
- 子程序是否具有强烈的功能上的内聚性?(即它是否做且只做一件事,并且把它做得很好?)
- 子程序之间是否为较松散的耦合?连接是否是小的 (small)、明确的 (intimate)、可见的 (visible) 和灵活的 (flexible)?
- 子程序的长度是否是由其功能和逻辑自然确定,而非遵循任何人为的编码标准?
参数传递事宜
- 整体来看,子程序的参数表是否表现出一种具有整体性且一致的接口抽象?
- 子程序参数的排列顺序是否合理?是否与类似的子程序的参数排列顺序相符?
- 接口假定是否已在文档中说明?
- 子程序的参数个数是否没有超过 7 个?
- 是否用到了每一个输入参数?
- 是否用到了每一个输出参数?
- 子程序是否避免了把输入参数用作工作变量(局部变量)?
- 如果子程序是一个函数,那么它是否在所有可能的情况下都能返回一个合法的值?
防御式编程
2007-06-29 https://www.iteye.com/blog/javatar-95560
摘自:《代码大全》 —— Steve McConnell
核心思想
防御式编程的主要思想是:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。这种思想是将可能出现的错误造成的影响控制在有限的范围内。
断言的使用 (Assertions)
断言主要用于开发和调试阶段,用来捕获那些“理论上不该发生”的程序逻辑错误。
- 区分处理预期与意外:
- 用错误处理代码来处理预期会发生的情况(如用户输入错误)。
- 用断言来处理绝不应该发生的状况(如内部逻辑逻辑破坏)。
- 避免副作用:避免把需要执行的代码放到断言中。
- 约束验证:用断言来注解并验证前条件(Preconditions)和后条件(Postconditions)。
- 双重保障:对于高健壮性的代码,应该先使用断言再处理错误。
错误处理技术
当防御式机制检测到错误时,根据系统需求的不同,可以采取以下十种反应技术:
- 返回中立值:返回一个已知无害的值(如返回 0、空字符串或空对象)。
- 换用下一个正确的数据:在处理数据流(如轮询传感器)时,直接跳过当前错误数据。
- 返回与前次相同的数据:保留上一次的正确结果。
- 换用最接近的合法值:即“数值约束”,将超出范围的值强行设定为最大或最小合法边界值。
- 把警告信息记录到日志文件中:记录异常行为但不中断程序。
- 返回一个错误码:让调用链上的上层逻辑决定如何处理。
- 调用错误处理子程序或对象:将错误交给专门的中心化错误处理器。
- 显示出错消息:当错误发生时直接反馈给用户(需注意避免暴露敏感信息)。
- 在局部处理错误:用最妥当的方式尝试现场修复。
- 关闭程序:对于安全性至上的系统(如医疗或核电软件),直接停机以防引发灾难。
隔离区策略 (Barricade)
《代码大全》中还提到了一个重要概念:隔栏 (Barricade)。
- 在“隔离区”外部,应使用错误处理技术,对数据进行严格清洗和验证。
- 在“隔离区”内部,可以放心使用断言,因为数据在进入该区域前应已被证明是合法的。
变量使用事项
2007-06-29 https://www.iteye.com/blog/javatar-95562
摘抄自:《代码大全》
变量定义
- “变量”在这里同时代表对象和内置数据类型。
- 要养成良好的创建变量的方法和习惯。
- 对支持隐式变量声明的语言,在未声明变量时,编译器会自动声明变量,但这种做法不好。
- 对支持隐式变量声明的语言,建议关闭隐式声明、声明全部变量、遵循某种命名规则、检查变量名。
变量初始化
- 在声明变量的时候初始化。
- 在靠近变量第一次使用的位置初始化。即就近原则,把相关的操作放在一起。
- 理想情况下,在靠近第一次使用变量的位置声明和定义该变量。声明指定了变量的类型,定义为变量指定特定的取值,每个变量都应该在声明的同时被定义。
- 在可能的情况下使用
final或者const。final和const关键字在定义常量、输入参数以及任何初始化后其值不再发生改变的局部变量时非常有用。 - 特别注意计数器和累加器。注意在下一次使用这些变量之前重置其值。
- 在类的构造函数里初始化该类的数据成员。如果在构造函数里分配了内存,就应该在析构函数中释放。
- 检查是否需要重新初始化。如果的确需要重新初始化,要确保初始化语句位于那些重复执行的代码内部。
- 一次性初始化具名常量;用可执行代码来初始化变量。若想用变量来模拟具名常量,则在程序开始处对常量做一次初始化即可。对于真正的变量,则应在靠近它们使用位置用可执行代码对其初始化。
- 使用编译器设置来自动初始化所有变量。确保记下所使用的编译器设置。
- 利用编译器的警告信息。
- 检查输入参数的合法性。赋值前确保数值合理。
- 使用内存访问检查工具来检查错误的指针。
- 在程序开始时初始化工作内存。
变量的作用域
作用域或者可见性指的是变量在程序内的可见和可引用的范围。
- 使变量引用局部化。好的做法是把对一个变量的引用局部化,即把引用点尽可能集中在一起,提高程序的可读性。(跨度)
- 尽可能缩短变量的“存活”时间。即一个变量存在期间所跨越的语句的总数,开始于引用它的第一条语句,结束于引用它的最后一条语句。可以减少初始化错误的可能,会使代码更具可读性,也会使把相关的代码片段重构为单独的子程序会非常容易了。(生存时间)
减小作用域的一般原则
- 在循环开始之前再去初始化该循环里使用的变量,而不是在该循环所属的子程序的开始处初始化这些变量。
- 直到变量即将被使用时再为其赋值。让变量的赋值位置越明显越好。
- 把相关语句放在一起。
- 把相关语句组提取成单独的子程序。
- 开始时采用最严格的可见性,然后根据需要扩展变量的作用域。在对变量的作用域进行选择时,应该倾向于选择该变量所能具有的最小的作用域:首选将变量局限于某个特定的循环,然后是局限于某个子程序,其次成为类的
private变量,protected变量,再其次对包 (package) 可见,最后在不得已的情况下再把它作为全局变量。
变量的持续性
- 在程序中加入调试代码或者断言来检查那些关键变量的合理取值。如果变量取值变得不合理,就发出警告信息通知去寻找是否有不正确的初始化。
- 准备舍弃变量时给它们赋上“不合理的数值”。
- 编写代码时要假设数据并没有持续性。
- 养成在使用所有数据之前声明和初始化的习惯。
变量的绑定时间
即变量和它的值绑定在一起的时间,采取越晚的绑定时间会越有利。变量与数值绑定时间的情况:
- 编码时(使用数值硬编码)
- 编译时(使用具名常量)
- 加载时(从外部数据源中读取数据)
- 对象实例化时(如每次窗体创建时读取数据)
- 即时(如每次窗体重绘时读取数据)
一般而言,绑定时间越早灵活性越差,但复杂度也会越低。但希望获得灵活性越强,则支持这种灵活性的代码就越复杂,出错几率也会越高。按照需要引入足够的灵活性来满足软件需求。
数据类型与控制结构
- 序列型数据对应程序中的顺序语句。序列型数据是由一些按照特定顺序使用的数据组成的。若在一行中写有五条语句,每条语句都负责处理一项不同的数据,则它们就是顺序语句。
- 选择型数据对应为程序中的
if和case语句。选择型数据指的是一组在任一特定时刻有且仅有一项被使用的数据。相应的程序语句必须做出实际的选择。 - 迭代型数据对应为程序中的
for、repeat、while等循环结构。迭代型数据是需要反复进行操作的同类型的数据,通常保存为容器中的元素、文件中的记录或者数组中的元素。迭代型数据与负责读取数据的迭代型代码(即循环)相对应。
在使用过程中,可能结合了序列型、选择型和迭代型数据,可以把这几种简单的构造块组合起来描述更复杂的数据类型。
为变量指定单一用途
- 每个变量只用于单一用途。
- 避免让代码具有隐含含义。
- 确保使用了所有已声明的变量。检查代码以确认使用了所有声明过的变量的习惯。
VRAPS原则
2007-07-13 https://www.iteye.com/blog/javatar-100798
VRAPS 模型是评估架构在组织环境中是否健康、是否具备可持续发展能力的核心框架。
V —— Vision(构想)
定义:构想是未来价值到架构约束的映射。
- 衡量维度:可以用架构的结构、目标明确程度、一致性、灵活性等来衡量。
- 核心点:构想决定了架构的方向,确保所有的技术决策都服务于业务的最终愿景。
R —— Rhythm(节奏)
定义:节奏是一个架构团体内部,以及它与客户或用户之间反复出现的、可预测的工程交互活动。
- 核心点:健康的节奏能为组织提供可预测性,降低沟通成本,使开发团队、测试团队以及市场部门能够协同工作,形成一种“节拍”。
A —— Anticipation(预见)
定义:预见是指架构人员根据技术的变化、竞争的发展以及用户的需求变更,来预测、验证和调整架构的程度。
- 核心点:架构必须能够前瞻性地处理未来的变化,避免架构过快地走向衰败或无法适应市场调整。
P —— Partnering(协作)
定义:协作是指架构受益人(Stakeholders)保持明确的、合作的角色,并将其所提供和获得的价值最大化的程度。
- 核心点:架构不仅仅是技术活,更是一种契约。确保所有相关利益方(如供应商、第三方开发者、客户等)在合作中双赢。
S —— Simplification(简化)
定义:简化是指对所作用的架构和组织环境进行巧妙的澄清和最小化。
- 核心点:无论是在技术实现还是组织流程上,都应追求最精炼的结构。去除不必要的复杂性,使系统和管理更加透明且易于维护。
中层管理人员必备能力
2007-07-13 https://www.iteye.com/blog/javatar-100801
麦肯锡公司的一项调查表明:有的公司能保持持续发展和改革,达到更高的业绩,关键的因素不在于高级管理者,而在于一批具有改革才能的中层管理者和专业人才。
可见中层管理人员在企业中起中流砥柱的作用,他们不同于一般员工,他们的素质高低,在很大程度上影响一般员工的职业行为。甚至关系企业发展的成败,因此对中层管理者的素质,要有更高层次的特殊的要求。
在企业发展的各个阶段,对中层的素质要求也不尽相同,但有几项是必须具备的:
主动
主动性是指管理者在工作中不惜投入较多的精力,善于发现和创造新的机会,提前预计到事情发生的可能性,并有计划地采取行动提高工作绩效、避免问题的发生、或创造新的机遇。
不能积极主动地前进,不敢为人先,集体的成绩就会受到限制。如果中层管理者不能对企业的总体绩效产生积极的推动作用,就是在为自己的事业自掘坟墓。衡量中层管理者工作成效的标准之一就是要看其个人主动发起的行动数量。在这一点上,中层管理者与冲浪运动员颇为相似。冲浪者只有赶在浪潮前面,才能够精彩地冲向岸边。而如果每次都慢半拍,就只能在海里起起落落,等待下一波浪涛的到来。走在时代前列需要真正的努力与积极性。
执行力
这点就不需多说了,执行力包括了沟通/协调能力、专业技术能力在内,由责任心驱动。
有家公司的编辑部经常抱怨技术没有解决好工作平台的问题,技术也抱怨编辑并没有将需求讲清楚。坐在一起开会也一直没有执行的方案,编辑负责人指责技术经理,技术经理认为他是做管理的,主要工作就是安排开发计划。这就是责任心的问题了,中层管理人员并不只是对部门内部负责,更多的在于部门之间的协调/沟通。
关注细节
任何事情从量变到质变都不是一个短暂的过程,如果中层管理者没有持之以恒的“举轻若重”,做好每一个细节的务实精神,就达不到“举重若轻”的境界。
影响力
一个拥有充分的影响力的中层领导者, 可以在领导岗位上指挥自如、得心应手, 带领队伍取得良好的成绩;相反, 一个影响力很弱的领导者, 过多地依靠命令和权力的领导者, 是不可能在分队中树立真正的威信和取得满意的领导效能的。
培养他人的能力
优秀的中层管理者更多的关注员工的潜能的开发,鼓励和帮助下属取得成功。安排各种经历以提高他的能力,帮助他成长。
松下公司的领导者认为,如果指示太过详尽,就可能使部属养成不动脑筋的依赖心理。一个命令一个动作地机械工作,不但谈不上提升效率,更谈不上培养人才。在训练人才方面,最重要的是引导被训练者反复思考、亲自制定计划策略并付诸实行。只有独立自主,才能独当一面。对中层管理者而言,最重要的工作就是启发部属的自主能力,使每一个人都能独立作业,而不是成为惟命是从的傀儡。(1)(2)
某公司的中层经理说:“我经常将一些非常重要的会议交给我的高级助理去主持,这样在我到会或者不到会的时候,会议都可以正常地进行,其他部门地经理都能将他们地问题反映给我地助理,他也能够基本按照我地意思现场处理某些急需决策地事务(3),我认为这对于我地助理来说就是最好地培训(4)。
带领团队的能力
某公司有两位刚从技术工作提升到技术管理职位的年轻管理者:A经理和B经理。
- A经理:觉得责任重大,技术进步日新月异,部门中又有许多技术问题没有解决,很有紧迫感,每天刻苦学习相关知识,钻研技术文件,加班加点解决技术问题。他认为,问题的关键在于他是否能向下属证明自己在技术方面是如何的出色。
- B经理:也认识技术的重要性和自己部门的不足,因此他花很多的时间向下属介绍自己的经验和知识;当他们遇到问题,他也帮忙一起解决,并积极地和相关部门联系和协调。
结果对比:
三个月后,A经理和B经理都非常好地解决了部门的技术问题,而且A经理似乎更突出。但半年后,A经理发现问题越来越多,自己越来越忙,但下属似乎并不满意,觉得很委屈。B经理却得到了下属的拥戴,部门士气高昂,以前的问题都解决了,还拥有新的技术研究成果。
对管理者而言,真正意义上的成功必然是团队的成功。脱离团队,去追求个人的成功,这样的成功即使得到了,往往也是变味的和苦涩的,长期是对公司有害的。因此,一个优秀的中层管理者决不是个人的勇猛直前、孤军深入,而是带领下属共同前进。
如何成为主管
2007-07-13 https://www.iteye.com/blog/javatar-100806
公司比较喜欢有抱负的员工。永远不要隐藏自己的抱负。在管理层和职位相当的其他员工面前展示它。让所有人都知道你渴望成功。
展示抱负的益处
- 赢得尊重:宣布你的雄心和抱负带来的即时好处之一就是人们会对你刮目相看,那些有积极目标的人通常都会受到他人的赞赏。
- 获得外部支持:当你告诉别人你想达到什么目标时,他们通常都会给予你帮助。
- 建立正面形象:你可以做一些笔记,这一行动会在管理者心目中给你建立起这样的形象:目标明确并且乐意听从别人的忠告。
- 进入观察视野:从那时起,这位资深管理者会开始观察你,看看你如何超越其他员工。
公司愿意提拔的优秀品质
动机是很重要的。公司寻找的候选人通常具有以下品质:
- 可靠性:他们寻找的是可靠的人。
- 领导力:他们寻找天生的领导者。
- 忠诚度:他们寻找的是忠诚的人。
- 责任感:他们寻找的是愿意承担责任的人。
- 绩效卓越:他们寻找的是在自己现有职位上有所成就的人。
- 沟通与领悟:他们寻找善于聆听和理解领导意图的人。
- 诚实:他们寻找诚实的人。
管理技能与领导力
2007-07-13 https://www.iteye.com/blog/javatar-100808
- 你所订出来的目标是否跟公司的价值观吻合?跟其他部门的目标有没有冲突?
- 你是否用清楚而明确的方式来界定目标?
- 你的目标与实行计划是否定好时间表? 有没有高估或低估完成所需的时间,人力及成本?
- 你所订出的目标是你真心想达成的吗?它确实可行还是空中楼阁?
- 你是否准备了每个部门成员的职务说明?
- 你是否定期举行部门会议?
- 你了解上司的目标吗?
- 是否就你自己的部门目标与上司达成协议?
- 你是否与部属共同订立目标?
- 是否建立并沟通过可行的绩效衡量指标?
- 是否定期检讨目标与绩效衡量指标(Performance measures)?
- 确定每个人的绩效评估都是当令(current)的吗?(新进员工6个月内;固定员工一年一次;换职,调部门员工要立即
- 每季检核员工薪资水准是否符合他的绩效评比吗?
- 你是否为每个部属拟妥了一份他的未来工作发展计划,并定期检讨?
- 你是否参与员工训练与发展计划?
- 你是否对刚受训回来的员工给予测试,以了解他增长多少知识?
什么时候可以用继承
2007-07-28 https://www.iteye.com/blog/javatar-106438
摘自《Java Design》
对于超类A和子类B,必需满足:
- 命题“B是一个由A扮演的角色”不成立。
- B永远不需要转型成为其他某些类别中的对象。
- B扩展而不是覆盖或废弃A的行为(即Liskov原则)。
- A不仅仅是一个工具类(一些可以重用的实用功能)。
- 对于一个问题域(特定的业务对象环境):A和B定义了同一类型的对象,或者是用户事务、角色、实体(团体、位置或其他东西),或其他物体的相似类别。
否则,改用关联关系可能更加稳固、正确。
表达式归约算法优化
2007-08-14 https://www.iteye.com/blog/javatar-112033
这两天将MeteorTL的表达式归约算法优化了一下,对常量计算提前到解释期,这样可以避免在运行期重复计算,提高性能,以及减小表达式树的大小。 如: coins + 2 * 3 * 4 被优化成 coins + 24, 2 + 3 + 4 + coins 被优化成 5 + coins, 但 coins + 2 + 3 + 4 的处理较麻烦, 因为操作符的优先级相同,并且是从左至右的结合律,归约成树时,coins与2组合成一个节点后,再与3组合,再与4组合,这样很难判断其为常量计算。 可能要改变中缀表达式的优先级算法,当为常量时,优先级加高。 待考虑。 BTW: 在任何代码中写表达式,都应该尽可能将常量写在前面,便于编译器优化。
FreeMarker代码质量真的很差
2007-08-14 https://www.iteye.com/blog/javatar-112111
这里说的版本是:FreeMarker 2.3.10 (April 20, 2007)
freemarker.core.TemplateElement 第101行:
1
2
3
4
5
6
7
8
9
10
public TemplateSequenceModel getChildNodes() {
if (nestedElements != null) {
return new SimpleSequence(nestedElements);
}
SimpleSequence result = null;
if (nestedBlock != null) {
result.add(nestedBlock);
}
return result;
}
居然在SimpleSequence result = null;后直接调用result.add(nestedBlock);
这行代码永远空指针异常!
freemarker.core.StopException 第84行:
public void printStackTrace(PrintStream ps) {
String msg = this.getMessage();
ps.print("Encountered stop instruction");
if (msg != null & !msg.equals("")) {
ps.println("\nCause given: " + msg);
} else ps.println();
super.printStackTrace(ps);
}
if (msg != null & !msg.equals("")) 没有用 短路与“&&” 而是 “&”,
当msg == null时, msg.equals("")总是会被调用,总是空指针异常!
else语句一点作用都没有。
freemarker.template.ObjectWrapper 第77行
1
ObjectWrapper DEFAULT_WRAPPER = DefaultObjectWrapper.instance;
freemarker.template.DefaultObjectWrapper 第70行
1
static final DefaultObjectWrapper instance = new DefaultObjectWrapper();
其中,DefaultObjectWrapper 是 ObjectWrapper 的子类。
他居然在父类静态块初始化过程中调用子类初始化方法,
晕倒,子类怎么能在父类之前实例化,这只会使构造失败,经常出一些莫明其妙的错误。
太多了,诸如此类问题用FindBugs就能查出几十个,
还有N多没用到的变量,方法,内部匿名类,到处乱七八糟,
不知道作者怎么对得起这么多用户。
同样的方法检查Spring和Hibernate,所有代码都写很严谨,也没有任何无用代码。
很无语…
Builder接口重构
2007-08-26 https://www.iteye.com/blog/javatar-116843
MeteorTL(http://www.meteortl.org)以前的(0.5.1以前)版本中的API接口为:
TemplateFactory 和 ContextFactory 用于创建 Template 和 Context 两个核心 Domain,
Factory 用于合并 TemplateFactory 和 ContextFactory 两个工厂,
Builder 用于创建 Factory,
Engine 实现 Builder,通过Builder接口一步步导航到其它接口,
然而一直感觉Builder接口很不顺眼,
因为它与core包中的其它接口内聚性不高,
它只是作为core包其它接口的使用者,而不是被core包其它接口使用,或继承于core包其它接口等更强一点的关联,并且不是核心Domain。
有几个版本试图将它移到engine包,但发现也并不妥善,
因为整体架构声明engine包用于实现core包所有接口,
仔细研究Builder的实现类Engine, 发现其持有一个不合理的Configuration状态,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Builder {
Factory buildFactory();
}
public class Engine implements Builder {
private Configuration config;
public Engine(Configuration config) {
this.config = config;
}
public Factory buildFactory() {
// 通过config信息创建一个Factory接口的实例并返回
}
}
用户调用方式:
1
Factory Factory = new Engine(config).buildFactory();
问题在于config只在一次建造过程有效,Engine类持有它毫无意义,重构如下:
1
2
3
4
5
6
7
8
9
10
public interface Builder {
Factory buildFactory(Configuration config);
}
public class Engine implements Builder {
public Factory buildFactory(Configuration config) {
// 通过config信息创建一个Factory接口的实例并返回
}
}
用户调用方式:
1
Factory Factory = new Engine().buildFactory(config);
但这样Builder就会依赖于Configuration接口,
而按整体架构core包是不能依config包的,
所以只能把Builder移到engine包,
发现Engine实例的存在也没起任何作用,再重构为:
1
Factory Factory = Engine.buildFactory(config);
这样,Builder接口就应该删除,
在我试着删掉Builder接口后, 猛然发现Engine如果直接实现Factory会更合理,如:
public class Engine implements Factory {
public Engine(Configuration config) {
// 通过config信息建造自身
}
public Template getTemplate(String name, String encoding) {
...
}
...
}
用户调用方式:
1
Factory Factory = new Engine(config);
总结:
当发现放在哪都不合理的类或接口,或许它本身就不应该存在。
TemplateExceptionHandler思考
2007-09-07 https://www.iteye.com/blog/javatar-121616
在重构MeteorTL(http://www.meteortl.org)的异常处理体系时,
对TemplateExceptionHandler的位置思索良久…
先帖几个相关类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class TemplateException extends RuntimeException {
private static final long serialVersionUID = 1L;
public TemplateException(TemplateSource templateSource, Range location) {
super();
this.templateSource = templateSource;
this.location = location;
}
public TemplateException(TemplateSource templateSource, Range location, String message) {
super(message);
this.templateSource = templateSource;
this.location = location;
}
public TemplateException(TemplateSource templateSource, Range location, Throwable cause) {
super(cause);
this.templateSource = templateSource;
this.location = location;
}
public TemplateException(TemplateSource templateSource, Range location, String message, Throwable cause) {
super(message, cause);
this.templateSource = templateSource;
this.location = location;
}
// 持有异常处理需要的数据 ------------
private TemplateSource templateSource;
public TemplateSource getTemplateSource() {
return templateSource;
}
private Range location;
public Range getLocation() {
return location;
}
}
public interface TemplateExceptionHandler {
void handleTemplateException(TemplateException exception) throws IOException;
}
public class ConsoleTemplateExceptionHandler implements TemplateExceptionHandler {
public void handleTemplateException(TemplateException exception) throws IOException {
// 向控制台输出异常信息,并指出其发生位置
}
}
public class HtmlTemplateExceptionHandler implements TemplateExceptionHandler {
// 页面输出端
private Writer output;
public HtmlTemplateExceptionHandler(Writer output) {
this.output = output;
}
public void handleTemplateException(TemplateException exception) throws IOException {
// 向页面输出端输出友好的HTML代码,如:用高亮显示出错代码行等
}
}
考虑的主要问题在于TemplateExceptionHandler应该放在哪,由谁管理?
选择一:
放在side包,因为引擎将包含相关数据的TemplateException抛出后,怎么处理TemplateException不再是引擎的职责,
但这样,就必须在:
org.meteortl.side.TemplateTool,
org.meteortl.side.servlet.TemplateServlet,
org.meteortl.side.jsp.TemplateTag,
org.meteortl.side.webwork.TemplateResult,
等周边集成类中都各自管理TemplateExceptionHandler
如:
1
2
3
4
5
6
7
8
TemplateExceptionHandler templateExceptionHandler = ...
try {
......
factory.getTemplate("xxx.mtl").render(context);
} catch (TemplateException e) {
templateExceptionHandler.handleTemplateException(e);
}
选择二:
放入config包,因为用户期望在配置中指定相应处理器,
这样,应将Handler后缀改成Interceptor,表示引擎可以拦截TemplateException,
但异常在引擎什么位置拦截,拦截后怎么处理返回,都有待考虑,
如:
1
2
3
4
5
6
7
8
public Template getTemplate(String name) throws IOException, TemplateException {
try {
return proxyFactory.getTemplate(name);
} catch (TemplateException e) {
templateExceptionHandler.handleTemplateException(e);
// 这里继续抛异常?还是返回null?
}
}
主控Iterator模式与被动Visitor模式
2007-09-11 https://www.iteye.com/blog/javatar-122984
有朋友问我MeteorTL(http://www.meteortl.org)优势列表中的主控Iterator模式优势具体指什么。
buaawhl的帖子已经分析很多: http://www.iteye.com/topic/21293
我再补充的说一下: Visitor模式使用最多的时候是处理合成模式的树结构, 估计是受[GoF95]的影响,书中提倡将树结构的表示与逻辑分开, 这样不同的Visitor对同一树结构可以做不同的事,以达到重用及扩展, 如:将一个模板解析成树后,针对同一树结构表示,EvaluationVisitor可以输出模板结果, BackupVisitor可以备份树,CountVisitor可以统计树中的节点…
这种思想很好,但实际中,如果以Visitor风格导出API,API很难理解,也很难使用, 因为扩展方很被动,并不是每一次使用树都是要做遍历操作的, 而且,Visitor通常会很大,因为它要处理所有节点类型,通常是用很多类型重载的visit方法, 再者,往往最后都是,整个系统只有一个Visitor,因为Visitor的实现很复杂,
其实Visitor模式的优势Iterator模式都能作到, 如果改用Iterator模式,可以使用解释器模式的层级调用, 在主控函数中回调Handler,以及发布访问前后Event,再加上个AOP拦截器链, 扩展者可以只Handle某一个节点类型的处理,也可通过监听事件处理周边事物,或者通过AOP统一处理所有节点, 灵活性远远超过Visitor模式, 如:FreeMarker, Velocity都使用了Visitor模式,扩展性是极其受限的,这可能是由于它们都使用JavaCC作为解板工具,而JavaCC生成Visitor模式的解析代码,有点被迫使用。
而MeteorTL(http://www.meteortl.org)则使用Iterator模式,在扩展性方面就很有优势。 节点的扩展是经常用到的, 举个例对比一下: Visitor模式的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class NodeHandler {
public static final int DO_BODY = 1;
...
// 因为Node是被动的,所以返回一个数字控制Visitor的遍历路线,
// 除非只作正规的前序遍历,就能满足你所有的需求,则可以不返回值,
public int handle() {
// 返回1,继续访问内部节点
// 返回2,继续访问下一兄弟节点
// 返回3,表示重复当前节点
// 返回4,表示跳过后面所有节点
// ...
}
}
看到上面的方式,估计写过JSP Tag Lib的朋友印象深刻,doStrat, doEnd就是这么做的。
调用者(JSP引擎相当于调用者)也很费神,要一个个判断状态位,if…else…估计是一大堆。
如果你的需求超过了上面的1, 2, 3, 4…,那就没办法,等新版本发布可能会有:-)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class NodeHandler {
// 每个节点持有自己的子节点,解析树时置入
Node[] childNodes;
public void handle() {
// 1.如果要忽略子节点,直接将函数留空
// 2.如果要运行字节点
handleChildNodes();
// 3.如果要迭代子节点n次
for (int i = 0, i < n; i ++) {
handleChildNodes();
}
}
private void handleChildNodes() {
for(int i = 0, n = childNodes.length; i < n; i ++) {
childNodes[i].handle();
}
}
}
调用者只需要调用根节点的handle(),根节点将调用其所有子节点,层级调用下去就完成了遍历,需不需要调用下级节点完全是主控的。
整理MeteorTL核心包的设计
2007-09-15 https://www.iteye.com/blog/javatar-124200
今天整理了一下MeteorTL(http://www.meteortl.org)核心包的设计,把UML也重画了一下(见附件)。
发现一个边界接口:EventListener,
按OOD启思录的说法,它只向core包发送消息(即只是使用Event类),
并没有接收core包其它类或接口发出的消息,或静态结构依于core包,
所以它不应该属于core包,
照此的话,应该将其放到config包,
但搬过去后,感觉有点语义分离,
EventListener, Event, EventPublisher应该是一体的,
所以最后决定还是保留在core包。
哪些接口改为抽象类
2007-10-31 https://www.iteye.com/blog/javatar-136956
CommonTemplate(http://commontemplate.org)开始是以接口驱动设计的,
core(API)包和config(SPI)包全部为接口,
准备发布正式版,保持API等向前兼容,为了项目的可维护性,需要将一些接口改成抽象类,
抽象类比接口的最大优势是能够在后续版本添加方法,并保持向前兼容(提供一个默认实现或空实现或抛出不支持异常都可以)。
就这一点,很明显,Context(包括GlobalContext,LocalContext),Configuration(包括DirectiveConfiguration,ExpressionConfiguration)必须改成抽象类,
因为当升级版本,需要为Context提供更多外部交互功能时,肯定要增加方法,
而如果新版本的引擎实现需要更多配置信息时,Configuration也需要增加方法,并提供默认配置信息以向前兼容。
当然只这样考虑抽象类的使用是很狭义的,按理论is-a而非has-a或like-a的都应是类,而非接口,
这样感觉TemplateSource, Template也应该改成抽象类,
相应的Directive(包括BlockDirective), Expression(包括Operator,BinaryOperator,UnaryOperator)是否也应该如此呢?
但Directive等在AOP时使用了代理等方式,使用接口更易实现。
思考…
CommonTemplate的API结构再思考
2007-10-31 https://www.iteye.com/blog/javatar-136958
DirectiveList是否应该实现List接口?
Expression是否应传入VariableLookuper而非Context?
Directive,Expression是否应抽取公共接口TemplateElement?
Constant, Variable是否应放入core包?
Template,Directive,Expression等接口是否应继承序列化接口,还是应在其具体类中加入?
思考…
velocity邮件列表中,问得频率比较高的几个问题
2007-11-01 https://www.iteye.com/blog/javatar-137357
- 没想到最高的是:特殊符怎么转义? velocity没有使用大家惯用的反斜杠,大部分人在尝试#或$或"失败后很疑惑,有人想出的“绝妙”办法是:#set($D=’$’),然后 ${D}
- 性能,Velocity1.5比1.4内存消耗更大,在单例使用VelocityEngine时经常出现OutOfMemoryError
- 模板加载路径,热加载
- 多数组取值
- 格式化处理
- 与其它框架的集成
….
看来在CommonTemplate(http://commontemplate.org)中,也应该多注意这些细节的设计,力求做到“别让我思考”,另外性能更是重中之重。
配置框架设计
2007-11-09 https://www.iteye.com/blog/javatar-139420
CommonTemplate 的配置方案一直没定,主 API 提供的都只是编程调用相应 setXXX, addXXX 完成相应配置,这两天思考了一下其配置框架的设计。
配置框架需要处理的问题
- 可编程性:可配置完成的工作,一定要能可编程实现。如果用户不用任何配置文件,而是编程调用相应
setXXX,addXXX,应能完成所有配置。 - 扩展类配置:扩展类也需要配置。例如
cache=org.commontemplate.standard.cache.FIFOCahce,其中FIFOCahce本身也需要配置信息,如缓存池大小等等。 - 配置复用与继承:配置应该可以放在多个文件中,或者配置文件间可以继承。例如用户写一个配置,但只想覆盖标准配置的部分设置,则应该可以继承标准配置。
- 多样化格式:考虑用 Properties,XML 等多种配置方式。
备选方案分析
方案一:基于接口的配置初始化(SettingsAware)
1
2
3
4
5
6
public interface SettingsAware {
/** * 通过配置初始化,此函数在单线程下被调用
* * @param settings 相关设置配置项,不可变键值对
*/
public void init(Map<String, String> settings);
}
- 实现逻辑:配置中的类若实现了
SettingsAware接口,则配置工厂在创建实例时,将配置信息通过init方法传递给实例。 - 示例应用:
cache=org.commontemplate.standard.cache.FIFOCahcecahce.maxSize=1000
- 缺点:此方案违背 IoC 原则,主动去取配置信息,扩展性较弱。例如若换成 XML 配置,
init方法可能需要传入org.w3c.dom.Document之类的配置信息。
方案二:简单 IoC 注入(模仿 log4j)
- 实现逻辑:不实现特定接口,而是利用反射进行层级注入。
- 示例应用:
cache=org.commontemplate.standard.cache.FIFOCahcecahce.maxSize=1000
- 执行方式:
FIFOCahce暴露setMaxSize(int),配置工厂实现自动注入。
方案三:引入成熟的 IoC 容器(如 Spring)
- 实现逻辑:如果
StandardConfiguration类所有配置都提供相应 setter 方法(包括集合),则可以直接使用 Spring 等容器完成注入。 - XML 示例:
<beans>
<bean id="configuration" class="org.commontemplate.standard.StandardConfiguration">
<property name="cache" ref="cache" />
</bean>
<bean id="cache" class="org.commontemplate.standard.cache.FIFOCahce">
<property name="maxSize" value="1000" />
</bean>
</beans>
- 优势:实例需要的配置得到根本解决,且可以灵活替换成其他 IoC 容器。
核心总结
只要保证整个配置树的可注入性,其配置方式就是极度可扩展的。
CommonTemplate 配置方案确定
根据前几天的思考,CommonTemplate 的配置方案确定,采用全 setter 方式配置,以保持可以用任意 IoC 容器进行配置。但为了不依赖于任何 IoC 容器使用组件,在 util 包中实现了一个简单的 BeanFactory,作为默认 IoC 容器实现:org.commontemplate.util.PropertiesBeanFactory。
采用 properties 作为配置,需遵循 java.util.Properties 的所有规则,如:# ! = : 等符号需转义。
实例创建与属性注入
以 () 结尾表示创建实例。实例可以注入属性:使用实例的 key 作为前缀,点号后跟其属性。前提是类中有一个对应的注入函数。
示例:
templateCache=org.commontemplate.standard.cache.FIFOCache()
templateCache.maxSize=1000
List 集合配置
以 [] 结尾表示 List<Object>,并以其 value 作为前缀搜索 List 的项。下标号用来唯一识别及排序,必需为数字,大小任意。
示例:
templateNameFilter=templateNameFilters[]
templateNameFilters[100]=org.commontemplate.standard.filter.TemplateNameRelativer()
templateNameFilters[200]=org.commontemplate.standard.filter.TemplateNameCleaner()
templateNameFilters[200].maxLength=10
继承特性:
通常下标号都留一点间距(如 100, 200),当子级配置继承当前配置时,可以让其配置项插在已有列表项中间。如子配置中有 templateNameFilters[101]=com.xxx.YYYFilter(),就会排序到上述配置中间。
Map 集合配置
以 {} 结尾表示 Map<String, Object>,并以其 value 作为前缀搜索 Map 的项。下标 {} 中名称若有符号,均作为字符串。
示例:
directiveHandlers=directive{}
directive{if}=org.commontemplate.standard.directive.condition.IfDirectiveHandler()
directive{for}=org.commontemplate.standard.directive.iteration.ForeachDirectiveHandler() directive{for}.statusName=for
基本类型处理
基本类型处理与 Java 相似:
null,true,false为关键字,表示特殊值。- 以数字开头的为 Number,识别后缀
L,F,D,S。 - 以单引号括起的为 Character。
- 以双引号括起的为 String,如:
"xxxx"。
类型处理补充
其它情况均作为 String 处理。但若要输出特殊标识的 String,必需用双引号,如:"true"。
配置继承
采用 @extends 作为配置继承专用属性,可以多继承,用逗号分隔各父级配置文件。
示例:
1
@extends=org/commontemplate/standard/commontemplate-standard.properties
设计思想与展望
- 尽可能把所有设置项行级展开,以便于子级配置可以轻松地覆盖父级的所有配置细节。
- 现有实现仅支持
List<Object>和Map<String, Object>作为容器类,后续会改进加入其它容器。 - 完善后考虑抽取出来单独作为一个发行包。
参考资料:
CommonTemplate 的标准配置:org/commontemplate/standard/commontemplate-standard.properties
模板是否应该支持函数调用?
2007-11-21 https://www.iteye.com/blog/javatar-142296
首先,将 Java 中的 Method 分成:Subroutine 和 Function 两种。按照“契约式设计原则”的说法:
- Subroutine:是有副作用的 (side-effect),语法上通常没有返回值,即
void方法。 - Function:是没有副作用的,语法上有返回值。
比较明确的是,模板肯定不允许调用 Subroutine,否则肯定会引入大量业务逻辑。现在待讨论的是,模板中是否允许调用 Function?如:${object.funtion(arg1, arg2)} 或 静态 Function:${funtion(arg1, arg2)}。
禁止 Function 调用的理由与替代方案
在 CommonTemplate 设计之初是禁止调用 Function 的,因为 Function 会使模板复杂化,通常可以用其它更好的方式表达:
- 无参 Function 简化为属性 直接去掉括号,如:
String.trimString.toUpperCaseList.sizeNumber.toString
- 有参 Function 转换为操作符 用更形象化的操作符处理,如:
- 用
Map[key]代替Map.get(key) - 用
List[2..4]代替List.subList(2, 4) - 将 String 看作
char[]数组,用String[2..4]代替String.substring(2, 4),用String[2]代替String.charAt(2) - 用
"aa" ~= "AA"(约等于) 代替"aa".equalsIgnoreCase("AA")
- 用
扩展点的实现需求
通常处理的 Domain 对象都是 POJO、基本数据类型、集合类等。如果标准包支持较好,禁掉 Function 是可行的。然而,标准包不可能预见用户所有需求,必须留有扩展点。
以最近版本加入的 orderby 为例(假设其不在标准包,需用户自行实现):
- 需求定义:实现集合类的按属性排序。
- 示例环境:
books是一个包含 Book 的集合,Book 是有title,price等属性的 POJO。需在循环前按价格排序。
三种扩展方案分析
- 扩展静态方法
- 语法:
$for{book : orderby(books, "price")} - 说明:JSP 2.0 的 EL 和 Velocity 采用类似方案。
- 语法:
- 扩展对象方法
- 语法:
$for{book : books.orderby("price")}
- 语法:
- 扩展二元操作符
- 语法:
$for{book : books orderby "price"}
- 语法:
设计抉择的考量
- 如果禁止方法调用,同时也应禁止静态方法,则只能采用第三种“操作符扩展”。
- 如果此类需求过多,会导致操作符数量骤增,从而降低表达式的可读性及易用性。
- 第二种方案(对象方法扩展)看起来比较合理,通过在外部给集合类扩展一个方法。
核心问题:是否应该为了这类扩展性需求而开启 Function 调用?
重用CommonTemplate的EL
2007-11-21 https://www.iteye.com/blog/javatar-142387
前几天和 jindw 讨论时,他提到想在他的一个开源项目重用 CommonTemplate 的 EL (Expression Language),问我是否可以抽取使用。我觉得这是一个很好的想法,为此重构了一下 CommonTemplate,以使 EL 分离于 TL (Template Language),可以单独使用。
重构背景
因为原始设计就将 EL 单独设计的,只是为了和 TL 统一,有些范围未最小化控制。
重构步骤
- 修改求值接口
- 将 Expression 的求值过程所依赖 Context 改成 VariableResolver。
- 将:
Object evaluate(Context context);改成:Object evaluate(VariableResolver variableResolver); - 引入 VariableResolver 接口
1 2 3
public interface VariableResolver { Object lookupVariable(String name) throw VariableException; }
- 调整继承关系 让 Context 继承于 VariableResolver,保持指令可以使用原有的 expression.evaluate(context) 调用表达式求值。 实际继承关系为:Context -> LocalContext -> VariableStorage -> VariableResolver。这样单独使用 EL 时,只需实现最简单的 VariableResolver 即可。
EL 独立使用体系
实现上述重构后,单独使用 EL 的核心类及配置包括:
org.commontemplate.core.VariableResolverorg.commontemplate.core.Expressionorg.commontemplate.cofig.ExpressionConfigurationorg.commontemplate.engine.expression.ExpressionEngineorg.commontemplate.standard.ExpressionSetting
示例代码
第三方只需要重新实现 org.commontemplate.core.VariableResolver,其它都可以直接使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 创建配置// 这里用的 org.commontemplate.util 包内置的 IoC 配置工具,// 也可以编程一个个 setXXX,或用其它 IoC 容器创建
BeanFactory beanFactory = new PropertiesBeanFactory("commontemplate-expression.properties");
ExpressionSetting setting = (ExpressionSetting)beanFactory.createBean(ExpressionSetting.class);
// 2. 自行实现变量解析器
VariableResolver variableResolver = ...
// 3. 通过配置创建引擎
ExpressionEngine engine = new ExpressionEngine(setting);
// 4. 解析表达式
String expr = "1 + 1"; // 待求值的表达式
Expression expression = engine.parseExpression("1 + 1");
// 5. 运行求值
Object result = expression.evaluate(variableResolver);
System.out.println(expr + " = " + result);
总结
应该重用方式已经非常简单了,
当然可以加几个工具类,把配置再封装一下,
或再提供一个用Map实现的最简单VariableResolver等,
但这些都不是必需的,
另外,是否应该将el相关的放到一个package下,我觉得没必要,
那会影响整个项目的package层级关系,
倒是可以写一个Ant脚本,抽取相关类,打包成commontemplate-el.jar
缓存同步策略重构
2007-11-23 https://www.iteye.com/blog/javatar-143090
简述一下CommonTemplate(http://www.commontemplate.org)的模板工厂每次获取模板的过程如下:
检查内存缓存中是否存在,
若不存在,则检查持久化缓存中是否存在,
若还不存在,则重新解析模板并将模板压入内存缓存及持久化缓存,
若存在,则检查是否需要热加载,
若需要热加载,则对比文件是否已更改,
若已更改,则重新解析模板并将模板压入内存缓存及持久化缓存,返回模板
因为很多检测点,开始整个过程都被同步执行了, 和huangyh讨论时, 他提出,如果去掉同步,会出现什么?
总结是: 一、在并发下,同一模板会多次被解析和缓存, 二、非同步缓存容器保存时可能出错,
讨论后发现:
第一种影响对程序的正确性没什么影响的,只是“觉得”多次解析和缓存会影响性能, 而实际上,这些影响性能比同步块对引擎活性的影响要小很多, 既然如此,那只要保证缓存容器自身是线程安全的,就没必要同步整个获取过程。
缓存容器采用的是策略模式, 引擎留出的SPI是:org.commontemplate.config.Cache接口,重构后:
- 第一种方式: 在Cache接口的文档中声明,Cache的实现者都必须线程安全,即自行实现同步。
- 第二种方式: 引擎在调用Cache之前,使用装饰器模式统一外包装实现同步。
- 第三种方式: 结合上面两种方式,再加一个是否需要外部同步的标识性接口,引擎基于此判断是否需要外部同步。
Java内存模型happens-before法则
2007-11-29 https://www.iteye.com/blog/javatar-144763
Happens-Before 规则
- 程序次序规则 (Program order rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。
- 管程锁定规则 (Monitor lock rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
- Volatile 变量规则 (Volatile variable rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
- 线程启动规则 (Thread start rule):Thread 对象的
start()方法先行发生于此线程的每一个动作。 - 线程终止规则 (Thread termination rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过
Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。 - 线程中断规则 (Interruption rule):对线程
interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生(可以通过interrupted()方法检测到)。 - 对象终结规则 (Finalizer rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的
finalize()方法的开始。 - 传递性 (Transitivity):如果操作 A 先行发生于操作 B,且操作 B 先行发生于操作 C,那么操作 A 必然先行发生于操作 C。
什么是 Happens-Before?
Happens-before 就是“什么什么一定在什么什么之前运行”,也就是保证顺序性。因为 CPU 是可以不按我们写代码的顺序执行内存的存取过程的,也就是指令会乱序或并行运行,只有在上述规定的情况下,才保证顺序性。
指令重排序示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
private int a = 0;
private long b = 0;
public void set() {
a = 1;
b = -1;
}
public void check() {
if (! ((b == 0) || (b == -1 && a == 1)))
throw new Exception("check Error!");
}
}
对于 set() 方法的执行,可能会发生以下情况:
- 编译器重排:编译器可以重新安排语句执行顺序,使
b在a之前赋值。 - 处理器重排:处理器可以改变机器指令执行顺序,甚至同时执行。
- 存储系统重排:存储系统可能重新安排写操作顺序。例如在 32 位机器上,
long类型变量b可能先写高位,再写a,最后写b的低位。 - 缓存可见性:编译器、处理器和存储系统可能使变量值暂存在寄存器中,直到后续调用才更新到主内存。
在单线程情况下,check() 永远不会报错,但在非同步多线程运行时却很有可能。
线程可见性问题
多个 CPU 之间的缓存不保证实时同步。只有在 synchronized、volatile 或 final 的情况下才能保证正确性。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
private int n;
public void set(int n) {
this.n = n;
}
public void check() {
if (n != n)
throw new Exception("check Error!");
}
}
在非同步时,n != n 这种看似不可能成立的条件是有可能发生的。
单例模式与 JMM
JMM 不保证创建过程的原子性,读写并发时可能看到不完整的对象。这也是“双重检查成例”在早期 Java 中行不通的原因(JDK 5.0 以后将 instance 声明为 volatile 则可行)。
方案一:非延迟加载单例类
1
2
3
4
5
6
7
8
9
10
public class Singleton {
private Singleton(){}
private static final Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
方案二:简单的同步延迟加载
1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static Singleton instance = null;
public static synchronized Singleton getInstance() {
if (instance == null)
instance = new Singleton();
return instance;
}
}
方案三:双重检查成例延迟加载 (DCL)(1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
private static volatile Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
方案四:类加载器延迟加载 (静态内部类)
1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static class Holder {
static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return Holder.instance;
}
}
Lambda 表达式语法思考
2007-12-17 https://www.iteye.com/blog/javatar-148780
CommonTemplate 最近实现了简单的 Lambda 表达式功能,但操作符语法未定。
语法可选方案
- 仿 Python,采用 “lambda”
- 示例:
list[lambda i : i > 0] - 特点:符合标准 Lambda 表达式定义格式,但较复杂,不够简洁。
- 示例:
**仿 JavaFX,采用 “ ”** - 示例:
list[i | i > 0] 特点:由于 “ ” 已作为“按位或”运算符,重载操作符可能导致优先级错误或引起歧义。
- 示例:
- 仿 C#,采用 “=>”
- 示例:
list[i => i > 0]
- 示例:
- 隐式变量
- 示例:
list[index > 0] - 特点:规定死命名(如假设用 “index”)。
- 示例:
多参数情况考虑
- 方案 A:
list[item,index | item ~ "[0-9]+" && index > 3] - 方案 B:
list[item,index => item ~ "[0-9]+" && index > 3]
与现有方案的功能重叠
现有方案与 Lambda 表达式在过滤器功能上存在重叠:
现有方案
1
2
3
4
5
list[1]
list[-1]
list[1..2]
list["value"] // 假设 list 中放的是 String 对象
list[name="james",role="admin"] // 假设 list 中放的是 User 对象
Lambda 表达式替换方案
(假设采用第 4 种方案,隐含变量:item, index, size)
1
2
3
4
5
list[index == 1]
list[index == size - 1]
list[index >= 1 && index <= 2]
list[item == "value"]
list[item.name == "james" && item.role == "admin"]
深度问题探讨
- 简化与完备性:Lambda 表达式逻辑完备性强,能表示更多情况,但原方案更直观简洁。
- 内递归推演:是否应该实现内递归推演(参考先前关于 内递归 的讨论)。
- 匿名回调类比:Lambda 表达式可看作匿名的回调函数。以过滤器功能为例,其逻辑类比于
File.listFiles(new FileFilter())。
Java 等价写法
1
2
3
4
5
subList = list.sub(new Filter() {
public boolean filter(Object item, int index, int size) {
return index > 0;
}
});
编译期模板区域定义
2007-12-18 https://www.iteye.com/blog/javatar-149131
CommonTemplate (http://www.commontemplate.org) 现在的所有区域信息定义(包括 zone, block, macro)都是动态产生的,而有些功能需要不执行模板就获取它的区域块。
局部包含功能需求
目前区域定义是在执行期产生的,名称甚至可以是变量,例如:
1
2
$zone{"body"}
$end
若要实现外部获取区域,需在编译期确定区域位置,通过 Template.getZone(String name) 获取。
静态区域定义语法方案
- 使用 “@” 前缀识别
1
2
3
$@body
...
$end
- 调用方式
1
2
$inline{"xxx.ctl@body"}
$include{"xxx.ctl@body"}
模板继承与覆写
静态区域可用于继承时的覆写:
- 显式覆写
1
2
3
4
5
6
$extends{"xxx.ctl"}
$override{@body}
$super
...
$end
$end
- 同名覆写策略(简化版)
1
2
3
4
5
6
$extends{"xxx.ctl"}
$@body
$super
...
$end
$end
Macro 与 Block 的静态化
目前 Macro 是运行期定义的,必须先定义再调用。若要实现位置任意(如在调用之后定义),需将其静态化。
Block 指令用法
1
2
3
4
5
6
7
8
9
$# block定义时不显示,需通过show显示,可多次显示。
$block{"xxx"}
...
$end
....
$# 在此位置显示block
$show{"xxx"}
静态 Block 方案
- API 支持:增加
Template.getBlock(String name)。 - 标识符:假设使用 “%” 作为前缀。
1
2
3
$%%xxx
...
$end
- 显示方式:
1
$show{\%xxx}
待考虑问题
- Macro 如何利用 Block 和 Zone 定义。
- Macro 是否需要另使用前缀标识,以及是否会导致标记过多。
- 是否采用声明式语法,例如:
1
2
$using{$\%xxx}
$using{@xxx}
/20260119103922276.png)