• 没有十几年的积累,你还真写不出什么好的代码


            如标题所说,我不知道大家怎么看待这句话?

            拿一个正常的程序员举个例子,18开始上大学学习写代码,22岁大学毕业,一直干到30岁就需要考虑程序员的中年危机。

            小编身边很多程序员都不喜欢写代码,感觉写代码没有意思, 每天围绕着业务写来写去,除了CURD就是CURD,写完DAO层、写Service,写完Service,在创建一个Controller去调一下Service。

            小编我不否定,很多中小型公司都是业务来推动技术,技术就是实现服务公司业务,从而给公司带来收益。不是每家公司都像阿里,华为那些大厂一样,有专门研究技术、写中间价、写开源的团队。

            久而久之,所以很多公司就开始裁员一些年纪偏大的程序员,用成本小、年纪轻的人来代替。

            今天不跑题,我们来聊聊平时在工作中,小编是如何写代码。

            小编从2017步入社会开始写代码,对代码质量的要求,是从2019年开始的,19年的时候又重新开始学习软件设计7大原则、设计模式。 

            那个时候是有想法去提升,但是学习完之后,根本就不能去理解其中的含义,就感觉学了,但又感觉什么没学到。

           再后来就是慢慢开始学习框架源码,看一个开源框架,里面就有很多灵活运用设计模式的例子,这样就会每次加深对某个设计模式的理解和使用方式。

            小编最近在写一个业务网关功能,网关有接入、接出的能力。

            接入一家新的第三方,需要按照第三方要求来对数据进行加解密,之前公司实现有一点不能让我理解是,我需要一步步找到网关最底层,竟然是一个HttpClientUtils的类,然后在里面doPost方法中,在写一个if判断,如果code=新公司的code,就调用新公司加解密的逻辑。

            当然不排除最开始的研发人员,可能要求段时间内完成,所以就这么简单的进行了实现,然后之后的伙伴也是图方便就直接在后面写if判断,就导致的后果,大家都懂得。

            我不知道小伙伴们在设计代码的时候,有没有模块化的概念,这个也是小编之前总监对我说的,在写代码的时候,不要把全部的逻辑都写在一个方法里,要学会拆分,把一件事拆成几件事。一个方法里面上百、上千行的代码,相信大家应该都见过吧,不管是写的人,还是后面接手的人,看这代码,谁心里都不好受哇。

            很多公司对于Http的请求,可能也只是利用一个HttpClient工具类,里面有post、get静态方法,传递url、data、header等参数,完成http请求就完事了。对于没有负责的业务逻辑来说,确实就行了,一旦有一些特殊化处理的,就不太好扩展了。

            小编先是定义了两个接口类,设计功能,底层一定要是抽象的,而绝对不能是具体的代码实现,抽象往往会使系统更加稳定。

    1. /**
    2. * Http 请求
    3. *
    4. * @author IT贱男
    5. * @version v1.0.0
    6. * @date 2022/10/08 09:48
    7. */
    8. public interface HttpClientRequest extends Closeable {
    9. /**
    10. * 执行http请求
    11. *
    12. * @param uri 请求地址
    13. * @param httpMethod 请求类型
    14. * @param requestHttpEntity 包装参数
    15. * @return
    16. * @throws Exception
    17. */
    18. HttpClientResponse execute(URI uri, String httpMethod, RequestHttpEntity requestHttpEntity) throws Exception;
    19. }
    1. /**
    2. * HTTP 返回处理
    3. *
    4. * @author IT贱男
    5. * @version v1.0.0
    6. * @date 2022/9/30 14:00
    7. */
    8. public interface HttpClientResponse extends Closeable {
    9. /**
    10. * 获取请求头
    11. *
    12. * @return
    13. */
    14. Header getHeaders();
    15. /**
    16. * 获取输入流
    17. *
    18. * @return
    19. * @throws IOException
    20. */
    21. InputStream getBody() throws IOException;
    22. /**
    23. * 设置String 类型的body
    24. *
    25. * @param body
    26. * @throws IOException
    27. */
    28. void setBodyString(String body);
    29. /**
    30. * 获取String body
    31. *
    32. * @param
    33. * @throws IOException
    34. */
    35. String getBodyString();
    36. /**
    37. * 获取http请求状态码
    38. *
    39. * @return
    40. * @throws IOException
    41. */
    42. int getStatusCode() throws IOException;
    43. /**
    44. * 获取http请求状态码 字符串类型
    45. *
    46. * @return
    47. * @throws IOException
    48. */
    49. String getStatusText() throws IOException;
    50. /**
    51. * 关闭输入流
    52. */
    53. @Override
    54. void close();
    55. }

            HttpClientRequest中定义了一个execute方法,然后交给子类去实现,比如说小编公司固定使用的就是RSA加解密,小编就创建RSAHttpClientRequest的类,去实现对参数进行RSA加解密的操作。DefaultHttpClientRequest就是默认的http请求,CustomHttpClientRequest就是为了满足新的接入方,就自己去创建一个新的HttpClient,去实现自己的业务逻辑。

            RSAClient、CustomClient都是在DefaultClient基础之上做一些自己额外的功能,所以继承了DefaultClient,为了代码复用。        

            Response就不多说了,只是为了响应请求做的一层包装。

            这样我们就把请求、响应的模块定义好了,那怎么使用?

            在上层只需要传入请求地址、请求头、请求参数,然后调用get、post方法就行,是不需要关心具体代码细节实现,所以肯定还会有一层包装类,包装类里面就是我们很熟悉的使用方式了。

    1. @Slf4j
    2. public class HttpClientRestTemplate extends AbstractHttpRestTemplate {
    3. private final HttpClientRequest requestClient;
    4. public HttpClientRestTemplate(HttpClientRequest requestClient) {
    5. this.requestClient = requestClient;
    6. }
    7. public HttpRestResult get(String url, Object query) throws Exception {
    8. return execute(url, HttpMethodConstants.GET, new RequestHttpEntity(this.buildHead(), conversionObj(query)), String.class);
    9. }
    10. private HttpRestResult execute(String url, String httpMethod, RequestHttpEntity requestEntity,
    11. Type responseType) throws Exception {
    12. URI uri = HttpUtils.buildUri(url, requestEntity.getQuery());
    13. // 获取response handler 处理类
    14. ResponseHandler responseHandler = super.selectResponseHandler(responseType);
    15. HttpClientResponse response = null;
    16. try {
    17. response = this.requestClient().execute(uri, httpMethod, requestEntity);
    18. } catch (Exception e) {
    19. log.error("HttpClientRestTemplate Http 请求失败", e);
    20. throw new BusinessException("HttpClientRestTemplate Http 请求失败", ErrorCodeEnum.SERVICE_EXCEPTION_500);
    21. }
    22. try {
    23. return responseHandler.handle(response);
    24. } catch (Exception e) {
    25. log.error("HttpClientRestTemplate handler 处理失败", e);
    26. throw new BusinessException("HttpClientRestTemplate handler 处理失败", ErrorCodeEnum.SERVICE_EXCEPTION_500);
    27. } finally {
    28. if (response != null) {
    29. response.close();
    30. }
    31. }
    32. }
    33. }

            包装类有了,怎么灵活去使用,应该是A公司就需要使用A公司的HttpClient、B公司就需要使用B公司的HttpClient,那这个时候工厂模式就派上使用了,交给工厂去生产相对应的HttpClient。

    1. /**
    2. * HTTP 工厂类
    3. *
    4. * @author IT贱男
    5. * @version v1.0.0
    6. * @date 2022/10/11 10:01
    7. */
    8. public interface HttpClientFactory {
    9. /**
    10. * 创建HttpClient
    11. *
    12. * @return
    13. */
    14. HttpClientRestTemplate createHttpClientRestTemplate();
    15. }

            小编拿了一个基于Config配置类的工厂为例子,Config配置是可以在Web后台有对应的页面进行配置,根据配置信息创建对应的HttpClient。

    1. /**
    2. * 基于配置类创建HttpClient工厂
    3. *
    4. * @author IT贱男
    5. * @version v1.0.0
    6. * @date 2022/10/11 13:19
    7. */
    8. @Slf4j
    9. public class ConfigHttpClientFactory implements HttpClientFactory {
    10. // API后台配置信息
    11. private OpenapiKeyOutConfigDO openapiKeyOutConfigDO;
    12. public ConfigHttpClientFactory(OpenapiKeyOutConfigDO openapiKeyOutConfigDO) {
    13. this.openapiKeyOutConfigDO = openapiKeyOutConfigDO;
    14. }
    15. @Override
    16. public HttpClientRestTemplate createHttpClientRestTemplate() {
    17. // 获取请求客户端
    18. CloseableHttpClient closeableHttpClient = getCloseableHttpClient();
    19. // 判断是否加密,不加密走DefaultClient
    20. if (OpenFlagEnum.CLOSE.getCode().equals(openapiKeyOutConfigDO.getIsEncryption())) {
    21. // 使用默认的 DefaultHttpClientRequest
    22. return new HttpClientRestTemplate(new DefaultHttpClientRequest(closeableHttpClient));
    23. }
    24. // 判断加密方式是不是RSA
    25. if (OpenFlagEnum.OPEN.getCode().equals(openapiKeyOutConfigDO.getIsEncryption())
    26. && EncryptionTypeEnum.RSA.getCode().equals(openapiKeyOutConfigDO.getEncryptionType())) {
    27. // 使用默认的 RSAHttpClientRequest
    28. return new HttpClientRestTemplate(new RSAHttpClientRequest(closeableHttpClient, openapiKeyOutConfigDO.getTheirPublicKey(), openapiKeyOutConfigDO.getSelfPrivateKey()));
    29. }
    30. // 判断是不是自定义类加密
    31. if (OpenFlagEnum.OPEN.getCode().equals(openapiKeyOutConfigDO.getIsEncryption())
    32. && EncryptionTypeEnum.CUSTOM_CLASS.getCode().equals(openapiKeyOutConfigDO.getEncryptionType())) {
    33. try {
    34. if (StringUtils.isEmpty(openapiKeyOutConfigDO.getRequestClientImplClassName())) {
    35. throw new BusinessException("创建自定义请求类错误,requestClientImplClassName参数为空", ErrorCodeEnum.SERVICE_EXCEPTION_500);
    36. }
    37. // 通过反射创建对应的HttpClient,代码省略
    38. } catch (Exception e) {
    39. log.error(e.getMessage(), e);
    40. }
    41. }
    42. throw new BusinessException("createHttpClientRestTemplate 失败", ErrorCodeEnum.SERVICE_EXCEPTION_500);
    43. }
    44. }

            在其他地方想要使用网关的话,只需要把相对应的配置告诉我,工厂就可以创建出你要想的HttpClient对象出来。

            这样以后接一家新的第三方,程序员只需要创建一个HttpClient,专注自己的业务逻辑,其他细节可以不用去考虑,这样也不需要去改之前原来的代码,符合开闭原则。

            为了扩展,小编设计了,有些第三方请求完成之后,需要做一些额外的特殊处理,最常见的就是字段映射,或者对象类型转换。

    1. /**
    2. * response 处理结果转换
    3. *
    4. * @param
    5. */
    6. public interface ResponseHandler {
    7. void setResponseType(Type responseType);
    8. /**
    9. * 处理response 转换成对应自己所需要的内容
    10. *
    11. * @param response
    12. * @return
    13. * @throws Exception
    14. */
    15. HttpRestResult handle(HttpClientResponse response) throws Exception;
    16. }

            首先会有一个公共处理的对象,比如说请求失败了,可以在这个类中做统一处理,如果没报错就调用convertResult方法,当然这个方法也是交给子类去实现。

            这个扩展是放在了HttpClientRestTemplate类中execute方法中去了

    1. /**
    2. * 公用处理对象
    3. *
    4. * @author IT贱男
    5. * @version v1.0.0
    6. * @date 2022/10/11 10:08
    7. */
    8. public abstract class AbstractResponseHandler implements ResponseHandler {
    9. private Type responseType;
    10. @Override
    11. public final void setResponseType(Type responseType) {
    12. this.responseType = responseType;
    13. }
    14. @Override
    15. public final HttpRestResult handle(HttpClientResponse response) throws Exception {
    16. // 请求失败就进行错误处理
    17. if (HttpStatus.SC_OK != response.getStatusCode()) {
    18. return handleError(response);
    19. }
    20. // 结果转换
    21. return convertResult(response, this.responseType);
    22. }
    23. private HttpRestResult handleError(HttpClientResponse response) throws Exception {
    24. Header headers = response.getHeaders();
    25. String message = IoUtils.toString(response.getBody(), headers.getCharset());
    26. return new HttpRestResult(headers, response.getStatusCode(), null, message);
    27. }
    28. /**
    29. * 把请求结果进行转换
    30. *
    31. * @param response http client response
    32. * @param responseType responseType
    33. * @return HttpRestResult
    34. * @throws Exception ex
    35. */
    36. public abstract HttpRestResult convertResult(HttpClientResponse response, Type responseType) throws Exception;
    37. }
    1. /**
    2. * 把结果处理成String类型
    3. *
    4. * @author IT贱男
    5. * @version v1.0.0
    6. * @date 2022/10/11 10:06
    7. */
    8. public class StringResponseHandler extends AbstractResponseHandler {
    9. @Override
    10. public HttpRestResult convertResult(HttpClientResponse response, Type responseType) throws Exception {
    11. final Header headers = response.getHeaders();
    12. if (StringUtils.isNotEmpty(response.getBodyString())) {
    13. String extractBody = ParameterRequestWrapper.jsonStringTrim(response.getBodyString());
    14. return new HttpRestResult<>(headers, response.getStatusCode(), extractBody, null);
    15. }
    16. String extractBody = ParameterRequestWrapper.jsonStringTrim(IoUtils.toString(response.getBody(), headers.getCharset()));
    17. return new HttpRestResult<>(headers, response.getStatusCode(), extractBody, null);
    18. }
    19. }

            其实这一套设计下来,代码结构上,小编觉得是很清晰的,在扩展上也是互不影响的,代码维护上也更加直观了,直接定位到类。

            小编平时也是在写这些业务代码,但是在写的时候,小编尽量做到可扩展、代码清爽、维护性高的方向去设计。当然不是吹捧小编写代码有多好,只是想提醒大家,写代码就应该去考虑这些问题,而不是一股脑的吧功能实现好了就行了。

            只有这样慢慢去锻炼自己,写出来的代码质量才会越来越好,写代码也才会越来越有意思。

            文章回到标题,“没有十几年的积累,你还真写不出什么好的代码”

            这句话是小编逛BOSS的时候看到的,所以想来说说,其实吧,在中小型公司,技术往上走,多多少少都会带点管理。

            

            标题中的话,小编也是觉得有道理,喜欢写代码的程序员,自然就会去考虑自己的代码该怎么写才是最好,这样累计起来的经验,代码质量才会越来越好。

            行吧,有感而发,到这里结束了,对于标题中的话,小伙伴们怎么看?

     

  • 相关阅读:
    基于SpringBoot的医院挂号系统
    18、Java——NullPointerException异常的原因和解决办法
    Transformer12
    如何防止内部员工数据外泄?
    QML-编辑框的使用
    华为云平台零代码搭建物联网可视化大屏体验:疫情防控数据大屏
    关于uni-app与vue路由配置的不同,不使用uni.navigateTo接口跳转时,使用this.$router.push的踩坑经验
    C#中的IQueryable vs IEnumerable (二)
    [经典力扣面试题]135. 分发糖果
    Jmeter配置脚本录制进行抓包并快速分析、定位接口问题
  • 原文地址:https://blog.csdn.net/weixin_38111957/article/details/127591889