如何写出优雅的代码

如何写出优雅的代码,第1张

如何写出优雅的代码

文章目录
  • 如何写出优雅的代码
  • 前言
  • 一、写出优雅代码的原则
    • 1.单一职责原则SRP
    • 2.最少知识原则
    • 3.开放-封闭原则
    • 4.依赖倒置原则
    • 5.里氏替换原则
  • 总结


前言

优雅的代码有几个特征:
1、可读性:代码是否容易看得懂
2、可维护性:改bug是否容易
3、可拓展性:是否容易添加新功能
4、灵活性:添加新功能是否容易,老方法和接口是否容易复用
5、简洁性:代码是否简单清晰
6、可复用性:相同的代码不要写两遍
7、可测试性:是否方便写单元测试和集成测试

一、写出优雅代码的原则 1.单一职责原则SRP

SRP 原则体现为:一个对象(方法)只做一件事情。
单一职责原则(SRP)的职责被定义为“引起变化的原因”。如果我们有两个动机去改写一
个方法,那么这个方法就具有两个职责。每个职责都是变化的一个轴线,如果一个方法承担了过
多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。
此时,这个方法通常是一个不稳定的方法,修改代码总是一件危险的事情,特别是当两个职
责耦合在一起的时候,一个职责发生变化可能会影响到其他职责的实现,造成意想不到的破坏,
这种耦合性得到的是低内聚和脆弱的设计

分离
SRP 原则是所有原则中最简单也是最难正确运用的原则之一。
要明确的是,并不是所有的职责都应该一一分离。
一方面,如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们。比如在 ajax
请求的时候,创建 xhr 对象和发送 xhr 请求几乎总是在一起的,那么创建 xhr 对象的职责和发送
xhr 请求的职责就没有必要分开。
另一方面,职责的变化轴线仅当它们确定会发生变化时才具有意义,即使两个职责已经被耦
合在一起,但它们还没有发生改变的征兆,那么也许没有必要主动分离它们,在代码需要重构的
时候再进行分离也不迟

2.最少知识原则

最少知识原则(LKP)说的是一个软件实体应当尽可能少地与其他实体发生相互作用。这
里的软件实体是一个广义的概念,不仅包括对象,还包括系统、类、模块、函数、变量等。本
节我们主要针对对象来说明这个原则,下面引用《面向对象设计原理与模式》一书中的例子来
解释最少知识原则:
某军队中的将军需要挖掘一些散兵坑。下面是完成任务的一种方式:将军可以通知
上校让他叫来少校,然后让少校找来上尉,并让上尉通知一个军士,最后军士唤来一个
士兵,然后命令士兵挖掘一些散兵坑。
这种方式十分荒谬,不是吗?不过,我们还是先来看一下这个过程的等价代码:
gerneral.getColonel( c ).getMajor( m ).getCaptain( c ) .getSergeant( s ).getPrivate( p ).digFoxhole();
让代码通过这么长的消息链才能完成一个任务,这就像让将军通过那么多繁琐的步骤才能命
令别人挖掘散兵坑一样荒谬!而且,这条链中任何一个对象的改动都会影响整条链的结果。
最有可能的是,将军自己根本就不会考虑挖散兵坑这样的细节信息。但是如果将军真的考虑
了这个问题的话,他一定会通知某个军官:“我不关心这个工作如何完成,但是你得命令人去挖
散兵坑。

3.开放-封闭原则

在面向对象的程序设计中,开放封闭原则(OCP)是最重要的一条原则。很多时候,一个
程序具有良好的设计,往往说明它是符合开放封闭原则的。
开放封闭原则最早由 Eiffel 语言的设计者 Bertrand Meyer 在其著作 Object-Oriented Software
Construction 中提出。它的定义如下:
软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改

有一种说法是,设计模式就是给做的好的设计取个名字。几乎所有的设计模式都是遵守开放-
封闭原则的,我们见到的好设计,通常都经得起开放封闭原则的考验。不管是具体的各种设计
模式,还是更抽象的面向对象设计原则,比如单一职责原则、最少知识原则、依赖倒置原则等,
都是为了让程序遵守开放封闭原则而出现的。可以这样说,开放封闭原则是编写一个好程序的
目标,其他设计原则都是达到这个目标的过程

  1. 发布订阅模式
    发布订阅模式用来降低多个对象之间的依赖关系,它可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。当有新的订阅者出现时,发布者的代码不需要进行任何修改;同样当发布者需要改变时,也不会影响到之前的订阅者。
  2. 模板方法模式
    模板方法模式是一种典型的通过封装变化来提高系统扩展性的设计模式。在一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以我们把这部分逻辑抽出来放到父类的模板方法里面;而子类的方法具体怎么实现则是可变的,于是把这部分变化的逻辑封装到子类中。通过增加新的子类,便能给系统增加新的功能,并不需要改动抽象父类以及其他的子类,这也是符合开放封闭原则的。
  3. 策略模式
    策略模式和模板方法模式是一对竞争者。在大多数情况下,它们可以相互替换使用。模板方法模式基于继承的思想,而策略模式则偏重于组合和委托。
    策略模式将各种算法都封装成单独的策略类,这些策略类可以被交换使用。策略和使用策略的客户代码可以分别独立进行修改而互不影响。我们增加一个新的策略类也非常方便,完全不用修改之前的代码。
