他们在学校里不会教你的编程原则

这篇具有很好参考价值的文章主要介绍了他们在学校里不会教你的编程原则。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

在大学的时候,学校一般只会教你你写编程语言,比如C、C++、JAVA等编程语言。但是当你离开大学进入这个行业开始工作时,才知道编程不只是知道编程语言、语法等,要想写好代码,必须还要了解一些编程原则才行。本文主要讨论KISSDRYSOLID这些常见的编程原则,而且你会发现随着工作时间越久,越能感受这些编程原则的精妙之处,历久弥香。

KISS原则

Keep It Simple, Stupid!

你是不是有过接手同事的代码感到十分头疼的经历,明明可以有更加简单、明白的写法,非要绕来绕去,看不明白?

其实,我们在写代码的时候应该要遵守KISS原则,核心思想就是尽量保持简单。代码的可读性和可维护性是衡量代码质量非常重要的两个标准。而 KISS 原则就是保持代码可读和可维护的重要手段。代码足够简单,也就意味着很容易读懂,bug 比较难隐藏。即便出现 bug,修复起来也比较简单。

我们写代码的的时候要站在别人的角度出发,就像马丁·福勒说的,我们写的代码不是给机器看的,而是给人看的。

“任何傻瓜都可以编写计算机可以理解的代码。优秀的程序员编写出人类可以理解的代码。” — 马丁·福勒

那么如何才能写出满足KISS原则的代码呢?

如何写出KISS原则的代码?

我们直接上例子,下面的校验IP是否合法的3种实现方式,大家觉得哪个最KISS?

  1. 写法一

他们在学校里不会教你的编程原则

  1. 写法二

他们在学校里不会教你的编程原则

  1. 写法三

他们在学校里不会教你的编程原则

  • 写法一代码量最少,正则表达式本身是比较复杂的,写出完全没有 bug 的正则表达本身就比较有挑战;另一方面,并不是每个程序员都精通正则表达式。对于不怎么懂正则表达式的同事来说,看懂并且维护这段正则表达式是比较困难的。这种实现方式会导致代码的可读性和可维护性变差,所以,从 KISS 原则的设计初衷上来讲,这种实现方式并不符合 KISS 原则。
  • 写法二使用了 StringUtils 类、Integer 类提供的一些现成的工具函数,来处理 IP地址字符串,逻辑清晰,可读性好。
  • 写法三不使用任何工具函数,而是通过逐一处理 IP 地址中的字符,来判断是否合法,容易出bug,不好理解。

所以说,符合KISS原则的代码并不是代码越少越好,还要考虑代码是否逻辑清晰、是否容易理解、是否够稳定。

总结以下如何写出KISS原则的代码:

  1. 不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。
  2. 不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出bug 的概率会更高,维护的成本也比较高。
  3. 不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。
  4. 主观站在别人的角度上编写代码。你在编写代码的时候就要思考我这个同事看这段代码是不是很快就能够明白理解。

DRY原则

Don't Repeat Yourself

你是不是有过这样的经历,项目中很多重复逻辑的代码,然后修改一个地方,另外一个地方忘记修改,导致测试给你提了很多bug?

DRY原则,英文全称Don’t Repeat Yourself,直译过来就是不要重复你自己。这里的重复不仅仅是代码一模一样,还包括实现逻辑重复、功能语义重复、代码执行重复等。我们不要偷懒,有责任把这些存在重复的地方识别出来,然后优化它们。

如何写出DRY原则的代码呢?

我们直接上例子,代码重复的我就不讲了,很好理解,关于实现逻辑或者功能语义重复的我觉个例子。

还是上面校验IP的例子,团队中两个同事由于不知道就有了两种写法。

  • 同事A写法

他们在学校里不会教你的编程原则

  • 同事B写法

他们在学校里不会教你的编程原则

尽管两段代码的实现逻辑不重复,但语义重复,也就是功能重复,我们认为它违反了 DRY 原则。我们应该在项目中,统一一种实现思路,所有用到判断 IP 地址是否合法的地方,都统一调用同一个函数。不然哪天校验规则变了,很容易只改了其中一个,另外一个漏改,就会出现莫名其妙的bug

其他的比如逻辑重复的意思是虽然功能是不一致的,但是里面的逻辑都是一模一样的。举个例子,比如校验用户名和校验密码,虽然功能不一致,但是校验逻辑都是相似,判空、字符长度等等,这种情况我们就需要把相似的逻辑抽取到一个方法中,不然也是不符合DRY原则。

那么我们平时写代码注意些什么才是符合DRY原则呢?

  • 使用现成的轮子,不轻易造轮子

