<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:series="http://unfoldingneurons.com/"
	>

<channel>
	<title>朱文昊 Albert Zhu &#187; 设计</title>
	<atom:link href="http://zhuwenhao.com/category/%e6%8a%80%e6%9c%af/%e8%bd%af%e4%bb%b6%e5%b7%a5%e7%a8%8b/%e8%ae%be%e8%ae%a1/feed/" rel="self" type="application/rss+xml" />
	<link>http://zhuwenhao.com</link>
	<description>朱文昊的中文博客－－专注技术，向往自由</description>
	<lastBuildDate>Sun, 22 Jan 2012 13:47:56 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>一些软件设计的原则</title>
		<link>http://zhuwenhao.com/771/%e6%8a%80%e6%9c%af/%e8%bd%af%e4%bb%b6%e5%b7%a5%e7%a8%8b/%e8%ae%be%e8%ae%a1/%e4%b8%80%e4%ba%9b%e8%bd%af%e4%bb%b6%e8%ae%be%e8%ae%a1%e7%9a%84%e5%8e%9f%e5%88%99/</link>
		<comments>http://zhuwenhao.com/771/%e6%8a%80%e6%9c%af/%e8%bd%af%e4%bb%b6%e5%b7%a5%e7%a8%8b/%e8%ae%be%e8%ae%a1/%e4%b8%80%e4%ba%9b%e8%bd%af%e4%bb%b6%e8%ae%be%e8%ae%a1%e7%9a%84%e5%8e%9f%e5%88%99/#comments</comments>
		<pubDate>Mon, 25 Apr 2011 06:27:18 +0000</pubDate>
		<dc:creator>朱文昊 Albert Zhu</dc:creator>
				<category><![CDATA[设计]]></category>

		<guid isPermaLink="false">http://zhuwenhao.com/?p=771</guid>
		<description><![CDATA[本文是专题：程序员修炼之路中的第6篇，共6篇转载自 http://coolshell.cn/articles/4535.html
作者  [...]]]></description>
			<content:encoded><![CDATA[<p>转载自 http://coolshell.cn/articles/4535.html</p>
<p>作者 陈皓</p>
<p>以前本站向大家介绍过一些软件开发的原则，比如<a title="优质代码的十诫" rel="bookmark" href="http://coolshell.cn/articles/1007.html" target="_blank">优质代码的十诫</a>和<a title="Unix传奇(下篇)" href="http://coolshell.cn/articles/2324.html" target="_blank">Unix传奇(下篇)</a>中所以说的UNIX的设计原则。相信大家从中能够从中学了解到一些设计原理方面的知识，正如我在《<a title="再谈“我是怎么招聘程序员的”（上）" href="http://coolshell.cn/articles/4506.html" target="_blank">再谈“我是怎么招聘程序”</a>》中所说的，一个好的程序员通常由其操作技能、知识水平，经验层力和能力四个方面组成。在这里想和大家说说设计中的一些原则，我认为这些东西属于长期经验总结出来的知识。这些原则，每一个程序员都应该了解。但是请不要教条主义，在使用的时候还是要多多考虑实际情况。其实，<strong>下面这些原则，不单单只是软件开发，可以推广到其它生产活动中，甚至我们的生活中</strong>。</p>
<h4>Don’t Repeat Yourself (DRY)</h4>
<p>DRY  是一个最简单的法则，也是最容易被理解的。但它也可能是最难被应用的（因为要做到这样，我们需要在泛型设计上做相当的努力，这并不是一件容易的事）。它意 味着，当我们在两个或多个地方的时候发现一些相似的代码的时候，我们需要把他们的共性抽象出来形一个唯一的新方法，并且改变现有的地方的代码让他们以一些 合适的参数调用这个新的方法。</p>
<p><strong>参考</strong>：<a title="http://en.wikipedia.org/wiki/Don%27t_repeat_yourself" rel="nofollow" href="http://en.wikipedia.org/wiki/Don%27t_repeat_yourself">http://en.wikipedia.org/wiki/Don%27t_repeat_yourself</a></p>
<h4>Keep It Simple, Stupid (KISS)</h4>
<p>KISS原则在设计上可能最被推崇的，在家装设计，界面设计 ，操作设计上，复杂的东西越来越被众人所BS了，而简单的东西越来越被人所认可，比如<a title="UI的恶梦" href="http://coolshell.cn/articles/1907.html" target="_blank">这些UI的设计</a>和我们<a title="为什么中国的网页设计那么烂？" href="http://coolshell.cn/articles/3605.html" target="_blank">中国网页</a>（尤其是<a title="微软用新浪来当反面教材" href="http://coolshell.cn/articles/3872.html" target="_blank">新浪的网页</a>） 者是负面的例子。“宜家”（IKEA）简约、效率的家居设计、生产思路；“微软”（Microsoft）“所见即所得”的理念；“谷歌”（Google) 简约、直接的商业风格，无一例外的遵循了“kiss”原则，也正是“kiss”原则，成就了这些看似神奇的商业经典。而苹果公司的iPhone/iPad 将这个原则实践到了极至。</p>
<p>&nbsp;</p>
<p>把一个事情搞复杂是一件简单的事，但要把一个复杂的事变简单，这是一件复杂的事。</p>
<p><strong>参考</strong>：<a title="http://en.wikipedia.org/wiki/KISS_principle" rel="nofollow" href="http://en.wikipedia.org/wiki/KISS_principle">http://en.wikipedia.org/wiki/KISS_principle</a></p>
<h4>Program to an interface, not an implementation</h4>
<p>这是设计模式中最根本的哲学，注重接口，而不是实现，依赖接口，而不是实现。接口是抽象是稳定的，实现则是多种多样的。以后面我们会面向对象的SOLID原则中会提到我们的依赖倒置原则，就是这个原则的的另一种样子。还有一条原则叫 <strong>Composition over inheritance</strong>（喜欢组合而不是继承），这两条是那23个经典设计模式中的设计原则。</p>
<h4>Command-Query Separation (CQS)  – 命令-查询分离原则</h4>
<ul>
<li>查询：当一个方法返回一个值来回应一个问题的时候，它就具有查询的性质；</li>
<li>命令：当一个方法要改变对象的状态的时候，它就具有命令的性质；</li>
</ul>
<p>通常，一个方法可能是纯的Command模式或者是纯的Query模式，或者是两者的混合体。在设计接口时，如果可能，应该尽量使接口单一化，保证 方法的行为严格的是命令或者是查询，这样查询方法不会改变对象的状态，没有副作用，而会改变对象的状态的方法不可能有返回值。也就是说：如果我们要问一个 问题，那么就不应该影响到它的答案。实际应用，要视具体情况而定，语义的清晰性和使用的简单性之间需要权衡。将Command和Query功能合并入一个 方法，方便了客户的使用，但是，降低了清晰性，而且，可能不便于基于断言的程序设计并且需要一个变量来保存查询结果。</p>
<p>在系统设计中，很多系统也是以这样原则设计的，查询的功能和命令功能的系统分离，这样有则于系统性能，也有利于系统的安全性。</p>
<p><strong>参考</strong>：<a title="http://en.wikipedia.org/wiki/Command-query_separation" rel="nofollow" href="http://en.wikipedia.org/wiki/Command-query_separation">http://en.wikipedia.org/wiki/Command-query_separation</a></p>
<h4>You Ain’t Gonna Need It (YAGNI)</h4>
<p>这个原则简而言之为——只考虑和设计必须的功能，避免过度设计。只实现目前需要的功能，在以后您需要更多功能时，可以再进行添加。</p>
<ul>
<li>如无必要，勿增复杂性。</li>
<li>软件开发先是一场沟通博弈。</li>
</ul>
<p>以前本站有一篇关于<a title="代码重构的一个示例" href="http://coolshell.cn/articles/3005.html" target="_blank">过度重构的文章</a>，这个示例就是这个原则的反例。而，WebSphere的设计者就<a href="http://www.bbc.co.uk/news/business-11944966" target="_blank">表示过他过度设计了这个产品</a>。我们的程序员或是架构师在设计系统的时候，会考虑很多扩展性的东西，导致在架构与设计方面使用了大量折衷，最后导致项目失败。这是个令人感到讽刺的教训，因为本来希望尽可能延长项目的生命周期，结果反而缩短了生命周期。</p>
<p><strong>参考</strong>：<a title="http://en.wikipedia.org/wiki/You_Ain%27t_Gonna_Need_It" rel="nofollow" href="http://en.wikipedia.org/wiki/You_Ain%27t_Gonna_Need_It" target="_blank">http://en.wikipedia.org/wiki/You_Ain%27t_Gonna_Need_It</a></p>
<h4><span id="more-771"></span>Law of Demeter – 迪米特法则</h4>
<p>迪米特法则(Law of Demeter)，又称“最少知识原则”（Principle of Least  Knowledge），其来源于1987年荷兰大学的一个叫做Demeter的项目。Craig Larman把Law of  Demeter又称作“不要和陌生人说话”。在《程序员修炼之道》中讲LoD的那一章叫作“解耦合与迪米特法则”。关于迪米特法则有一些很形象的比喻：</p>
<ul>
<li>如果你想让你的狗跑的话，你会对狗狗说还是对四条狗腿说？</li>
<li>如果你去店里买东西，你会把钱交给店员，还是会把钱包交给店员让他自己拿？</li>
</ul>
<p>和狗的四肢说话？让店员自己从钱包里拿钱？这听起来有点荒唐，不过在我们的代码里这几乎是见怪不怪的事情了。</p>
<p>对于LoD，正式的表述如下：</p>
<blockquote><p>对于对象 ‘O’ 中一个方法’M&#8217;，M应该只能够访问以下对象中的方法：</p>
<ol>
<li>对象O；</li>
<li>与O直接相关的Component Object；</li>
<li>由方法M创建或者实例化的对象；</li>
<li>作为方法M的参数的对象。</li>
</ol>
</blockquote>
<p>在《Clean Code》一书中，有一段Apache framework中的一段违反了LoD的代码：</p>
<p>final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();</p>
<p>这么长的一串对其它对象的细节，以及细节的细节，细节的细节的细节……的调用，增加了耦合，使得代码结构复杂、僵化，难以扩展和维护。</p>
<p>在《重构》一书中的代码的环味道中有一种叫做“Feature Envy”(依恋情结），形象的描述了一种违反了LoC的情况。Feature  Envy就是说一个对象对其它对象的内容更有兴趣，也就是说老是羡慕别的对象的成员、结构或者功能，大老远的调用人家的东西。这样的结构显然是不合理的。 我们的程序应该写得比较“害羞”。不能像前面例子中的那个不把自己当外人的店员一样，拿过客人的钱包自己把钱拿出来。“害羞”的程序只和自己最近的朋友交 谈。这种情况下应该调整程序的结构，让那个对象自己拥有它羡慕的feature，或者使用合理的设计模式（例如Facade和Mediator）。</p>
<p><strong>参考</strong>：<a title="http://en.wikipedia.org/wiki/Principle_of_Least_Knowledge" rel="nofollow" href="http://en.wikipedia.org/wiki/Principle_of_Least_Knowledge">http://en.wikipedia.org/wiki/Principle_of_Least_Knowledge</a></p>
<h4>面向对象的S.O.L.I.D 原则</h4>
<p>一般来说这是面向对象的五大设计原则，但是，我觉得这些原则可适用于所有的软件开发。</p>
<p><strong>Single Responsibility Principle (SRP) – 职责单一原则</strong></p>
<p>关于单一职责原则，其核心的思想是：<strong>一个类，只做一件事，并把这件事做好，其只有一个引起它变化的原因</strong>。单一职 责原则可以看作是低耦合、高内聚在面向对象原则上的引申，将职责定义为引起变化的原因，以提高内聚性来减少引起变化的原因。职责过多，可能引起它变化的原 因就越多，这将导致职责依赖，相互之间就产生影响，从而极大的损伤其内聚性和耦合度。单一职责，通常意味着单一的功能，因此不要为一个模块实现过多的功能 点，以保证实体只有一个引起它变化的原因。</p>
<ul>
<li>Unix/Linux是这一原则的完美体现者。各个程序都独立负责一个单一的事。</li>
<li>Windows是这一原则的反面示例。几乎所有的程序都交织耦合在一起。</li>
</ul>
<p><strong>Open/Closed Principle (OCP) – 开闭原则</strong></p>
<p>关于开发封闭原则，其核心的思想是：模块是可扩展的，而不可修改的。也就是说，<strong>对扩展是开放的，而对修改是封闭的</strong>。</p>
<ul>
<li>对扩展开放，意味着有新的需求或变化时，可以对现有代码进行扩展，以适应新的情况。</li>
<li>对修改封闭，意味着类一旦设计完成，就可以独立完成其工作，而不要对类进行任何修改。</li>
</ul>
<p>对于面向对象来说，需要你依赖抽象，而不是实现，23个经典设计模式中的“策略模式”就是这个实现。对于非面向对象编程，一些API需要你传入一个 你可以扩展的函数，比如我们的C  语言的qsort()允许你提供一个“比较器”，STL中的容器类的内存分配，ACE中的多线程的各种锁。对于软件方面，浏览器的各种插件属于这个原则的 实践。</p>
<p><strong>Liskov substitution principle (LSP) – 里氏代换原则</strong></p>
<p>软件工程大师Robert C. Martin把里氏代换原则最终简化为一句话：“Subtypes must be substitutable  for their base  types”。也就是，子类必须能够替换成它们的基类。即：子类应该可以替换任何基类能够出现的地方，并且经过替换以后，代码还能正常工作。另外，不应该 在代码中出现if/else之类对子类类型进行判断的条件。里氏替换原则LSP是使代码符合开闭原则的一个重要保证。正是由于子类型的可替换性才使得父类 型的模块在无需修改的情况下就可以扩展。</p>
<p>这么说来，似乎有点教条化，我非常建议大家看看这个原则个两个最经典的案例——“正方形不是长方形”和“鸵鸟不是鸟”。通过这两个案例，你会明白 《墨子 小取》中说的  ——“娣，美人也，爱娣，非爱美人也….盗，人也；恶盗，非恶人也。”——妹妹虽然是美人，但喜欢妹妹并不代表喜欢美人。盗贼是人，但讨厌盗贼也并不代表 就讨厌人类。<strong>这个原则让你考虑的不是语义上对象的间的关系，而是实际需求的环境</strong>。</p>
<p>在很多情况下，在设计初期我们类之间的关系不是很明确，LSP则给了我们一个判断和设计类之间关系的基准：需不需要继承，以及怎样设计继承关系。</p>
<p><strong>Interface Segregation Principle (ISP) – 接口隔离原则</strong></p>
<p>接口隔离原则意思是把功能实现在接口中，而不是类中，使用多个专门的接口比使用单一的总接口要好。</p>
<p>举个例子，我们对电脑有不同的使用方式，比如：写作，通讯，看电影，打游戏，上网，编程，计算，数据等，如果我们把这些功能都声明在电脑的抽类里 面，那么，我们的上网本，PC机，服务器，笔记本的实现类都要实现所有的这些接口，这就显得太复杂了。所以，我们可以把其这些功能接口隔离开来，比如：工 作学习接口，编程开发接口，上网娱乐接口，计算和数据服务接口，这样，我们的不同功能的电脑就可以有所选择地继承这些接口。</p>
<p>这个原则可以提升我们“搭积木式”的软件开发。对于设计来说，Java中的各种Event Listener和Adapter，对于软件开发来说，不同的用户权限有不同的功能，不同的版本有不同的功能，都是这个原则的应用。</p>
<p><strong>Dependency Inversion Principle (DIP) – 依赖倒置原则</strong></p>
<p>高层模块不应该依赖于低层模块的实现，而是依赖于高层抽象。</p>
<p>举个例子，墙面的开关不应该依赖于电灯的开关实现，而是应该依赖于一个抽象的开关的标准接口，这样，当我们扩展程序的时候，我们的开关同样可以控制 其它不同的灯，甚至不同的电器。也就是说，电灯和其它电器继承并实现我们的标准开关接口，而我们的开关产商就可不需要关于其要控制什么样的设备，只需要关 心那个标准的开关标准。这就是依赖倒置原则。</p>
<p>这就好像浏览器并不依赖于后面的web服务器，其只依赖于HTTP协议。这个原则实在是太重要了，社会的分工化，标准化都是这个设计原则的体现。</p>
<p><strong>参考</strong>：<a href="http://en.wikipedia.org/wiki/Solid_%28object-oriented_design%29">http://en.wikipedia.org/wiki/Solid_(object-oriented_design)</a></p>
<h4>Common Closure Principle（CCP）– 共同封闭原则</h4>
<p>一个包中所有的类应该对同一种类型的变化关闭。一个变化影响一个包，便影响了包中所有的类。一个更简短的说法是：一起修改的类，应该组合在一起（同 一个包里）。如果必须修改应用程序里的代码，我们希望所有的修改都发生在一个包里（修改关闭），而不是遍布在很多包里。CCP原则就是把因为某个同样的原 因而需要修改的所有类组合进一个包里。如果2个类从物理上或者从概念上联系得非常紧密，它们通常一起发生改变，那么它们应该属于同一个包。</p>
<p>CCP延伸了开闭原则（OCP）的“关闭”概念，当因为某个原因需要修改时，把需要修改的范围限制在一个最小范围内的包里。</p>
<p><strong>参考</strong>：<a href="http://c2.com/cgi/wiki?CommonClosurePrinciple">http://c2.com/cgi/wiki?CommonClosurePrinciple</a></p>
<h4>Common Reuse Principle (CRP) – 共同重用原则</h4>
<p>包的所有类被一起重用。如果你重用了其中的一个类，就重用全部。换个说法是，没有被一起重用的类不应该被组合在一起。CRP原则帮助我们决定哪些类 应该被放到同一个包里。依赖一个包就是依赖这个包所包含的一切。当一个包发生了改变，并发布新的版本，使用这个包的所有用户都必须在新的包环境下验证他们 的工作，即使被他们使用的部分没有发生任何改变。因为如果包中包含有未被使用的类，即使用户不关心该类是否改变，但用户还是不得不升级该包并对原来的功能 加以重新测试。</p>
<p>CCP则让系统的维护者受益。CCP让包尽可能大（CCP原则加入功能相关的类），CRP则让包尽可能小（CRP原则剔除不使用的类）。它们的出发点不一样，但不相互冲突。</p>
<p><strong>参考</strong>：<a href="http://c2.com/cgi/wiki?CommonReusePrinciple">http://c2.com/cgi/wiki?CommonReusePrinciple</a></p>
<h4>Hollywood Principle – 好莱坞原则</h4>
<p>好莱坞原则就是一句话——“don’t call us, we’ll call  you.”。意思是，好莱坞的经纪人们不希望你去联系他们，而是他们会在需要的时候来联系你。也就是说，所有的组件都是被动的，所有的组件初始化和调用都 由容器负责。组件处在一个容器当中，由容器负责管理。</p>
<p>简单的来讲，就是由容器控制程序之间的关系，而非传统实现中，由程序代码直接操控。这也就是所谓“控制反转”的概念所在：</p>
<ol>
<li>不创建对象，而是描述创建对象的方式。</li>
<li>在代码中，对象与服务没有直接联系，而是容器负责将这些联系在一起。</li>
</ol>
<p>控制权由应用代码中转到了外部容器，控制权的转移，是所谓反转。</p>
<p>好莱坞原则就是IoC（Inversion of Control）或DI（Dependency Injection  ）的基础原则。这个原则很像依赖倒置原则，依赖接口，而不是实例，但是这个原则要解决的是怎么把这个实例传入调用类中？你可能把其声明成成员，你可以通过 构造函数，你可以通过函数参数。但是 IoC可以让你通过配置文件，一个由Service Container  读取的配置文件来产生实际配置的类。但是程序也有可能变得不易读了，程序的性能也有可能还会下降。</p>
<p><strong>参考</strong>：</p>
<ul>
<li><a href="http://en.wikipedia.org/wiki/Hollywood_Principle">http://en.wikipedia.org/wiki/Hollywood_Principle</a></li>
<li><a href="http://en.wikipedia.org/wiki/Inversion_of_Control">http://en.wikipedia.org/wiki/Inversion_of_Control</a></li>
</ul>
<h4>High Cohesion &amp; Low/Loose coupling &amp; – 高内聚， 低耦合</h4>
<p>这个原则是UNIX操作系统设计的经典原则，把模块间的耦合降到最低，而努力让一个模块做到精益求精。</p>
<ul>
<li>内聚：一个模块内各个元素彼此结合的紧密程度</li>
<li>耦合：一个软件结构内不同模块之间互连程度的度量</li>
</ul>
<p>内聚意味着重用和独立，耦合意味着多米诺效应牵一发动全身。</p>
<p><strong>参考</strong>：</p>
<ul>
<li><a title="http://en.wikipedia.org/wiki/Coupling_(computer_science)" rel="nofollow" href="http://en.wikipedia.org/wiki/Coupling_%28computer_science%29">http://en.wikipedia.org/wiki/Coupling_(computer_science)</a></li>
<li><a title="http://en.wikipedia.org/wiki/Cohesion_(computer_science)" rel="nofollow" href="http://en.wikipedia.org/wiki/Cohesion_%28computer_science%29">http://en.wikipedia.org/wiki/Cohesion_(computer_science)</a></li>
</ul>
<h4>Convention over Configuration（CoC）– 惯例优于配置原则</h4>
<p>简单点说，就是将一些公认的配置方式和信息作为内部缺省的规则来使用。例如，Hibernate的映射文件，如果约定字段名和类属性一致的话，基本 上就可以不要这个配置文件了。你的应用只需要指定不convention的信息即可，从而减少了大量convention而又不得不花时间和精力啰里啰嗦 的东东。配置文件很多时候相当的影响开发效率。</p>
<p>Rails 中很少有配置文件（但不是没有，数据库连接就是一个配置文件），Rails 的fans号称期开发效率是 java 开发的 10  倍，估计就是这个原因。Maven也使用了CoC原则，当你执行mvn  -compile命令的时候，不需要指源文件放在什么地方，而编译以后的class文件放置在什么地方也没有指定，这就是CoC原则。</p>
<p><strong>参考</strong>：<a title="http://en.wikipedia.org/wiki/Convention_over_Configuration" rel="nofollow" href="http://en.wikipedia.org/wiki/Convention_over_Configuration">http://en.wikipedia.org/wiki/Convention_over_Configuration</a></p>
<h4>Separation of Concerns (SoC) – 关注点分离</h4>
<p>SoC  是计算机科学中最重要的努力目标之一。这个原则，就是在软件开发中，通过各种手段，将问题的各个关注点分开。如果一个问题能分解为独立且较小的问题，就是 相对较易解决的。问题太过于复杂，要解决问题需要关注的点太多，而程序员的能力是有限的，不能同时关注于问题的各个方面。正如程序员的记忆力相对于计算机 知识来说那么有限一样，程序员解决问题的能力相对于要解决的问题的复杂性也是一样的非常有限。在我们分析问题的时候，如果我们把所有的东西混在一起讨论， 那么就只会有一个结果——乱。</p>
<p>我记得在上一家公司有一个项目，讨论就讨论了1年多，项目本来不复杂，但是没有使用SoC，全部的东西混为一谈，再加上一堆程序员注入了各种不同的观点和想法，整个项目一下子就失控了。最后，本来一个1年的项目做了3年。</p>
<p>实现关注点分离的方法主要有两种，一种是标准化，另一种是抽象与包装。标准化就是制定一套标准，让使用者都遵守它，将人们的行为统一起来，这样使用 标准的人就不用担心别人会有很多种不同的实现，使自己的程序不能和别人的配合。Java  EE就是一个标准的大集合。每个开发者只需要关注于标准本身和他所在做的事情就行了。就像是开发镙丝钉的人只专注于开发镙丝钉就行了，而不用关注镙帽是怎 么生产的，反正镙帽和镙丝钉按标来就一定能合得上。不断地把程序的某些部分抽像差包装起来，也是实现关注点分离的好方法。一旦一个函数被抽像出来并实现 了，那么使用函数的人就不用关心这个函数是如何实现的，同样的，一旦一个类被抽像并实现了，类的使用者也不用再关注于这个类的内部是如何实现的。诸如组 件，分层，面向服务，等等这些概念都是在不同的层次上做抽像和包装，以使得使用者不用关心它的内部实现细节。</p>
<p>说白了还是“高内聚，低耦合”。</p>
<p><strong>参考</strong>：<a href="http://sulong.me/archives/99">http://sulong.me/archives/99</a></p>
<h4>Design by Contract (DbC) – 契约式设计</h4>
<p>DbC的核心思想是对软件系统中的元素之间相互合作以及“责任”与“义务”的比喻。这种比喻从商业活动中“客户”与“供应商”达成“契约”而得来。例如：</p>
<ul>
<li>供应商必须提供某种产品（责任），并且他有权期望客户已经付款（权利）。</li>
<li>客户必须付款（责任），并且有权得到产品（权利）。</li>
<li>契约双方必须履行那些对所有契约都有效的责任，如法律和规定等。</li>
</ul>
<p>同样的，如果在程序设计中一个模块提供了某种功能，那么它要：</p>
<ul>
<li>期望所有调用它的客户模块都保证一定的进入条件：这就是模块的先验条件（客户的义务和供应商的权利，这样它就不用去处理不满足先验条件的情况）。</li>
<li>保证退出时给出特定的属性：这就是模块的后验条件——（供应商的义务，显然也是客户的权利）。</li>
<li>在进入时假定，并在退出时保持一些特定的属性：不变式。</li>
</ul>
<p>契约就是这些权利和义务的正式形式。我们可以用“三个问题”来总结DbC，并且作为设计者要经常问：</p>
<ul>
<li>它期望的是什么？</li>
<li>它要保证的是什么？</li>
<li>它要保持的是什么？</li>
</ul>
<p>根据Bertrand  Meyer氏提出的DBC概念的描述，对于类的一个方法，都有一个前提条件以及一个后续条件，前提条件说明方法接受什么样的参数数据等，只有前提条件得到 满足时，这个方法才能被调用；同时后续条件用来说明这个方法完成时的状态，如果一个方法的执行会导致这个方法的后续条件不成立，那么这个方法也不应该正常 返回。</p>
<p>现在把前提条件以及后续条件应用到继承子类中，子类方法应该满足：</p>
<ol>
<li>前提条件不强于基类．</li>
<li>后续条件不弱于基类．</li>
</ol>
<p>换句话说，通过基类的接口调用一个对象时，用户只知道基类前提条件以及后续条件。因此继承类不得要求用户提供比基类方法要求的更强的前提条件，亦 即，继承类方法必须接受任何基类方法能接受的任何条件（参数）。同样，继承类必须顺从基类的所有后续条件，亦即，继承类方法的行为和输出不得违反由基类建 立起来的任何约束，不能让用户对继承类方法的输出感到困惑。</p>
<p>这样，我们就有了基于契约的LSP，基于契约的LSP是LSP的一种强化。</p>
<p><strong>参考</strong>：<a href="http://en.wikipedia.org/wiki/Design_by_contract">http://en.wikipedia.org/wiki/Design_by_contract</a></p>
<h4>Acyclic Dependencies Principle (ADP) – 无环依赖原则</h4>
<p>包之间的依赖结构必须是一个直接的无环图形，也就是说，在依赖结构中不允许出现环（循环依赖）。如果包的依赖形成了环状结构，怎么样打破这种循环依 赖呢？有2种方法可以打破这种循环依赖关系：第一种方法是创建新的包，如果A、B、C形成环路依赖，那么把这些共同类抽出来放在一个新的包D里。这样就把 C依赖A变成了C依赖D以及A依赖D，从而打破了循环依赖关系。第二种方法是使用DIP（依赖倒置原则）和ISP（接口分隔原则）设计原则。</p>
<p>无环依赖原则（ADP）为我们解决包之间的关系耦合问题。在设计模块时，不能有循环依赖。</p>
<p><strong>参考</strong>：<a href="http://c2.com/cgi/wiki?AcyclicDependenciesPrinciple">http://c2.com/cgi/wiki?AcyclicDependenciesPrinciple</a></p>
<h4>————————————————————————————</h4>
<p>上面这些原则可能有些学院派，也可能太为理论，我在这里说的也比较模糊和简单，这里只是给大家一个概貌，如果想要了解更多的东西，大家可以多google一下。</p>
<p>不过这些原则看上去都不难，但是要用好却并不那么容易。要能把这些原则用得好用得精，而不教条，我的经验如下：（我以为这是一个理论到应用的过程）</p>
<ol>
<li>你可以先粗浅或是表面地知道这些原则。</li>
<li>但不要急着马上就使用。</li>
<li>在工作学习中观察和总结别人或自己的设计。</li>
<li>再回过头来了回顾一下这些原则，相信你会有一些自己的心得。</li>
<li>有适度地去实践一下。</li>
<li>Goto第 3步。</li>
</ol>
<p>我相信可能还会有其实一些原则，欢迎大家提供。</p>
<p>（全文完）</p>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>http://zhuwenhao.com/771/%e6%8a%80%e6%9c%af/%e8%bd%af%e4%bb%b6%e5%b7%a5%e7%a8%8b/%e8%ae%be%e8%ae%a1/%e4%b8%80%e4%ba%9b%e8%bd%af%e4%bb%b6%e8%ae%be%e8%ae%a1%e7%9a%84%e5%8e%9f%e5%88%99/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
	
		<series:name><![CDATA[程序员修炼之路]]></series:name>
	</item>
		<item>
		<title>C语言嵌入式系统编程修炼之二:软件架构篇</title>
		<link>http://zhuwenhao.com/411/%e6%8a%80%e6%9c%af/%e8%bd%af%e4%bb%b6%e5%b7%a5%e7%a8%8b/%e8%ae%be%e8%ae%a1/c%e8%af%ad%e8%a8%80%e5%b5%8c%e5%85%a5%e5%bc%8f%e7%b3%bb%e7%bb%9f%e7%bc%96%e7%a8%8b%e4%bf%ae%e7%82%bc%e4%b9%8b%e4%ba%8c%e8%bd%af%e4%bb%b6%e6%9e%b6%e6%9e%84%e7%af%87/</link>
		<comments>http://zhuwenhao.com/411/%e6%8a%80%e6%9c%af/%e8%bd%af%e4%bb%b6%e5%b7%a5%e7%a8%8b/%e8%ae%be%e8%ae%a1/c%e8%af%ad%e8%a8%80%e5%b5%8c%e5%85%a5%e5%bc%8f%e7%b3%bb%e7%bb%9f%e7%bc%96%e7%a8%8b%e4%bf%ae%e7%82%bc%e4%b9%8b%e4%ba%8c%e8%bd%af%e4%bb%b6%e6%9e%b6%e6%9e%84%e7%af%87/#comments</comments>
		<pubDate>Sat, 12 Jun 2010 06:19:06 +0000</pubDate>
		<dc:creator>朱文昊 Albert Zhu</dc:creator>
				<category><![CDATA[设计]]></category>

		<guid isPermaLink="false">http://zhuwenhao.com/?p=411</guid>
		<description><![CDATA[作者：宋宝华　来源：天极网
朱文昊按语: 最近读到的一篇文章,收藏并作为一篇程序员修炼之路专辑中的一篇文章.
模块划分
 
模块划分的”划”是规划的意思，意指怎样合理的将一个很大的软件划分为一系列功能独立的部分合作完成系统的需求。C语言作为一种结构化的程序设计语言，在模块的划分上主要依据功能（依功能进行划分在面向对象设计中成为一个错误，牛顿定律遇到了相对论），C语言模块化程序设计需理解如下概念：
（1） 模块即是一个.c文件和一个.h文件的结合，头文件(.h)中是对于该模块接口的声明；
（2） 某模块提供给其它模块调用的外部函数及数据需在.h中文件中冠以extern关键字声明；
（3）  [...]]]></description>
			<content:encoded><![CDATA[<p>作者：宋宝华　来源：天极网</p>
<p>朱文昊按语: 最近读到的一篇文章,收藏并作为一篇程序员修炼之路专辑中的一篇文章.</p>
<p><strong>模块划分</strong></p>
<p><strong> </strong></p>
<p>模块划分的”划”是规划的意思，意指怎样合理的将一个很大的软件划分为一系列功能独立的部分合作完成系统的需求。C语言作为一种结构化的程序设计语言，在模块的划分上主要依据功能（依功能进行划分在面向对象设计中成为一个错误，牛顿定律遇到了相对论），C语言模块化程序设计需理解如下概念：</p>
<p>（1） 模块即是一个.c文件和一个.h文件的结合，头文件(.h)中是对于该模块接口的声明；</p>
<p>（2） 某模块提供给其它模块调用的外部函数及数据需在.h中文件中冠以extern关键字声明；</p>
<p>（3） 模块内的函数和全局变量需在.c文件开头冠以static关键字声明；</p>
<p>（4） 永远不要在.h文件中定义变量！定义变量和声明变量的区别在于定义会产生内存分配的操作，是汇编阶段的概念；而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。如：</p>
<table border="1" width="90%" align="center" bgcolor="#dadacf" bordercolor="#ffcc66">
<tbody>
<tr>
<td>/*module1.h*/<br />
int a = 5; /* 在模块1的.h文件中定义int a */</p>
<p>/*module1 .c*/<br />
#include “module1.h” /* 在模块1中包含模块1的.h文件 */</p>
<p>/*module2 .c*/<br />
#include “module1.h” /* 在模块2中包含模块1的.h文件 */</p>
<p>/*module3 .c*/<br />
#include “module1.h” /* 在模块3中包含模块1的.h文件 */</td>
</tr>
</tbody>
</table>
<p>以上程序的结果是在模块1、2、3中都定义了整型变量a，a在不同的模块中对应不同的地址单元，这个世界上从来不需要这样的程序。正确的做法是：</p>
<table border="1" width="90%" align="center" bgcolor="#dadacf" bordercolor="#ffcc66">
<tbody>
<tr>
<td>/*module1.h*/<br />
extern int a; /* 在模块1的.h文件中声明int a */</p>
<p>/*module1 .c*/<br />
#include “module1.h” /* 在模块1中包含模块1的.h文件 */<br />
int a = 5; /* 在模块1的.c文件中定义int a */</p>
<p>/*module2 .c*/<br />
#include “module1.h” /* 在模块2中包含模块1的.h文件 */</p>
<p>/*module3 .c*/<br />
#include “module1.h” /* 在模块3中包含模块1的.h文件 */</td>
</tr>
</tbody>
</table>
<p>这样如果模块1、2、3操作a的话，对应的是同一片内存单元。</p>
<p>一个嵌入式系统通常包括两类模块：</p>
<p>（1）硬件驱动模块，一种特定硬件对应一个模块；</p>
<p>（2）软件功能模块，其模块的划分应满足低偶合、高内聚的要求。</p>
<p><strong>多任务还是单任务</strong></p>
<p>所谓”单任务系统”是指该系统不能支持多任务并发操作，宏观串行地执行一个任务。而多任务系统则可以宏观并行（微观上可能串行）地”同时”执行多个任务。</p>
<p>多任务的并发执行通常依赖于一个多任务操作系统（OS），多任务OS的核心是系统调度器，它使用任务控制块（TCB）来管理任务调度功能。TCB包括任务的当前状态、优先级、要等待的事件或资源、任务程序码的起始地址、初始堆栈指针等信息。调度器在任务被激活时，要用到这些信息。此外，TCB还被用来存放任务的”上下文”（context)。任务的上下文就是当一个执行中的任务被停止时，所要保存的所有信息。通常，上下文就是计算机当前的状态，也即各个寄存器的内容。当发生任务切换时，当前运行的任务的上下文被存入TCB，并将要被执行的任务的上下文从它的TCB中取出，放入各个寄存器中。</p>
<p>嵌入式多任务OS的典型例子有Vxworks、ucLinux等。嵌入式OS并非遥不可及的神坛之物，我们可以用不到1000行代码实现一个针对80186处理器的功能最简单的OS内核，作者正准备进行此项工作，希望能将心得贡献给大家。</p>
<p>究竟选择多任务还是单任务方式，依赖于软件的体系是否庞大。例如，绝大多数手机程序都是多任务的，但也有一些小灵通的协议栈是单任务的，没有操作系统，它们的主程序轮流调用各个软件模块的处理程序，模拟多任务环境。</p>
<p><span id="more-411"></span></p>
<p><strong>单任务程序典型架构</strong></p>
<p><strong> </strong></p>
<p>（1）从CPU复位时的指定地址开始执行；</p>
<p>（2）跳转至汇编代码startup处执行；</p>
<p>（3）跳转至用户主程序main执行，在main中完成：</p>
<p>a.初试化各硬件设备；</p>
<p>b.初始化各软件模块；</p>
<p>c.进入死循环（无限循环），调用各模块的处理函数</p>
<p>用户主程序和各模块的处理函数都以C语言完成。用户主程序最后都进入了一个死循环，其首选方案是：</p>
<table border="1" width="90%" align="center" bgcolor="#dadacf" bordercolor="#ffcc66">
<tbody>
<tr>
<td>while(1)<br />
{<br />
}</td>
</tr>
</tbody>
</table>
<p>有的程序员这样写：</p>
<table border="1" width="90%" align="center" bgcolor="#dadacf" bordercolor="#ffcc66">
<tbody>
<tr>
<td>for(;;)<br />
{<br />
}</td>
</tr>
</tbody>
</table>
<p>这个语法没有确切表达代码的含义，我们从for(;;)看不出什么，只有弄明白for(;;)在C语言中意味着无条件循环才明白其意。</p>
<p>下面是几个”著名”的死循环：</p>
<p>（1）操作系统是死循环；</p>
<p>（2）WIN32程序是死循环；</p>
<p>（3）嵌入式系统软件是死循环；</p>
<p>（4）多线程程序的线程处理函数是死循环。</p>
<p>你可能会辩驳，大声说：”凡事都不是绝对的，2、3、4都可以不是死循环”。Yes，you are right，但是你得不到鲜花和掌声。实际上，这是一个没有太大意义的牛角尖，因为这个世界从来不需要一个处理完几个消息就喊着要OS杀死它的WIN32程序，不需要一个刚开始RUN就自行了断的嵌入式系统，不需要莫名其妙启动一个做一点事就干掉自己的线程。有时候，过于严谨制造的不是便利而是麻烦。君不见，五层的TCP/IP协议栈超越严谨的ISO/OSI七层协议栈大行其道成为事实上的标准？</p>
<p>经常有网友讨论：</p>
<table border="1" width="90%" align="center" bgcolor="#dadacf" bordercolor="#ffcc66">
<tbody>
<tr>
<td>printf(“%d,%d”,++i,i++); /* 输出是什么？*/<br />
c = a+++b; /* c=? */</td>
</tr>
</tbody>
</table>
<p>等类似问题。面对这些问题，我们只能发出由衷的感慨：世界上还有很多有意义的事情等着我们去消化摄入的食物。</p>
<p>实际上，嵌入式系统要运行到世界末日。</p>
<p><strong>中断服务程序</strong></p>
<p>中断是嵌入式系统中重要的组成部分，但是在标准C中不包含中断。许多编译开发商在标准C上增加了对中断的支持，提供新的关键字用于标示中断服务程序(ISR)，类似于__interrupt、#program interrupt等。当一个函数被定义为ISR的时候，编译器会自动为该函数增加中断服务程序所需要的中断现场入栈和出栈代码。</p>
<p>中断服务程序需要满足如下要求：</p>
<p>(1)不能返回值；</p>
<p>(2)不能向ISR传递参数；</p>
<p>(3) ISR应该尽可能的短小精悍；</p>
<p>(4) printf(char * lpFormatString,…)函数会带来重入和性能问题，不能在ISR中采用。</p>
<p>在某项目的开发中，我们设计了一个队列，在中断服务程序中，只是将中断类型添加入该队列中，在主程序的死循环中不断扫描中断队列是否有中断，有则取出队列中的第一个中断类型，进行相应处理。</p>
<table border="1" width="90%" align="center" bgcolor="#dadacf" bordercolor="#ffcc66">
<tbody>
<tr>
<td>/* 存放中断的队列 */<br />
typedef struct tagIntQueue<br />
{<br />
int intType; /* 中断类型 */<br />
struct tagIntQueue *next;<br />
}IntQueue;</p>
<p>IntQueue lpIntQueueHead;</p>
<p>__interrupt ISRexample ()<br />
{<br />
int intType;<br />
intType = GetSystemType();<br />
QueueAddTail(lpIntQueueHead, intType)；/* 在队列尾加入新的中断 */<br />
}</td>
</tr>
</tbody>
</table>
<p>在主程序循环中判断是否有中断：</p>
<table border="1" width="90%" align="center" bgcolor="#dadacf" bordercolor="#ffcc66">
<tbody>
<tr>
<td>While(1)<br />
{<br />
If( !IsIntQueueEmpty() )<br />
{<br />
intType = GetFirstInt();<br />
switch(intType) /* 是不是很象WIN32程序的消息解析函数? */<br />
{<br />
/* 对，我们的中断类型解析很类似于消息驱动 */<br />
case xxx: /* 我们称其为”中断驱动”吧？ */<br />
…<br />
break;<br />
case xxx:<br />
…<br />
break;<br />
…<br />
}<br />
}<br />
}</td>
</tr>
</tbody>
</table>
<p>按上述方法设计的中断服务程序很小，实际的工作都交由主程序执行了。</p>
<p><strong>硬件驱动模块</strong></p>
<p><strong> </strong></p>
<p>一个硬件驱动模块通常应包括如下函数：</p>
<p>（1）中断服务程序ISR</p>
<p>（2）硬件初始化</p>
<p>a.修改寄存器，设置硬件参数（如UART应设置其波特率，AD/DA设备应设置其采样速率等）；</p>
<p>b.将中断服务程序入口地址写入中断向量表：</p>
<table border="1" width="90%" align="center" bgcolor="#dadacf" bordercolor="#ffcc66">
<tbody>
<tr>
<td>/* 设置中断向量表 */<br />
m_myPtr = make_far_pointer(0l); /* 返回void far型指针void far * */<br />
m_myPtr += ITYPE_UART; /* ITYPE_UART： uart中断服务程序 */<br />
/* 相对于中断向量表首地址的偏移 */<br />
*m_myPtr = &amp;UART _Isr; /* UART _Isr：UART的中断服务程序 */</td>
</tr>
</tbody>
</table>
<p>（3）设置CPU针对该硬件的控制线</p>
<p>a.如果控制线可作PIO（可编程I/O）和控制信号用，则设置CPU内部对应寄存器使其作为控制信号；</p>
<p>b.设置CPU内部的针对该设备的中断屏蔽位，设置中断方式（电平触发还是边缘触发）。</p>
<p>（4）提供一系列针对该设备的操作接口函数。例如，对于LCD，其驱动模块应提供绘制像素、画线、绘制矩阵、显示字符点阵等函数；而对于实时钟，其驱动模块则需提供获取时间、设置时间等函数。</p>
<p><strong>C的面向对象化</strong></p>
<p>在面向对象的语言里面，出现了类的概念。类是对特定数据的特定操作的集合体。类包含了两个范畴：数据和操作。而C语言中的struct仅仅是数据的集合，我们可以利用函数指针将struct模拟为一个包含数据和操作的”类”。下面的C程序模拟了一个最简单的”类”：</p>
<table border="1" width="90%" align="center" bgcolor="#dadacf" bordercolor="#ffcc66">
<tbody>
<tr>
<td>#ifndef C_Class<br />
#define C_Class struct<br />
#endif<br />
C_Class A<br />
{<br />
C_Class A *A_this; /* this指针 */<br />
void (*Foo)(C_Class A *A_this); /* 行为：函数指针 */<br />
int a; /* 数据 */<br />
int b;<br />
};</td>
</tr>
</tbody>
</table>
<p>我们可以利用C语言模拟出面向对象的三个特性：封装、继承和多态，但是更多的时候，我们只是需要将数据与行为封装以解决软件结构混乱的问题。C模拟面向对象思想的目的不在于模拟行为本身，而在于解决某些情况下使用C语言编程时程序整体框架结构分散、数据和函数脱节的问题。我们在后续章节会看到这样的例子。</p>
<p><strong>总结</strong></p>
<p>本篇介绍了嵌入式系统编程软件架构方面的知识，主要包括模块划分、多任务还是单任务选取、单任务程序典型架构、中断服务程序、硬件驱动模块设计等，从宏观上给出了一个嵌入式系统软件所包含的主要元素。</p>
<p>请记住：软件结构是软件的灵魂！结构混乱的程序面目可憎，调试、测试、维护、升级都极度困难。</p>
]]></content:encoded>
			<wfw:commentRss>http://zhuwenhao.com/411/%e6%8a%80%e6%9c%af/%e8%bd%af%e4%bb%b6%e5%b7%a5%e7%a8%8b/%e8%ae%be%e8%ae%a1/c%e8%af%ad%e8%a8%80%e5%b5%8c%e5%85%a5%e5%bc%8f%e7%b3%bb%e7%bb%9f%e7%bc%96%e7%a8%8b%e4%bf%ae%e7%82%bc%e4%b9%8b%e4%ba%8c%e8%bd%af%e4%bb%b6%e6%9e%b6%e6%9e%84%e7%af%87/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
	
		<series:name><![CDATA[程序员修炼之路]]></series:name>
	</item>
	</channel>
</rss>