4.依赖倒置原则

我们应该面向接口(抽象)编程,而不是面向实现编程

目前我们有一个鸭子类 Duck,还有一个让鸭子发出叫声的 AnimalSound 类,该类有一个 makeSound 方法,接收 Duck 类型的对象作为参数,这几个类一直合作得很愉快,代码如下:

public class Duck { // 鸭子类
 public void makeSound(){ 
 System.out.println( "嘎嘎嘎" ); 
 } 
}

public class AnimalSound { 
 public void makeSound( Duck duck ){ // (1) 只接受 Duck 类型的参数
 duck.makeSound(); 
 } 
} 
public class Test { 
 public static void main( String args[] ){ 
 AnimalSound animalSound = new AnimalSound(); 
 Duck duck = new Duck(); 
 animalSound.makeSound( duck ); // 输出:嘎嘎嘎
 } 
} 

目前已经可以顺利地让鸭子发出叫声。后来动物世界里又增加了一些鸡,现在我们想让鸡也
叫唤起来,但发现这是一件不可能完成的事情,因为在上面这段代码的(1)处,即 AnimalSound 类
的 sound 方法里,被规定只能接受 Duck 类型的对象作为参数

public class Chicken { // 鸡类
 public void makeSound(){ 
 System.out.println( "咯咯咯" ); 
 } 
} 
public class Test { 
 public static void main( String args[] ){ 
 AnimalSound animalSound = new AnimalSound(); 
 Chicken chicken = new Chicken(); 
 animalSound.makeSound( chicken ); 
 // 报错,animalSound.makeSound 只能接受 Duck 类型的参数
 } 
} 

在享受静态语言类型检查带来的安全性的同时,我们也失去了一些编写代码的自由。
我们已经明白,静态类型语言通常设计为可以“向上转型”。当给一个类变量赋值时,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类。就像看到天上有只麻雀,我们既可以说“一只麻雀在飞”,也可以说“一只鸟在飞”,甚至可以说成“一只动物在飞”。通过向上转型,对象的具体类型被隐藏在“超类型”身后。当对象类型之间的耦合关系被解除之后,这些对象才能在类型检查系统的监视下相互替换使用,这样才能看到对象的多态性。所以如果想让鸡也叫唤起来,必须先把 duck 对象和 chicken 对象都向上转型为它们的超类型Animal 类,进行向上转型的工具就是抽象类或者 interface。我们即将使用的是抽象类。
先创建一个 Animal 抽象类

public abstract class Animal { 
 abstract void makeSound(); // 抽象方法
} 

然后让 Duck 类和 Chicken 类都继承自抽象类 Animal

public class Chicken extends Animal{ 
 public void makeSound(){ 
 System.out.println( "咯咯咯" ); 
 } 
} 
public class Duck extends Animal{ 
 public void makeSound(){ 
 System.out.println( "嘎嘎嘎" ); 
 } 
} 

也可以把 Animal 定义为一个具体类而不是抽象类,但一般不这么做。Scott Meyers 曾指出,
只要有可能,不要从具体类继承。
现在剩下的就是让 AnimalSound 类的 makeSound 方法接收 Animal 类型的参数,而不是具体的
Duck 类型或者 Chicken 类型

public class AnimalSound{ 
 public void makeSound( Animal animal ){ // 接收 Animal 类型的参数,而非 Duck 类型或 Chicken 类型
 animal.makeSound(); 
 } 
} 
public class Test { 
 public static void main( String args[] ){ 
 AnimalSound animalSound = new AnimalSound (); 
 Animal duck = new Duck(); // 向上转型
 Animal chicken = new Chicken(); // 向上转型
 animalSound.makeSound( duck ); // 输出:嘎嘎嘎
 animalSound.makeSound( chicken ); // 输出:咯咯咯
 } 
}
5.里氏替换原则

里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
子类中可以增加自己特有的方法
当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等


总结

下面这段话引自 Bob 大叔的《敏捷软件开发原则、模式与实践》。有句古老的谚语说:“愚弄我一次,应该羞愧的是你。再次愚弄我,应该羞愧的是我。”这也是一种有效的对待软件设计的态度。为了防止软件背着不必要的复杂性,我们会允许自己被愚弄一次让程序一开始就尽量遵守开放封闭原则,并不是一件很容易的事情。一方面,我们需要尽快知道程序在哪些地方会发生变化,这要求我们有一些“未卜先知”的能力。另一方面,留给程序员的需求排期并不是无限的,所以我们可以说服自己去接受不合理的代码带来的第一次愚弄。在最初编写代码的时候,先假设变化永远不会发生,这有利于我们迅速完成需求。当变化发生并且对我们接下来的工作造成影响的时候,可以再回过头来封装这些变化的地方。然后确保我们不会掉进同一个坑里,这有点像星矢说的:“圣斗士不会被同样的招数击倒第二次。”

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/724841.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-26
下一篇 2022-04-26

发表评论

登录后才能评论

评论列表(0条)

保存