其实最关键的就是写代码带脑子,用到一个方法先看看有没有现成的,不要看看不看,就动手在那里造轮子。

  • 减少代码耦合

对于高度耦合的代码,当我们希望复用其中的一个功能,想把这个功能的代码抽取出来成为一个独立的模块、类或者函数的时候,往往会发现牵一发而动全身。移动一点代码,就要牵连到很多其他相关的代码。所以,高度耦合的代码会影响到代码的复用性,我们要尽量减少代码耦合。

  • 满足单一职责原则

我们前面讲过,如果职责不够单一,模块、类设计得大而全,那依赖它的代码或者它依赖的代码就会比较多,进而增加了代码的耦合。根据上一点,也就会影响到代码的复用性。相反,越细粒度的代码,代码的通用性会越好,越容易被复用。

  • 模块化

这里的“模块”,不单单指一组类构成的模块,还可以理解为单个类、函数。我们要善于将功能独立的代码,封装成模块。独立的模块就像一块一块的积木,更加容易复用,可以直接拿来搭建更加复杂的系统。

  • 业务与非业务逻辑分离

越是跟业务无关的代码越是容易复用,越是针对特定业务的代码越难复用。所以,为了复用跟业务无关的代码,我们将业务和非业务逻辑代码分离,抽取成一些通用的框架、类库、组件等。

  • 通用代码下沉

从分层的角度来看,越底层的代码越通用、会被越多的模块调用,越应该设计得足够可复用。一般情况下,在代码分层之后,为了避免交叉调用导致调用关系混乱,我们只允许上层代码调用下层代码及同层代码之间的调用,杜绝下层代码调用上层代码。所以,通用的代码我们尽量下沉到更下层。

  • 继承、多态、抽象、封装

在讲面向对象特性的时候,我们讲到,利用继承,可以将公共的代码抽取到父类,子类复用父类的属性和方法。利用多态,我们可以动态地替换一段代码的部分逻辑,让这段代码可复用。除此之外,抽象和封装,从更加广义的层面、而非狭义的面向对象特性的层面来理解的话,越抽象、越不依赖具体的实现,越容易复用。代码封装成模块,隐藏可变的细节、暴露不变的接口,就越容易复用。

  • 应用模板等设计模式

一些设计模式,也能提高代码的复用性。比如,模板模式利用了多态来实现,可以灵活地替换其中的部分代码,整个流程模板代码可复用。

SOLID原则

SOLID原则不是一个单一的原则,而是对软件开发至关重要的 5 条原则,遵循这些原则有助于我们写出高内聚、低耦合、可扩展、可维护性好的代码。

S—单一职责原则

一个类应该有一个,而且只有一个改变它的理由。

单一职责原则在我看来是最容易理解也是最重要的一个原则。它的核心思想就是一个模块、类或者方法只做一件事,只有一个职责,千万不要越俎代庖。它可以带来下面的好处:

  • 可以让代码耦合度更低
  • 使代码更容易理解和维护
  • 使代码更易于测试和维护,使软件更易于实施,并有助于避免未来更改的意外副作用

举个例子,我们有两个类PersonAccount。 两者都负有存储其特定信息的单一责任。 如果要更改Person的状态,则无需修改类Account,反之亦然, 不要把账户的行为比如修改账户名changeAcctName写在Person类中。

    public class Person {
    	private Long personId;
    	private String firstName;
    	private String lastName;
    	private String age;
    	private List<Account> accounts;

        // 错误做法
        public void changeAcctName(Account account, String acctName) {
            acccount.setAccountName(acctName);
            // 更新到数据库
        }
    }

    public class Account {
    	private Long guid;
    	private String accountNumber;
    	private String accountName;
    	private String status;
    	private String type;

    }

所以大家在编写代码的时候,一定要停顿思考下这个段代码真的写在这里吗?另外很关键的一点是如果发现一个类或者一个方法十分庞大,那么很有可能已经违背单一职责原则了,后续维护可想而知十分痛苦。

O—开闭原则

软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。

对扩展开放,对修改关闭,什么意思?很简单,其实就是我们要尽量通过新增类实现功能,而不是修改原有的类或者逻辑。因为修改已有代码很有可能对已有功能引入bug。

