设计模式 | 设计原则(六大设计原则+KISS+DRY+YAGNI)

设计原则

六大设计原则

设计原则的最终目的:高内聚、低耦合 [即迪米特原则]

六大原则:SOLID+迪米特

  • 迪米特:最少知道原则,即高内聚、低耦合
  • SOLID

Single: 单一职责
Open: 开闭原则
LSP:里氏替换原则
Interface:接口隔离
Dependency:依赖倒置

KISS原则

keep it simple and short

不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。
不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出 bug 的概率会更高,维护的成本也比较高。
不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替 if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。

DRY原则

Don’t Repeat Yourself。中文直译为:不要重复自己。将它应用在编程中,可以理解为:不要写重复的代码。

主要讲三种典型的代码重复情况,它们分别是:实现逻辑重复、功能语义重复和代码执行重复。

YAGNI原则

You Ain’t Gonna Need It。直译就是:你不会需要它。

这条原则也算是万金油了。当用在软件开发中的时候,它的意思是:不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计。

单一职责

  • 一个类只负责一件事。
  • 类的单一是一个相对的,最初设计一个单一的类,随着业务的扩张,它可能不再单一。
    需求:最初开发系统时,所有的字段都放在了User表中,但随着物流业务的日益复杂,User不再满足单一职责,这时不得不将Address类抽离出来,抽离出来后,满足了单一职责。同样的,后续平台变多,系统需要做单点登录,Token也变的日益复杂,这时也需要抽离出UserToken。
    示例
    public class User
    {
        public string name;
        public string pwd;

        public DateTime birth;
        public int age { get { return DateTime.Now.Year - birth.Year; } }

        public UserToken token;

        public Address addr;

        public bool IsNullOrEmpty(string s)
        {
            return s == null || s.Length == 0;
        }
    }

    public class Address
    {
        public string city;
        public string province;
        public string street;
    }

    public class UserToken
    {
        public int appid;
        public string addToken;
    }

开闭原则

对扩展开放,对修改关闭

  • 扩展:修改代码,不影响以前的业务代码
  • 修改:修改代码,以前的业务代码受影响
  • 参数的开闭:将函数的多个入参封装成类
  • 方法的开闭:引入 handler 的概念,将 if 判断逻辑分散在各个 handler 中。
    需求:

现有一段API接口监控告警代码,最初规模小,需求是检查接口是否出错,如果出错,打印错误信息。随着业务的增长,系统响应变慢,现需求变更为相应时长超过5秒发出告警。需求日益复杂,当接口的pts超过5人时,也要求发出告警。
改造前

   public class Test
    {
        [Fact]
        public void test() {
            Alert alert = new Alert();
            alert.check("","接口请求出错",50,100);
        }
    }

    public class Alert
    {
        //public void check(string api, string err)
        //{
        //    if (err != null) Debug.WriteLine(err);
        //}
        public void check(string api,string err,int count,int timeout) {
            if (err != null) Debug.WriteLine(err);
            if(count>5) Debug.WriteLine("请求并发过高");
            if(timeout>5) Debug.WriteLine("请求超时");
        }
    }

改造后

 public class Test
 {
     [Fact]
     public void test() {
         AlertInfo alertInfo = new AlertInfo() { err="请求错误",timeout=10,count=100};
         Alert alert = new Alert();
         alert.Register(new AlertErrHandle());
         alert.Register(new AlertCountHandle());
         alert.Register(new AlertTimeOutHandle());
         alert.check(alertInfo);
     }
 }

 public class Alert
 {
     private List<IAlertHandler> _handlers = new List<IAlertHandler>();
     public void Register(IAlertHandler h) { _handlers.Add(h); }

     public void check(AlertInfo alertInfo)
     {
         foreach (var item in _handlers)
         {
             item.check(alertInfo);
         }
     }
 }
 public class AlertInfo
 {
     public string api;
     public string err;
     public int timeout;
     public int count;
 }
 public interface IAlertHandler
 {
     void check(AlertInfo info);
 }

 public class AlertErrHandle : IAlertHandler
 {
     public void check(AlertInfo info)
     {
         if (info.err!=null) Debug.WriteLine(info.err);
     }
 }
 public class AlertCountHandle : IAlertHandler
 {
     public void check(AlertInfo info)
     {
         if (info.count > 5) Debug.WriteLine("请求并发过高");
     }
 }
 public class AlertTimeOutHandle : IAlertHandler
 {
     public void check(AlertInfo info)
     {
         if (info.timeout > 5) Debug.WriteLine("请求超时");
     }
 }

里氏替换

  • 父类出现的地方,都可以用子类替换
  • 父类中没有异常处理,子类中出现了,这种情况不符合里氏替换原则。

接口隔离

一组 API 接口集合

我们的后台管理系统要实现删除用户的功能,希望用户系统提供一个删除用户的接口。删除用户是一个非常慎重的操作,我们只希望通过后台管理系统来执行,所以这个接口只限于给后台管理系统使用。如果我们把它放到 UserService 中,那所有使用到 UserService 的系统,都可以调用这个接口。不加限制地被其他业务系统调用,就有可能导致误删用户。

    public interface ICommonOP
    {
        void Select();
        void Insert();
        void Update();
    }
    public interface IAdminOP
    {
        void Delete();
    }

设计微服务或者类库接口的时候,如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。

单个 API 接口或函数

public class Computer//: ICount, ISum, IAvg
    {
        List<string> list = new List<string>();

        public decimal sum;
        public int count;
        public decimal avg;
        public int quantity;

        public void doSum() { sum = 1; }
        public void doCount() { count = 2; }

        public void goAll()
        {
            doSum();
            doCount();
        }
    }

