软件开发是一门复杂的领域。是什么让高质量的软件与容易出错、充满错误的软件有所不同?答案通常在开发人员在编写代码时采用的核心原则中。
编程原则是卓越软件的基石。这些建议和最佳实践指导开发人员编写既功能强大又优雅、易维护和可扩展的代码。
在本文中,我们深入探讨了每个开发者工具包中都应该有的7个基本编程原则:
DRY:不要重复自己 — 减少冗余的关键原则。如果你发现自己复制粘贴同一段代码超过两次,现在是考虑抽象的时候了。
考虑这种情况:你有三个函数,每个函数都以相同的方式格式化日期。与其在所有三个函数中都有重复的格式化代码,不如创建一个单一的辅助函数:
// 格式化日期的辅助函数functionformatDate(date){return`${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`;}// 函数 1: 显示今天的日期functiondisplayTodaysDate(){consttoday=newDate();returnformatDate(today);}// 函数 2: 显示一周后的日期functiondisplayDateOneWeekFromNow(){constoneWeekFromNow=newDate();oneWeekFromNow.setDate(oneWeekFromNow.getDate()+7);returnformatDate(oneWeekFromNow);}// 函数 3: 显示一个月前的日期functiondisplayDateOneMonthAgo(){constoneMonthAgo=newDate();oneMonthAgo.setMonth(oneMonthAgo.getMonth()-1);returnformatDate(oneMonthAgo);
KISS:保持简单,愚蠢 — 在你的代码中追求简单。例如,如果你写了一个复杂的 if-else 链,也许使用 switch 语句或字典会简化和美化结构:
之前:
functiongetErrorMessage(errorCode){if(errorCode='E001'){return'Invalid input.';}elseif(errorCode='E002'){return'Connection timed out.';}elseif(errorCode='E003'){return'Database error.';}elseif(errorCode='E004'){return'File not found.';}else{return'Unknown error.';}}
重构后:
constERROR_MESSAGES={'E001':'Invalid input.','E002':'Connection timed out.','E003':'Database error.','E004':'File not found.'};functiongetErrorMessage(errorCode){returnERROR_MESSAGES[errorCode]||'Unknown error.';}
SOLID 不是一个单一的原则,而是五个设计原则的集合。尽管它们根植于面向对象编程(OOP),但它们的智慧可以更广泛地应用。
1. 单一职责原则(SRP): 一个类应该只有一个改变的理由。这意味着每个类应该只有一个任务或功能,确保更容易维护和在更改过程中减少副作用。
考虑这个例子:
// 错误的方法classUserManager{saveUser(user){// 保存用户到数据库的逻辑}generateReport(user){// 生成用户报告的逻辑}}
更优雅的解决方案是将其拆分为两个各自处理单一职责的类:
// 正确的方法classUserDatabase{save(user){// 将用户数据保存到数据库}}classUserReport{generate(user){// 为用户生成报告}}
在上面的代码片段中,我们分担了责任:UserReport 处理用户的报告生成,而 UserDatabase 管理将用户数据保存到数据库。
2. 开闭原则(OCP): 软件组件应该对扩展开放,对修改关闭。这允许开发人员在不修改现有代码的情况下添加新功能,促进可重用性并减少错误。
假设你有一个 AreaCalculator 类,用于计算矩形的面积。现在,如果我们添加一个 Circle,AreaCalculator 将需要修改。
// 错误的方法classAreaCalculator{calculateArea(shape){if(shape.type="circle"){return3.14*shape.radius*shape.radius;}elseif(shape.type="square"){returnshape.side*shape.side;}}}
相反,使用 OCP:我们从一个基础的 Shape 类扩展我们的形状,允许轻松添加新形状而不修改 AreaCalculator。
// 正确的方法classShape{calculateArea(){}}classCircleextendsShape{constructor(radius){super();this.radius=radius;}calculateArea(){return3.14*this.radius*this.radius;}}classSquareextendsShape{constructor(side){super();this.side=side;}calculateArea(){returnthis.side*this.side;}}
3. 里氏替换原则(LSP): 子类应该能够替换其基类而不产生异常。这确保继承类保持其父类的属性和行为。
遵循 LSP,我们应该重构设计以确保正确的继承:
classBird{fly(){// 通用飞行行为}}classPenguinextendsBird{// 企鹅不能飞,所以这个方法不应该在这里}
正确的方法是我们将形状从基本的 Shape 类扩展出来,允许轻松添加新的形状而不修改 AreaCalculator。
4. 接口隔离原则(ISP): 类不应该被迫实现它们不使用的接口。相反,接口应该对其目的具体而清晰。
这意味着接口不应该有太多方法,尽量我们将小接口抽取出来,以便类可以只实现它们需要的接口,就像下面的例子:
// 错误的方法interfaceWorker{work();eat();sleep();swim();}// 正确的方法interfaceWorker{work();}interfaceEater{eat();}interfaceSwimmer{swim();}
5. 依赖反转原则(DIP): 高层模块不应与低层模块纠缠在一起;它们都应依赖于抽象。例如在开关和设备的设计中可以找到:
// 错误的方法classLightBulb{turnOn(){}turnOff(){}}classSwitch{constructor(bulb){this.bulb=bulb;}operate(){// 直接控制灯泡}}
我们可以重构这样,以便 Switch 可以对任何实现 SwitchableDevice 的设备进行操作,而不仅仅是 LightBulb。
// 正确的方法classSwitchableDevice{turnOn(){}turnOff(){}}classBulbextendsSwitchableDevice{// 实现 turnOn 和 turnOff 方法}classSwitchDIP{constructor(device){this.device=device;}operate(){// 控制设备}}
YAGNI,“你不会需要它”,警告不要在必要之前添加功能。
例如,如果你正在构建一个博客网站,并考虑添加一个基于用户写作的功能来预测用户的心情,但这对于网站正常运行并不是必需的,那么最好将其留在一边,至少现在是这样。
有不必要功能的应用:
classBlog{constructor(posts){this.posts=posts;}addPost(post){this.posts.push(post);}displayPosts(){// 显示所有帖子}predictUserMoodBasedOnWritings(post){// 预测情绪的复杂算法// ...return"Happy";// 只是一个示例情绪}notifyUserAboutMood(mood){// 通知逻辑console.log(`Based on your writing, you seem to be${mood}`);}}
删除不必要功能后:
classBlog{constructor(posts){this.posts=posts;}addPost(post){this.posts.push(post);}displayPosts(){// 显示所有帖子}}
SoC,或“关注点分离”,建议不同的功能区域应由不同且最小重叠的模块管理。
例如,在一个天气应用程序中,一个模块可以处理数据获取,另一个可以管理数据存储,另一个则可以控制用户界面。每个都有自己的关注点,与其他模块分开。
// 1. 数据获取模块functionfetchWeatherData(city){constapiKey='YOUR_API_KEY';constresponse=fetch(`https://api.weather.com/v1/${city}?apiKey=${apiKey}`);returnresponse.json();}// 2. 数据存储模块functionstoreWeatherData(data){localStorage.setItem('weatherData',JSON.stringify(data));}// 3. 用户界面模块functiondisplayWeatherData(data){constweatherBox=document.querySelector('#weather-box');weatherBox.innerHTML=`<h1>${data.city}</h1><p>${data.temperature}°C</p>`;}// 在主应用程序函数中组合它们functionweatherApp(city){constdata=fetchWeatherData(city);storeWeatherData(data);displayWeatherData(data);}
LoD(迪米特法则)是开发软件的一个指导原则,特别是面向对象的程序。在其一般形式中,LoD是松散耦合的一个具体案例。
想象一下餐厅的场景:顾客将订单(方法)交给服务员,然后服务员将订单交给厨师。顾客不直接与厨师互动。
classCustomer{constructor(waiter){this.waiter=waiter;}giveOrder(order){console.log("Customer: I'd like to order "+order);this.waiter.takeOrder(order);}}classWaiter{constructor(chef){this.chef=chef;}takeOrder(order){console.log('Waiter: Order received - '+order);this.chef.prepareOrder(order);}}classChef{prepareOrder(order){console.log('Chef: Preparing '+order);// Logic to prepare the food...}}
组合优于继承原则(COI)建议使用组合(将简单对象组合以创建更复杂的对象)而不是类继承。
想象一下你有一个类 Bird 和一个类 Airplane。它们都能飞,但有继承关系并不合理。相反,你可以有一个 CanFly 类,并将你的 Bird 和 Airplane 类与它组合。
// 错误的方法:继承classCanFly{fly(){console.log(this.constructor.name+' is flying!');}}classBirdInheritextendsCanFly{}classAirplaneInheritextendsCanFly{}
通过以下组合方法,你可以轻松地向 BirdCompose 或 AirplaneCompose 添加或删除功能,而无需进行结构更改或添加不必要的冗余,强调灵活性。
// 正确的方法:组合classCanFlyComposition{fly(){console.log('Flying in the sky!');}}classBirdCompose{constructor(){this.flyingCapability=newCanFlyComposition();}fly(){this.flyingCapability.fly();}}classAirplaneCompose{constructor(){this.flyingCapability=newCanFlyComposition();}fly(){this.flyingCapability.fly();}}
掌握这7个编程原则将显著提升你在软件开发和问题解决方面的方法。