前言:
如今大家对“好的代码是什么”大约比较注意,你们都需要学习一些“好的代码是什么”的相关内容。那么小编在网上收集了一些关于“好的代码是什么””的相关知识,希望小伙伴们能喜欢,我们一起来了解一下吧!代码是给人阅读的
大部分时间都用于理解和维护先人遗留的代码,而实际编写新代码的时间相对较少。
换句话,请做好阅读一大堆代码的准备。
编程是一种让他人了解你想让电脑做什么的艺术。
Donald Kunth代码布局
代码布局的规律是程序组织的重要表现,违反这些规律会导致读者生产效率和理解能力的急剧下降。
因此,团队应该强制实施一套代码布局的约定,包括空白、新行、缩进和大括号的使用。许多编辑器都提供了设置格式的工具,应该充分利用这些工具来保持代码的整洁和可读性。
语法工具存在这么多个世纪并不是偶然,它们满足了读者的需求和潜意识的要求。
William Zinsser
格式编码的基础理论表明,良好的视觉布局可以展示程序的逻辑结构。让代码看起来漂亮肯定是值得的,但更宝贵的是可以展示代码结构。
Steve Macconnell命名
每一个代码库都定义了自己的命名语言,包括类名、方法名、变量名、函数名、包名、文件名和目录名。如果代码布局是语法,那么代码中的名称就是单词,它们是你用来思考代码的语言。好的名称应该能够回答所有的重要问题,因此它们应该是精确、全面,能够揭示意图,且遵循约定。
回答所有重要的问题
不要让其他人阅读代码时需要绞尽脑汁去理解这些名称的含义。开发人员阅读代码的时间通常比编写代码的时间还要多,如果还需要弄清楚这些神秘名称的含义,那么时间差距就更明显了。因此,应该让名称尽可能地清晰明了,能够回答是什么、为什么和怎么样的问题。
变量、函数、类名称应该回答所有重要的问题。它告诉你存在的原因、它是干什么的、以及如何使用。
Robert C.Martin要精确
我们要挑选能够精确描述该段代码“是做什么” 和 “为什么做”的名称。
有几个建议原则:
要精确的使用对应的词,如:convert(File inputCsv, File outputJson);避免使用不明确的变量名称,如:tmp,data;数字符号用有含义的单词代替,如:subtotal1 -> subtotalWithShipping;要全面
名称应该能全面反映是什么、为什么和怎么样这三个问题。
上例调整如:convert -> convertCvsToJson(File inputCsv, File outputJson);
PS:如果力求全面导致名称过长而显得可笑,那么就应该考虑是否所命名的对象承担了太多的职责,是否应该进行拆分。
揭示意图
好的命名应该能够揭示意图,因为计算机只关注代码的功能,而人类更关注代码背后的原因和目的。
例:咱很常写出下这样的代码:
String = csvFileds[0] + csvFileds[1] + csvFileds[2];
魔法值0、1、2到底是啥东西,是否能够替换成有命名的常量。
遵循约定
一致性比规则更重要。在项目中,遵循特定的命名规则是必要的,但更重要的是在整个项目中保持一致性。避免使用不一致的接口名称(如PaymentInterface和IPaymentInterface),以及滥用共享词汇。为了提高代码的可读性和维护性,应该建立一套编码约定,包括命名规则,并在整个代码库中强制执行。
如果我在看一个由10名工程师写的文件,应该让我无法区分哪部分是哪个人写的才对。对我来说,这就是整洁的代码。如果做到这点,每个人都会变得更有生产效率。到了那个阶段,你们主要关注的就是在写什么,而不是怎么写的问题了。
Nick Dellamaggiore,LinkedIn,Coursera命名真难
好的名称与所处的环境密切相关。通常,在想出好的名称之前,你必须要先成为你产品领域、技术栈、团队文化习俗方面的专家。
然,很多时候需要做出妥协和退让,有时候一开始不得不用foo这样无意义的名称,先实现功能。一旦对问题领域有更深入的理解,就应该回头修改一个更好的名称,而不是置之不理。
计算机科学只有两件难事:缓存失效和命名。
Phil Karlton错误处理
清晰的错误消息是整洁代码的主要特征,我们可以抛出异常、把错误消息作为返回值的一部分,或是将错误记录在日志中,只要不是静静地发生失败就行。要避免如:catch (Exception e) {} ,异常捕获缺啥也没干。
如果碰到没有业务类型的错误,尽可能让数据保持原样,同时可以选择往上抛,让业务调用者决定更合理的处理方式。
不要重复自己
避免重复是实现整洁代码最根本的原则之一。
有意思的,这个思想在不同的语境下有不同的表述,如DRY(Don't Repeat Yourself)、单点真理、一次并且只有一次。避免重复的思想可以应用于技术实现的各个方面,包括架构、代码、测试、过程、需求和文档等。重复出现的原因可能有以下几点:
缺少时间:直接复制和黏贴代码;没有意识到方面问题,如:在大型代码库中手撸StringUtil类,不会去想是否已经存在;语言限制,用多种方式表达相同的信息,缺少统一规范化。
系统中的每一项知识都必须具有单一、无歧义、权威的表示。
Andrew Hunt、David Thomas
重复是编程中的一大问题,不仅浪费时间,还增加了代码的复杂性,使得理解和维护变得困难。避免重复是实现高效、可维护代码的关键。我们应该尽可能利用现有的开源能力,避免重复发明轮子,以减少不必要的重复工作。
当代码不够DRY时,每次修改都需要确保所有副本都被更新,否则容易导致bug出现;
如果发现修改一小部分代码会涉及多个代码块,那么应该考虑让代码更加DRY;
如果每次实现都需要重复相同的过程,那么应该实现自动化过程;
如果存在多个地方的相同逻辑,应该实现抽象,以便共享单一的实现。
单一职责原理
SRP(Single Responsibility Principle)要求每个类、函数和变量都应该有一个清晰、明确且单一的目的。从另一个角度看,这意味着每个类、函数和变量都应该有且仅有一个改变的原因。
如果一个单独的函数具备过多的职责,每次修改其中的一项就可能导致其他功能的破坏。就得考虑改进了,把每一项职责都放在独立的函数中。确保修改单一项时不用担心影响到负责其它职责的代码。
函数式编程
遵循单一职责原理会使设计中出现许多短小的、简单的、独立的函数。把这些函数中的几个组合一起,创建具有更复杂行为的函数。 这就是函数式编程的基本原理。
而其中的关键就使用一种安全且容易组合的方式去设计函数。
不可变数据
阅读以下一段简单代码:
public class Groceries { public List<String> shoppingList = new ArrayList<>(); public void fillShoppingList(){ shoppingList.add("milk"); shoppingList.add("eggs"); shoppingList.add("bread"); if (!isOnDiet()) { addCandy(shoppingList); } if (!isXmas()) { addXmasFoods(shoppingList); } }}
当调用fillShoppingList函数时,初始时shoppingList是空的,然后被填充为["milk","eggs","bread"]。然而,由于addCandy和addXmasFoods函数获得了shoppingList的引用,并且shoppingList是Groceries类中的公共字段,可以被任何方法修改,因此无法仅凭这段代码确定shoppingList的最终值。如果Groceries类被用于多线程环境,那么由于线程执行的不确定顺序,更无法确定shoppingList中存放的内容。
由于shoppingList是一个可变变量,指向内存位置的指针,该位置在不同的时间可能存入不同的值,因此推导出它的值很困难。考虑时间因素和并发性会使问题更加复杂,跟踪代码几乎不可能。
解决这个问题的方法是使用不可变变量。不可变变量只是一个固定值的标识符,永远不会发生变化。使用不可变变量可以简化代码的跟踪和理解,避免由于并发性和时间因素带来的复杂性。
调整后的简单的代码:
public List<String> buildShoppingList(){ List<String> basics = ImmutableList.of("milk","eggs","bread"); List<String> candy = !isOnDiet() ? getCandy() : emptyList(); List<String> xmas = !isXmas() ? getXmasFoods() : emptyList(); return new ImmutableList.Builder<String>() .addAll(basics) .addAll(candy) .addAll(xmas) .build();}
思路是将每次“计算”的结果存储在不可变的本地中间列表对象中。最后,将这些列表连接到一个新的列表并返回。由于每个中间列表对象都有名称,这有助于更好地理解代码逻辑。由于所有对象都是不可变的,buildShoppingList函数的代码逻辑现在是完全本地的,没有其他函数、类或线程会影响结果。使用不可变数据可以避免考虑时间轴的问题。
高阶函数
buildShoppingList()方法中的所有变量改成不可变的,是因为提前知道了要执行“计算”的次数,所以才可以把每一次的计算结果都赋给一个命名的中间不可变变量。那如果执行次数不可预估呢?又当如何。
public List<Book> parseBooksFromCsvFile(File inputCsv) throws IOException{ List<CSVRecord> records = CSVFormat .DEFAULT .withHeader(TITLE.name(), AUTHOR.name(),PAGES.name(),CATEGORY.name()) .parse(new FileReader(inputCsv)) .getRecords(); List<Book> books = new ArrayList(); for(CSVRecord record:records){ books.add(parseBookFromCsvRecord(record)); } return books;}
必须进行的“计算”(即调用parseBookFromCsvRecord)的次数是无法提前预知的,那么如何在没有可变变量的情况下构造出Book对象的List呢?一种解决的办法是使用高阶函数,一种能够把其他函数作为参数的函数。
java增加了的高阶函数的支持,如:map、filter和reduce,在java8中成为了Stream API中的一部分。
public List<Book> parseBooksFromCsvFile(File inputCsv) throws IOException{ List<CSVRecord> records = CSVFormat .DEFAULT .withHeader(TITLE.name(), AUTHOR.name(),PAGES.name(),CATEGORY.name()) .parse(new FileReader(inputCsv)) .getRecords(); return records .stream() .map(this::parseBookFromCsvRecord) .collect(Collectors.toList());}纯函数
要充分发挥函数式编程的优势,需要使用不可变数据和纯函数。纯函数满足以下条件:
幂等性:给定相同的输入参数,函数总是返回精确的相同结果。无副作用:函数不依赖或修改外部世界的状态。(副作用的例子如:改变全局变量、写到硬盘、读取用户控制台的输入、通过网络接收数据)
纯函数只对输入参数进行转换并返回新的值,这使得纯函数的推导简单且容易组合。只要一个纯函数的返回值是另一个纯函数的有效参数,组合起来就是安全的。如:result = method3(method2(method1(arg)));
我认为可重用性的缺乏存在于面向对象语言,而不是函数式语言,因为面向对象语言的问题是它们离不开各种隐性的环境。你要一根香蕉,但得到的却是一只拿着香蕉的大猩猩和整个丛林。
如果你的代码是引用透明的,如果你有纯函数——所有的数据都来自输入的参数,输出的所有东西也没有留下什么状态——那么它的可重用性将是惊人的。
Joe Armstrong, Erlang
有副作的函数在组合时就更困难一些。
public void convertCsvToJson(File inputCsv,File outputCsv) throws IOException{
该函数没有返回值,很难将其与其他函数组合起来,因为它们将不得不通过文件系统或共享的可变变量进行通信,这两种方式都比使用参数和返回值更加复杂且容易出错。
事实上如果没有,那么一个函数唯一的功能就执行有副作用的操作。
即使函数有返回值,仍然可能有副作用,这使得对函数的推导和组合更加困难。例如,要推导convertCsvToJson的行为,除了查看其内部代码或签名,还需要知道外部世界的状态,如CSV文件的存在性、读取权限、覆盖文件的可能性、JSON文件的存在性以及存储空间等。
不可变数据要求在脑海中弄清楚多条时间轴,而具有副作用的函数则要求在脑海中弄清楚多条时间轴和多个可能的全局状态。将几个具有副作用的函数组合起来可能会导致所有时间轴和状态相互交互,导致复杂度呈指数级增长。
在大多数语言中,副作用在很大程度上是隐藏的,任何函数,不管他的签名是什么,都可以进行网络调用,改变全局变量。除了转到纯函数式语言之外,没有什么简单的解决办法。
最好的办法是将有副作用的操作推到入口点,尽可能的保证方法签名和文档可以精确的反映所有副作用。
例如:
在BookParser中,每个函数都是纯函数,没有副作用,这使得它们易于阅读、维护和重用。副作用现在都被隔离到main方法中,这是应用的入口点,是天生进行I/O操作的地方。在纯函数模式下,可以轻松编写它们的单元测试。
public class BookParser{ public String convertCsvToJson(String csv) throws IOException{ List<Book> books = parseBooksFromCsvString(csv); return writeBookAsJsonString(books); } public List<Book> parseBooksFromCsvString(String csv) throws IOException{ List<CSVRecord> records = CSVFormat .DEFAULT .withHeader(TITLE.name(), AUTHOR.name(),PAGES.name(),CATEGORY.name()) .parse(new StringReader(csv)) .getRecords(); return records .stream() .map(this::parseBookFromCsvRecord) .collect(Collectors.toList()); } public Book parseBookFromCsvRecord(CSVRecord record) { String title = record.get(TITLE); String author = record.get(AUTHOR); int pages = Integer.parseInt(record.get(PAGES)); Category category = Category.valueOf(record.get(CATEGORY)); return new Book(title,author,pages,category); } public String writeBooksAsJsonString(List<Book> books) throws JsonProcessingException{ ObjectMapper mapper = new ObjectMapper(); return mapper.writeValueAsString(books); }}public class main { public static void main(String[] args) throws IOException{ String inputCsv = args[0]; String outputCsv = args[1]; String csv = IOUtils.toString(new FileInputStream(inputCsv)); String json = new BookParser().convertCsvToJson(csv); IOUtils.write(json, new FileOutputStream(outputCsv)); }}松耦合
在软件领域,模块之间的依赖程度称为耦合。如果一个模块的更新导致另一个模块频繁更新,则这些模块是紧耦合的,这种代码既脆弱又难以维护。
整洁的代码应该遵循依赖反转原则:
高级模块不应该依赖低级模块,而应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
看以下一段代码:
public class NewsFeed{ List<Article> getLastesArticesShareByUser(User user) { long user = user.data().getProfile().getDatabaseKeys().id; List<Article> articles = GlobalCache.get(userId); if (articles == null) { Date oneMothAgo = new DateTime().minusDay(30).toDate(); String query = "selct * from articles where userId = ? and date> ?"; articles=parseArticles(DB.query(query,userId,oneMothAgo)); GlobalCache.put(userId, articles); } return articles; }}
NewsFeedl类中的getLastesArticesShareByUser方法尝试获取特定用户在过去30天内分享的文章。它先从缓存中查找,如果缓存没命中的话就去数据库中查找。
而这段代码违反了依赖反转原则,从而导致紧耦合,四种常见方式:
内部实现依赖性:User类;系统依赖性:时间;库依赖性:DB类;全局变量:GlobalCache类。内部实现依赖性
一长串的方法调用和字段查询通常就是紧耦合的标志。
long user = user.data().getProfile().getDatabaseKeys().id;
上面代码中,NewsFeed深入到了User类中,所以一旦User类发生了变动,就必须对User进行更新,这就是违反了依赖反转原则的。因为User类的底层实现,而不是对获取用户id方法进行抽象。比如在User类中公开getId()方法:
public long getId() { return user.data().getProfile().getDatabaseKeys().id;}
对getId()更高级抽象的依赖降低了耦合,而在底层可以按照自己的方式在User类中实现getId()方法,而不需要改变NewsFeed类或其他客户端。
系统依赖性
Date oneMothAgo = new DateTime().minusDay(30).toDate();
它调用new DateTime()查询当前的日期,每次运行代码都会有不同的表现,使得函数变的不幂等。因此对系统时钟的依赖使得代码的测试和推导变得更加困难。
可以通过添加参数(Date since)方式使得函数对系统时钟不再有依赖性,所以它的表现也会是幂等的。
库依赖性
String query = "selct * from articles where userId = ? and date> ?";
articles=parseArticles(DB.query(query,userId,oneMothAgo));
NewsFeed代码不应该去管文章存储在何处,它所关注的是以何种特定的方式满足特定条件的文章——至于底层发生了什么那是别人的问题。换句话说,这一代码违背了依赖反转原则。NewsFeed依赖了底层的数据库访问细节,而不是依赖检索文章的高级抽象。在java中定义抽象,我们可以使用接口:
public interface ArticleStore{ List<Article> getArticlesForUserSince(long userId, Date since);}public class NewsFeed{ private final ArticleStore articleStore; public NewsFeed(ArticleStore articleStore) { this.articleStore = articleStore; }}
上述代码将获取文章的实现细节与NewsFeed本身的实现进行了解耦。后续要变更获取文章方式,就不用修改NewsFeed的代码。
那么现在有个问题来了,我们应该把哪些暴露为依赖项?例如:我们可以Jackson库将java对象转为JSON,并使用Apache Commons CSV库解析CSV文件。我们应该把哪些库去作为抽象依赖后注入?根据经验,我们更应该把有如下特征的的库的具体实现进行注入抽象:
具有副作用;在不同的环境中有不同的表现。全局变量
NewsFeed类问题最大的依赖项就是使用了GlobalCache类;
List<Article> articles = GlobalCache.get(userId);
//...
GlobalCache.put(userId, articles);
正如名称所言,GlobalCache是一个全局变量,即可以被代码库中所有代码访问的可变状态。如果调用函数前GlobalCache被提前初始化,会发生什么?如果不止一个用户去初始化,又会怎样?又或是如果是多线程中呢?如果有一些不相关的代码使用GlobalCache去存储,或者没有使用userId为key,有会发生什么呢?
全局变量是危险的,如果你正在处理遗留代码,和全局变量做斗争,你可以遵循依赖反转来降低这种危害。
NewsFeed不需要知道实现缓存的底层细节,它需要的只是缓存的高级抽象。那么可以定义传递缓存的接口,再注入使用。如:
public interface PassthroughCache(K, V) { V getOrElseUpdate(K key, Supplier<V> valueIfMissing);}public class NewsFeed{ private final ArticleStore articleStore; private final PassthroughCache<Long, List<Article>> cache; public NewsFeed(ArticleStore articleStore, PassthroughCache<Long, List<Article>> cache) { this.articleStore = articleStore; this.cache = cache; }}
现在已经将所有的依赖性都倒置到NewsFeed代码中,降低了耦合,使得代码更加容易维护和更安全的测试。
高内聚
整洁的代码应该有高内聚:所有的变量和方法都应该是有关联的,一切都应该在同一个抽象层次上操作,每一部分都应该要有很好的互相配合。
内聚(cohesion)一词和“附着力”(adhesion)一词有相同的词根,这是一个表示黏性的词,当什么东西附着在其他东西上(换句话说,它是有粘性的),它是一种单面的、外向的东西:这种东西(比如胶水)能把一种东西粘到另一种东西上。换句话说,有黏着力的东西本身就会彼此的粘在一起,因为它们就时这样的东西,或者因为它们可以很好的结合起来。厚布胶带可以粘住东西是因为它们是有黏性的,不是因为它们必须让任何东西和它们一样。但是当你把两块黏土放到一起时,精心加工匹配,有时看起来就像凝聚在一起,是因为它们是精确地匹配在一起的。
Glenn VanderBurg, Livingsocial
看以下一段代码:
public calss util{ void generateReport(){//...} void connecToDb(String user, String password){//...} void fireTheMissles(){//...}}
这是一个技术上正确,但完全没意义的一个类。因为这些方法毫不相干,产生了一个低内聚的类。
在看以下一段代码:
public interface HttpClient{ byte[] sendRequest(String url, Map<String,String> headers,byte[] body) Document getXml(String url); int postOnSeparateThread(String url, String body, ExecutorService executor) Void setHeader(String headerName, String headerValue); Boolean statusCode();}
与util类的其他方法相比,伪HttpClient接口中的方法的关系会更紧密一些,因为这些方法全部都和HTTP请求的发送有关。然,这些方法也是低内聚的,因为它们是在许多不同的抽象层次上操作的:
sendRequest 把字节数组用在请求和响应的body上,postOnSeparateThread把String用于请求的body而没有返回响应的body(它只是返回一个状态码),getXml没有请求body,它返回一个XMLDocument给响应的body。postOnSeparateThread 负责底层的线程处理细节(通过ExecutorService),但其他方法都不会这么做。setHeader和statusCode方法暗示了HttpClient是可变的,可以存储下一个请求或前一个响应的状态。这些方法如何与其他方法交互并不清楚,特别是sendRequest,该方法把一个HTTP headers的Map对象作为参数之一。
那么从高内聚的角度出发,上述的代码又该如何调整呢?
public interface HttpClient{ HttpResponse sendRequest(HttpRequest request);}public interface HttpRuest{ URL getUrl(); Map<String, String> getHeaders(); byte[] getBody();}public interface HttpResponse{ Map<String, String> getHeaders(); byte[] getBody();}
新的HttpClient API只有一个发送请求的单独的方法,其他所有的逻辑都由其他类去处理。例如:HTTP头部、URL和body的细节由HttpRuest和HttpResponse处理。
如果我们不只手动设置HTTP头部和处理请求body的字节数组,而是要处理更高级的请求,可以创建一个HttpRuestBuilder类:
public class HttpRuestBuilder{ public HttpRuest postJson(String url, String json) throw Exception{ return new BasicHttpRequest( url, ImmutableMap.of("Method","POST","Content-Type","application/json")), json.getBytes("UTF-8")); }}
同理,如果用更高级的方式去处理响应的body而不是使用字节数组,我们可以创建一个HttpResponseParser类:
public class HttpResponseParser{ public Document asXml(HttpResponse response){ return DocumentBuilder.parse( new ByteArrayInpuStream(response.getBody())); }}
以及任何有关线程处理的任务都可以由HttpClient实现去处理:
public class ThreadedHttpClient implements HttpClient{ private final ExecutorService executor; public ExecutorService(ExecutorService executor){ this.executor = executor; } public HttpResponse sendRequest(HttpRequest request){ try{ return executor.submit(()->dosend(request)).get(); }catch(Exception e){ throw new ThreadedHttpClient(e); } }}
这里并没有使用个较大的HttpClient 类去处理许多不相干的任务,而是使用几个较小的类,没有类都处理几个高度相关的任务。我们从一个庞然大物转到若干专注、高度内聚的类,这就是实习整洁代码的标准模式。
注释
把介绍注释放在比较靠后的位置,是因为代码本身应该告诉你需要知道的几乎一切。如果代码没有做到,在你费劲地编写任务注释之前,应该对代码进行改进。
不要为糟糕的代码注释——重新写吧。
Brian W.Kernighan、P.J.Plauger
注释是文档的一部分,我们可以添加一条注释,对代码中无法体现的内容进行解释,比如当初为什么会有这段代码、我们对输入或输出所做的所有假设以及一些例子。
重构
对代码进行增量式的改进,每次只对内部实现细节做一点小改变,这就是所谓的重构。重构是改变代码结构而没有改变其外部行为的过程,这是一种只影响软件“非功能”方面的编码任务:从外部看,代码实现的功能并没有变化;但从内部看,我们已经改进了它的设计。
编程语言是对程序的思考,而不是表达你已经思考过的程序。
Paul Graham,Y Combinator
重构并不是一个单独的任务——不是只在项目的“清理阶段”(这样的阶段永远不会到来)才做的事情。重构是编写软件非常核心的部分,它应该是持续不断进行着的。
我是“先做纯粹重构,再做纯粹扩展”的忠实信徒。我们要对代码进行整理,直到可以轻易地增加下一个功能,但它在行为上不会有任务改变。之后就可以再增加下一个功能,接着再重复整个过程。
Deam Thompson,Transarc公司
修改是优秀作品的精髓。
William Zinser总结
编写整洁代码和难看代码所花费的时间差不多,但阅读和更新这两种代码所花费的时间不是一个量级。因此,我们应该始终编写整洁的代码,并不断进行代码重构以适应新的需求。如果不保持代码的整洁和进化,最终会导致技术债务的累积,影响生产效率,增加人力成本,并给程序员带来痛苦。
编程是一门手艺,需要选择正确的工具、努力工作并制作出精美的东西,而丑陋的拼凑会让程序员感到悲伤。
更糟糕的是,技术债务会像滚雪球一样不断积累,一旦容忍难看的代码进入系统而不及时解决,会导致更多难看的代码涌现,最终导致整个系统崩溃。这就是典型的破窗寓言。
在城市中,有些建筑是漂亮而干净的、而有些建筑则破败不堪,为什么呢?犯罪和城市衰退领域的研究人员发现了一种引入关注的触发机制,这是一种非常快就能把一个干净、完好、有人居住的建筑编程破败、被人抛弃的建筑的机制。
一个破碎的窗户。
一个破碎的窗户,如果长期以来没有修改,就会给建筑中的居民逐渐带来一种被抛弃的感觉——感觉管理者并没有在意这栋建筑。这样会再有一个窗户被损坏。人们也开始乱丢东西、涂鸦、建筑被严重损坏。很快,这栋建筑的损坏程度使得其主人都不想再维修它了,放弃的意愿就成了现实。
Andrew Hunt 和 David Thomas
破窗理论在代码和建筑上同样适用。如果代码库中充斥着丑陋的修改和混乱的代码,新来的开发人员只会加剧问题,而不是解决它。随着难看代码的增多,理顺代码会变得越来越困难,问题会加速发展。因此,我们应该及时修复坏掉的代码,坚持编写整洁的代码,并不断进行重构以保持代码的整洁。
来源:《奔跑吧,程序员:从零开始打造产品、技术和团队》
标签: #好的代码是什么