研发的那些事2—设计之惑
设计真是件奇妙的事情,能造就璀璨的明珠,也能带来一堆万年不去核废料;能让人享受释放智慧的乐趣,也能品尝挫败的沮丧。Why?
设计的过程
工程角度,设计是一个过程,包含三种不同层次的活动:架构设计,概要设计和详细设计。三者由全局到局部,依次展开,逐渐深入细节,最终完成一个技术解决方案,给出可行的如何实现需求的答案。此三者的一般性过程如下:
架构设计
目标:定位全局,确定技术方案的方向、后续技术活动策略。
输入:需求文档。敏捷过程常常是是一组用户故事。
输出:架构设计文档。敏捷过程称之为应用全局视图Application Overview。
角色:架构师
任务:
1.分析需求,识别关键功能、质量需求和约束
2.分析目标系统为达成以上目的,需要哪些流程,流程中有多少环节,各环节涉及哪些角色,这些角色的职责及他们之间的协作关系。
3.将角色按逻辑分类抽象,变成子系统。角色的职责即是子系统的接口,角色间的协作关系即为各子系统间的关系。
4.各子系统内部,根据职责拆分模块,确定它们的交互方式。
5.针对关键功能,确定完成这些功能的流程,在时序上由各子系统,模块协同工作的顺序。
6.考虑各子系统的部署方式。
7.将各子系统、模块转换成实现语言的元素。如对于.net,规划各子系统有哪些namespace,namespace如何分配到assembly,assembly之间的依赖关系。再将assembly分配到project中。期间需要注意避免循环依赖,明确接口(作为接口的assembly和project)。
8.规划project在SCM中的组织结构。
9.根据质量需求确定全局性的技术策略(方案):如线程控制,错误处理,数据存储等。
10.分析其余需求,验证当前方案是否能支持,根据发现的问题做必要调整。
概要设计
目标:在具体实现语言层面上展开工作,确定每个project的主要类及交互过程和总体算法。
输入:架构设计文档和需求。
输出:概要设计文档。
角色:高级程序员
任务:
1.设计提供暴露服务的接口的具体实现类,补充所需的相类,通常可按界面(接口)、活动和存储三方面考虑。进一步落实对项目外接口的依赖关系和位置。
2.设计算法实现接口暴露的相关功能。
详细设计
目标:确定概设提出的每个方法的具体实现。
输入:概要设计
输出:伪代码。
角色:程序员
任务:用伪代码描述每个方法。
这里看到的是一个按需求->系统->子系统->模块->类->方法,自顶向下逐步求精的过程。
设计的技术
技术角度,设计是一系列方法和工具,通过应用这些方法和工具给出一个可行解决方案的描述。常用方法有:
OO:面向对象方法。其目的是统一问题的描述与解决方法,通过一致的抽象提高开发效率。核心思想是,将问题空间映射到计算机模型上,在计算机中建立一个同我们日常感知世界相同的模型,解决问题。实践中,通过类描述问题空间的概念,通过类的消息描述这些概念的交互形成一个模型,再将模型落实到OOP的语言中,如C++,C#,Java等。
AOP:面向方面的方法。期望将主要业务同这些业务中散落的支撑功能(服务)如日志,权限等分离。结合OOP应用时,通过横切点定义跨多个类的支持服务,由方面(特殊类)实现这些服务,再通过切入点连接方面与横切点,达到运行时自动提供服务的目的。
DDD:领域驱动的设计。采用传统OO方法时,一般的需求分析结果是用自然语言描述的与OOD存在脱节,容易生造出问题域中不存在的概念,建立与实际需求不一致的模型。因此,从需求分析起,首先为问题域即领域建模,完全不考虑如何设计、实现,用客户能理解的方法仅描述问题现状。常用的建模方法是OOA,模型可用UML表达,也可以用类似框图联系,总是以能让用户明白这就是问题的描述,方便沟通为原则。
在不断使用这些方法的过程中,一些大师们发现了某些经常重复的解决方案,便对它们进行了提炼和总结,以便后来者这能利用这些经验,提高工作效率和质量,少走弯路。这些经验有:架构风格,架构模式,设计模式和反模式。
架构风格
架构层次的,根据系统的架构呈现出的总体特点进行架构分类的方式。主要的风格有:
1.客户-服务器:系统分为客户与服务端两部分,客户端发送请求,服务端执行并响应。
2.分层(级)架构:系统按关注点水平分层,每一层为上层提供一个抽象。近一步,可将层分布到不同计算机上。
3.面向对象:系统分成单独的可重用的对象,每个对象包含数据及处理它们的行为。
4.基于消息(事件):系统各部分通过发送和接收约定格式的消息工作,无需关心实际的收发者。
5.面向服务(SOA):系统通过约定的契约暴露功能,并根据这些契约工作。
6.基于组件:系统分解为逻辑的可重用位置透明的组件,通过明确定义的通信接口工作。
7.管道-过滤器:由管道连接过滤器一组,处理流经的数据。
8.微内核:分离最小功能核心和可扩展部分。
它们在最高层次根据问题的类型给出了一般的解决方向。如:
1.通信问题:基于消息、管道-过滤器
2.部署问题:客户/服务器、分层架构
3.重用与可扩展:OO、基于组件、SOA
但是,不同风格并不是互相排斥的,相反,一个实际系统通常同时呈现出多种风格。如一个分布系统,功能可通过语言无关的契约暴露,用OOP实现这些契约,实现对象又被组织成一个个组件,每个组件定义了彼此的通信接口,而通信又可是基于消息的,组件本身运行在一个支持插件的容器中,可随时添加新组件,提供新服务。这里表现出了SOA、OO、Component、Messaging和Mico-Kernel多种风格。
架构模式
一系列可重用的架构设计方案,每个方案在满足适用场景的前提下,解决一种或一类问题。经典的POSA给出了一些常用的模式分类:
1.服务访问和配置:包装器(Wrapper Facade)、组件配置器(Component Configurator)、截取器(Interrceptor)、扩展接口(Extension Interface)
2.事件处理:反应器(Reactor)、主动器(Proactor)、异步完成标记(ACT)、接收-连接器(Acceptor-Connector)
3.同步:界定枷锁(Scoped Locking)、策略化加锁(Strategized Locking)、线程安全接口(Thread-Safe Interface)、双检查加锁优化(Double-Checked Locking Optimization)
4.并发:主动对象(Active Object)、监视器对象(Monitor Object)、半同步/半异步(Half-Sync/Half-Asynce)、领导者/追随者(Leader/Followers)、线程特定存储器(Thread-Specific Storage)
5.资源获取:查找(Lookup)、懒加载(Lazy Acquistion)、预加载(Eager Acquistion)、分步加载(Partial Acquisition)
6.资源生命周期:缓存(Caching)、池(Pooling)、协调器(Coordinator)、资源生命周期管理(Resource Lifecycle Manager)
7.资源释放:租约(Leasing)、清除者(Evictor)
此类模式给出了全局性问题的一般处理方案,大都是关于子系统、模块及相互之间关系的粗粒度的描述。
设计模式与反模式
设计模式指OO的设计模式,是可反复使用的代码经验总结。通过GoF经典的《设计模式》广为人知。GoF将它们分类为:
1.创建型:简单工厂(Simple Factory)、工厂方法(Factory Method)、抽象工厂(Abstract Factory)、创建者(Builder)、原型(Prototype)、单例(Singleton)
2.结构型:外观(Facade)、适配器(Adapter)、代理(Proxy)、装饰(Decorator)、桥接(Bridge)、组合(Composite)、享元(Flyweight)
3.行为型:模板方法(Template Method)观察者(Observer)、状态(State)、策略(Strategy)、职责链(Chain of Responsibility)、访问者(Visitor)、调停者(Mediator)、备忘录(Memento)、迭代器(Iterator)、解释器(Interpreter)
它们在代码层面给出解决上述三类问题的一般做法及使用场景。
设计模式如红日般普照大地,光芒万丈,导致做OO的言必称设计模式,不用上几个都不好意思拿去见人。免不了被乱用、误用,明明需要避光保存的,偏偏加个LED增加照明,还曰节能、低碳。所以不得不需要反模式来拨乱反正。反模式说明了,当在错误的时间,错误的地点,使用了错误设计模式后,出现的严重后果,提醒人们过犹不及。
设计的人员
今天,从人员角度,设计是一系列扮演不同角色的人员的协作,他们通过某种过程,应用某些技术,相互配合,共同完成一个解决方案。一个采用传统设计过程的大型系统涉及的角色通常有:
1.架构师:一个人或者一个团队,负责将系统分解成子系统和模块,去顶它们之间的关系(开发期、运行期)并制定相关的技术决策,如部署、开发、性能等。
2.高级程序员(设计师):负责完成一个或多个子系统、模块的概要设计。
3.程序员:负责详细设计。
4.项目经理:负责整个活动的任务协调,并根据架构安排开发任务。
其中,架构师是核心,其工作成果是后续管理和实现的基石。有什么样的架构,便会有什么样的开发组织结构。如分层架构,必然会存在界面、业务、持久化及公共模块的开发职责分配,由不同人(团队)完成不同层的工作。
现代软件,因为规模和复杂性,再也无法由个人独立完成所有工作,必须依靠协作。协作首先需要分工,明确各工种的职责,个人依照职责行事。分工之后便有了工作的先后次序,不同次序的串联需要一定规则,便形成了一些过程规范,大家依照规范协同。所以,才有了那么多的软件工程方法论,开发才有了架构师,设计师和程序员的细分。不能简单的认为架构师>设计师>程序员,他们主要的区别在于工作范围的广度和深度的侧重点不同。架构师更广,程序员工作的更深入。
为了使用一致的思维考虑问题与问题的解决方法,诞生了OOP。为了分离业务与支持服务,让不同的人在不同的时间和地方分别解决不同问题,诞生了AOP。为了便于开发人员与用户达成待解决问题的一致认识,诞生了DDD。
设计时无论采用何种种过程、技术和人员组织方式,根本目的只有一个:给出关于需求的技术解决方案。
实际工作中还会碰到一个严重的问题,常常发现,要解决的问题,并不是地上的石头,静静的躺在那儿,等你照剑谱挥剑的。经常是,哦,我要砍的不是这块,是那块,甚至不是碎石而是需要劈柴,一身武艺无处使。不由怒从心头,不时问候需求人员或者客户,干嘛不一次说清楚,写的仔细点。害我改这改那儿的。对此,Brooks在《设计原本》说:设计的本质是帮助客户发现他们想要的需求。
设计的本质!
根本上,解决方案和问题是共同变化的,甚至会相互影响。现实中,需求阶段给出的需求,往往是初始需求,随设计过程的推进,它会奇妙的发生一些变化:
1.需求描述更精确了:随着设计深入,发现原来的描述存在模糊的方,需要更精确才能做出设计决策。
2.需求描述错了:设计着,突然,卡住了,一交流,发现,哦原来这不是用户要的,他们要那样的,其实很简单。
3.出现新需求:会发现之前不曾注意的需求,会加入系统性的需求,如缓存等。
设计必须能适应这些变化,有些需要通过技术方法,柔性的容纳新变化,将变化点抽象成接口,隔离变化,新需求只要设计成新的实现了即可。有些则只能通过总体过程来适应,如敏捷过程的高迭代,分批交付,在每个迭代间能响应变化。企图一次就做出美妙的设计是不现实的,设计必须具备响应变化的能力。因此,设计人员需要:
- KISS:时刻注意保持简单性,简单的方法往往也是最正确高效的。
- 关注需求:时刻注意什么是真正的需求,遇到困难,不妨先想想,真的需要解决这个问题吗,能换种方式吗?
- 适度的远见:预见同类需求发生的可能性,并提前考虑对策。如看到报表需要导出成excel,想想是否有生成pdf的可能性,如有必要尽早隔离这种变化。
- 系统性思维:时刻注意用需求去验证设计,确认设计是否满足需求,满足的是否牵强,是否因假想了需求而增加了额外的复杂性。
- 全局性思维:思考设计会对开发、测试和部署运营造成什么样的影响,因为这些方面往往存在致命的隐含需求。
- 提升抽象层次:从一次一个系统转换到一次一个系统族,考虑所有同类系统的共性和可变性,将共性做成框架,可变性提炼成配置,DSL留到具体项目实施时完成。