接口隔离原则跟单一职责原则有点类似,不过稍微还是有点区别。单一职责原则针对的是模块、类、接口的设计。而接口隔离原则相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它的思考的角度不同。它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

OOP 中的接口概念

 public interface IMssage
    {
        void Post();
        void Get();
    }
    public interface ISave
    {
        void Save();
    }
    public class Notification //: IMssage//, ISave
    {
        private HttpClient _clent;
        private IMssage _msg;

        public void Send() { _msg.Post(); }
    }

依赖倒置

控制反转(IOC,Inversion Of Control)

        [Fact]
        public void test()
        {
            if (doTest()) {
                Debug.WriteLine("Test succeed.");
            } 
            else {
                Debug.WriteLine("Test failed.");
            }
        }

        public static bool doTest()
        {
            return false;
        }

所有的流程都由程序员来控制。如果我们抽象出一个下面这样一个框架,我们再来看,如何利用框架来实现同样的功能。具体的代码实现如下所示:

    public abstract class TestCase {
        public void run() {
            if (doTest())
            {
                Debug.WriteLine("Test succeed.");
            }
            else
            {
                Debug.WriteLine("Test failed.");
            }
        }
        public abstract bool doTest();
    }

    public class UserServiceTest : TestCase
    {
        public override bool doTest()
        {
            //复杂的业务代码
            return true;
        }
    }

    public class JunitApplication
    {
        private static List<TestCase> testCases = new List<TestCase>();
        public static void Register(TestCase testCase) { testCases.Add(testCase); }
        [Fact]
        public void test()
        {
            // 注册操作还可以通过配置的方式来实现,不需要程序员显示调用register()
            JunitApplication.Register(new UserServiceTest());

            foreach (var item in testCases)
            {
                item.run();
            }
        }
    }

依赖注入(DI)

依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。

// 非依赖注入实现方式
public class Notification {
  private MessageSender messageSender;
  
  public Notification() {
    this.messageSender = new MessageSender(); //此处有点像hardcode
  }
  
  public void sendMessage(String cellphone, String message) {
    //...省略校验逻辑等...
    this.messageSender.send(cellphone, message);
  }
}

public class MessageSender {
  public void send(String cellphone, String message) {
    //....
  }
}
// 使用Notification
Notification notification = new Notification();

// 依赖注入的实现方式
public class Notification {
  private MessageSender messageSender;
  
  // 通过构造函数将messageSender传递进来
  public Notification(MessageSender messageSender) {
    this.messageSender = messageSender;
  }
  
  public void sendMessage(String cellphone, String message) {
    //...省略校验逻辑等...
    this.messageSender.send(cellphone, message);
  }
}
//使用Notification
MessageSender messageSender = new MessageSender();
Notification notification = new Notification(messageSender);

上面代码还有继续优化的空间,我们还可以把 MessageSender 定义成接口,基于接口而非实现编程。

依赖注入框架

在采用依赖注入实现的 Notification 类中,虽然我们不需要用类似 hard code 的方式,在类内部通过 new 来创建 MessageSender 对象,但是,这个创建对象、组装(或注入)对象的工作仅仅是被移动到了更上层代码而已,还是需要我们程序员自己来实现。具体代码如下所示:

public class Demo {
  public static final void main(String args[]) {
    MessageSender sender = new SmsSender(); //创建对象
    Notification notification = new Notification(sender);//依赖注入
    notification.sendMessage("13918942177", "短信验证码:2346");
  }
}

在实际的软件开发中,一些项目可能会涉及几十、上百、甚至几百个类,类对象的创建和依赖注入会变得非常复杂。如果这部分工作都是靠程序员自己写代码来完成,容易出错且开发成本也比较高。而对象创建和依赖注入的工作,本身跟具体的业务无关,我们完全可以抽象成框架来自动完成。

你可能已经猜到,这个框架就是“依赖注入框架”。我们只需要通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象、类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。实际上,现成的依赖注入框架有很多,比如 Google Guice、Java Spring、Pico Container、Butterfly Container 等。不过,如果你熟悉 Java Spring 框架,你可能会说,Spring 框架自己声称是控制反转容器(Inversion Of Control Container)。

实际上,这两种说法都没错。只是控制反转容器这种表述是一种非常宽泛的描述,DI 依赖注入框架的表述更具体、更有针对性。因为我们前面讲到实现控制反转的方式有很多,除了依赖注入,还有模板模式等,而 Spring 框架的控制反转主要是通过依赖注入来实现的。不过这点区分并不是很明显,也不是很重要,你稍微了解一下就可以了。

依赖反转(Dependency Inversion Principle)

高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。

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

迪米特

“高内聚”用来指导类本身的设计,
“松耦合”用来指导类与类之间依赖关系的设计。

迪米特法则的英文翻译是:Law of Demeter,缩写是 LOD。单从这个名字上来看,我们完全猜不出这个原则讲的是什么。不过,它还有另外一个更加达意的名字,叫作最小知识原则,英文翻译为:The Least Knowledge Principle。

不该有直接依赖关系的类之间,不要有依赖;

有依赖关系的类之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。

其他

参数多变–>对象化
过程复杂–>Handler
流程控制–>框架

配置的本质是工厂模式
第一遍写代码不需要考虑框架,后面重复功能才需要考虑
框架一般是针对于纯技术,不适用于业务场景

业务如何实现高内聚低耦合?

同级的业务:通知
上下级的业务:硬接口

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

靓仔很忙i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值