让我们通过一个例子来理解这个原则,比如一个通知服务。

    public class NotificationService {
    	public void sendOTP(String medium) {
            if (medium.equals("email")) {
                //email 发送
            } else if (medium.equals("mobile")) {
                // 手机发送
        	} 
    }

现在需要新增微信的方式通知,你要怎么做呢? 是在加一个if else吗? 这样就不符合开闭原则了,我们看下开闭原则该怎么写。

  • 定义一个通知服务接口

    public interface NotificationService {
    	public void sendOTP();
    }
  • E-mail方式通知类EmailNotification

    public class EmailNotification implements NotificationService{
    	public void sendOTP(){
    		// write Logic using JavaEmail api
    	}
    }
  • 手机方式通知类MobileNotification

    public class MobileNotification implements NotificationService{
        public void sendOTP(){
    		// write Logic using Twilio SMS API
    	}
    }
  • 同样可以添加微信通知服务的实现WechatNotification

    public class WechatNotification implements NotificationService{
    	public void sendOTP(String medium){
    		// write Logic using wechat API
    	}
    }

这样的方式就是遵循开闭原则的,你不用修改核心的业务逻辑,这样可能带来意向不到的后果,而是扩展实现方式,由调用方根据他们的实际情况调用。

是不是想到了设计模式中的策略模式,其实设计模式就是指导我们写出高内聚、低耦合的代码。

L—里氏替换原则

派生类或子类必须可替代其基类或父类

这个原则稍微有点难以理解,它的核心思想是每个子类或派生类都应该可以替代/等效于它们的基类或父类。这样有一个好处,就是无论子类是什么类型,客户端通过父类调用都不会产生意外的后果。

理解不了?那我我们通过一个例子来理解一下。

让我们考虑一下我有一个名为 SocialMedia 的抽象类,它支持所有社交媒体活动供用户娱乐,如下所示:

    package com.alvin.solid.lsp;

    public abstract class SocialMedia {
        
        public abstract  void chatWithFriend();
        
        public abstract void publishPost(Object post);
        
        public abstract  void sendPhotosAndVideos();
        
        public abstract  void groupVideoCall(String... users);
    }

社交媒体可以有多个实现或可以有多个子类,如 FacebookWechatWeiboTwitter 等。

现在让我们假设 Facebook 想要使用这个特性或功能。

    package com.alvin.solid.lsp;

    public class Wechat extends SocialMedia {

        public void chatWithFriend() {
            //logic  
        }

        public void publishPost(Object post) {
            //logic  
        }

        public void sendPhotosAndVideos() {
            //logic  
        }

        public void groupVideoCall(String... users) {
            //logic  
        }
    }

我们都知道Facebook都提供了所有上述的功能,所以这里我们可以认为FacebookSocialMedia类的完全替代品,两者都可以无中断地替代。

现在让我们讨论 Weibo

    package com.alvin.solid.lsp;

    public class Weibo extends SocialMedia {
        public void chatWithFriend() {
            //logic
        }

        public void publishPost(Object post) {
          //logic
        }

        public void sendPhotosAndVideos() {
          //logic
        }

        public void groupVideoCall(String... users) {
            //不适用
        }
    }

我们都知道Weibo微博这个产品是没有群视频功能的,所以对于 groupVideoCall方法来说 Weibo 子类不能替代父类 SocialMedia。所以我们认为它是不符合里式替换原则。

如果强行这么做的话,会导致客户端用父类SocialMedia调用,但是实现类注入的可能是个Weibo的实现,调用groupVideoCall行为,产生意想不到的后果。

那有什么解决方案吗?

那就把功能拆开呗。

    public interface SocialMedia {   
       public void chatWithFriend(); 
       public void sendPhotosAndVideos() 
    }

    public interface SocialPostAndMediaManager { 
        public void publishPost(Object post); 
    }


    public interface VideoCallManager{ 
       public void groupVideoCall(String... users); 
    }

现在,如果您观察到我们将特定功能隔离到单独的类以遵循LSP。

现在由实现类决定支持功能,根据他们所需的功能,他们可以使用各自的接口,例如 Weibo 不支持视频通话功能,因此 Weibo 实现可以设计成这样:

    public class Instagram implements SocialMedia,SocialPostAndMediaManager{
    	public void chatWithFriend(){
        //logic
        }
        public void sendPhotosAndVideos(){
        //logic
        }
        public void publishPost(Object post){
        //logic
        }
    }

这样子就是符合里式替换原则LSP。

I—接口隔离原则

接口不应该强迫他们的客户依赖它不使用的方法。

大家可以看看自己的工程,是不是一个接口类中有很多很多的接口,每次调用API方法的时候IDE工具给你弹出一大堆,十分的"臃肿肥胖"。所以该原则的核心思想要将你的接口拆小,拆细,打破”胖接口“,不用强迫客户端实现他们不需要的接口。是不是和单一职责原则有点像?

例如,假设有一个名为 UPIPayment 的接口,如下所示

    public interface UPIPayments {
        
        public void payMoney();
        
        public void getScratchCard();
        
        public void getCashBackAsCreditBalance();
    }

现在让我们谈谈 UPIPayments 的一些实现,比如 Google PayAliPay

Google Pay 支持这些功能所以他可以直接实现这个 UPIPaymentsAliPay 不支持 getCashBackAsCreditBalance() 功能所以这里我们不应该强制客户端 AliPay 通过实现 UPIPayments 来覆盖这个方法。

我们需要根据客户需要分离接口,所以为了满足接口隔离原则,我们可以如下设计:

  • 创建一个单独的接口来处理现金返还。

    public interface CashbackManager{ 
    	public void getCashBackAsCreditBalance(); 
    }

现在我们可以从 UPIPayments 接口中删除getCashBackAsCreditBalanceAliPay也不需要实现getCashBackAsCreditBalance()这个它没有的方法了。

D—依赖倒置原则

高层模块不应该依赖低层模块,两者都应该依赖于抽象(接口)。抽象不应该依赖于细节(具体实现),细节应该取决于抽象。

这个原则我觉得也不是很好理解,所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。比如大家都知道的MVC模式,controller是调用service层接口这个抽象,而不是实现类。这也是我们经常说的要面向接口编程,而非细节或者具体实现,因为接口意味着契约,更加稳定。

我们通过一个例子加深一下理解。

  • 借记卡

    public class DebitCard { 
    	public void doTransaction(int amount){ 
            System.out.println("tx done with DebitCard"); 
        } 
    }
  • 信用卡

    public class CreditCard{ 
    	public void doTransaction(int amount){ 
            System.out.println("tx done with CreditCard"); 
        } 
    }

现在用这两张卡你去购物中心购买了一些订单并决定使用信用卡支付

    public class ShoppingMall {
    	private DebitCard debitCard;
    	public ShoppingMall(DebitCard debitCard) {
            this.debitCard = debitCard;
       	}
    	public void doPayment(Object order, int amount){              
            debitCard.doTransaction(amount); 
     	}
    	public static void main(String[] args) {
         	DebitCard debitCard=new DebitCard();
         	ShoppingMall shoppingMall=new ShoppingMall(debitCard);
         	shoppingMall.doPayment("some order",5000);
        }
    }

上面的做法是一个错误的方式,因为 ShoppingMall 类与 DebitCard 紧密耦合。

现在你的借记卡余额不足,想使用信用卡,那么这是不可能的,因为 ShoppingMall 与借记卡紧密结合。

当然你也可以这样做,从构造函数中删除借记卡并注入信用卡。但这不是一个好的方式,它不符合依赖倒置原则。

那该如何正确设计呢?

  • 定义依赖的抽象接口BankCard

    public interface BankCard { 
      public void doTransaction(int amount); 
    }
  • 现在 DebitCardCreditCard 都实现BankCard

    public class CreditCard implements BankCard{
    	public void doTransaction(int amount){            
            System.out.println("tx done with CreditCard");
        }
    }

    public class DebitCard implements BankCard { 
    	public void doTransaction(int amount){ 
    		System.out.println("tx done with DebitCard"); 
        } 
    }
  • 现在重新设计购物中心这个高级类,他也是去依赖这个抽象,而不是直接低级模块的实现类

    public class ShoppingMall {
    	private BankCard bankCard;
    	public ShoppingMall(BankCard bankCard) {
            this.bankCard = bankCard;
        }
    	public void doPayment(Object order, int amount){
            bankCard.doTransaction(amount);
        }
    	public static void main(String[] args) {
            BankCard bankCard=new CreditCard();
            ShoppingMall shoppingMall1=new ShoppingMall(bankCard);
            shoppingMall1.doPayment("do some order", 10000);
        }
    }

我们还可以拿 Tomcat这个 Servlet 容器作为例子来解释一下。

Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Sevlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet规范。

总结

本文总结了软件编程中的黄金原则,KISS原则,DRY原则,SOLID原则。这些原则不仅仅适用于编程,也可以指导我们在架构设计上。虽然其中有些原则很抽象,但是大家多多实践和思考,会体会到这些原则的精妙。

欢迎关注个人公众号【JAVA旭阳】交流学习文章来源地址https://www.toymoban.com/news/detail-459637.html

到了这里,关于他们在学校里不会教你的编程原则的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 还不会用Yakit&Bp?来,我教你

    🍀作者简介:被吉师散养、喜欢前端、学过后端、练过CTF、玩过DOS、不喜欢java的不知名学生。 🍁个人主页:被吉师散养的职业混子 🍂相应专栏:CTF专栏 好兄弟,你听说过yakit吗?一看你就没见识 我也没听说过 感谢我寄师傅,跟我说过几回,后来发现这玩意比我之前用的

    2024年02月10日
    浏览(34)
  • 如何保证三个线程按顺序执行?不会我教你

    👨‍🎓作者:bug菌 ✏️博客:CSDN、掘金、infoQ、51CTO等 🎉简介:CSDN|阿里云|华为云|51CTO等社区博客专家,历届博客之星Top30,掘金年度人气作者Top40,51CTO年度博主Top12,掘金 | InfoQ | 51CTO等社区优质创作者,全网粉丝合计 15w+   ;硬核微信公众号「猿圈奇妙屋」,欢迎你的

    2024年02月07日
    浏览(44)
  • 如果还不会Elasticsearch这七个问题 那么你的Elasticsearch白学

    答:提高查询效率 答:elasticsearch-6.8.15binelasticsearch.bat 答:es图形化工具 更方便查询数据 答:elasticsearch-6.8.15headheadapache-tomcat-8.0.32binstartup.bat 答:1.没有配置跨域 2.没有开启elasticsearch 答:使用中文分词器 中文分词器就像一个词库 可以把一句话分成多个部分 只要词库里

    2023年04月08日
    浏览(41)
  • 用这几种方式清理Mac缓存,你的Mac就不会卡了

    与网络浏览器和应用程序非常相似,您的 Mac 有自己的系统缓存,它可以在后台临时存储数据,以加快软件安装速度并减少互联网数据使用量(通过Apple)。与其他浏览器和应用程序类似,这些缓存数据可能会开始堆积——占用存储空间,可能会降低性能导致卡顿,甚至在缓存

    2024年02月04日
    浏览(28)
  • 不会数学统计没关系——5分钟教你轻松掌握箱线图

    箱线图也称箱须图、箱形图、盒图,用于反映一组或多组连续型定量数据分布的中心位置和散布范围。箱形图包含数学统计量,不仅能够分析不同类别数据各层次水平差异,还能揭示数据间离散程度、异常值、分布差异等等。 箱线图(Box plot)也称箱须图(Box-whisker Plot)、箱

    2024年01月25日
    浏览(33)
  • SpringBoot系列(四十三):如何集成ElasticSearch,不会我教你|超级详细,建议收藏

            ElasticSearch是一款基于Lucene的开源搜索引擎,具有高效、可扩展、分布式的特点,可用于全文搜索、日志分析、数据挖掘等场景。Spring Boot作为目前最流行的微服务框架之一,也提供了对ElasticSearch的支持。本篇文章将介绍如何在Spring Boot项目中整合ElasticSearch,并展

    2023年04月09日
    浏览(51)
  • 在Linux上不会部署私有IPFS?(保姆级教程!!!)教你一步一步部署。

    系统要求 至少需要2G内存,2 核 CPU。 实验环境 共计两个节点,192.168.159.102、192.168.159.103,系统采用centos7 环境准备后,按照如下步骤部署ipfs,每个节点都要单独部署ipfs。 配置 golang 环境 下载go 配置 golang 环境变量 所有节点上 都需要部署ipfs: 1、安装 2、指定IPFS的存储位置

    2024年01月18日
    浏览(39)
  • 手把手教你用YOLOv5算法训练数据和检测目标(不会你捶我)

    本人从一个小白,一路走来,已能够熟练使用YOLOv5算法来帮助自己解决一些问题,早就想分析一下自己的学习心得,一直没有时间,最近工作暂时告一段落,今天抽空写点东西,一是为自己积累一些学习笔记,二是可以为一些刚接触YOLOv5算法的小白们提供一些参考,希望大家

    2024年02月01日
    浏览(63)
  • Spring Boot进阶(25):文件上传的单元测试怎么写?不会我教你 | 超级详细,建议收藏

            文件上传是现代Web应用程序中常见的功能,因此编写高效的文件上传单元测试是确保应用程序质量的关键步骤之一。但是,很多开发者可能会遇到单元测试速度慢或者不准确的问题,这使得测试变得更加繁琐和无聊。因此,本篇文章将为你提供一些实用技巧和最佳实

    2024年02月08日
    浏览(45)
  • 初入职场不会Git?经常被团队成员怼?手把手教你如何使用git

    简介: 用Git进行多人协作开发时,必然会合并代码,解决冲突。然而合并代码也是需要点技巧的,如果对一些关键命令没有理解去使用的话,git的版本演进路线就会变得很乱,从而造成了日后维护的一些麻烦。 Git上合并代码有git merge 以及 git rebase 两种方式。 用Git进行多人协

    2024年01月20日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包