技术相对论之软件架构

作者: 欧阳锋 | 来源:发表于2018-03-19 19:44 被阅读65次
    文 | 欧阳锋

    有同学问我,你是怎样学习编程的呢?为了回答你的这个问题,今天,我们一起来做一件非常有意思的事情。我们以MVC架构为基,从服务端编程开始,依次类推iOS、Android,并最终完成登录、注册功能。

    What is MVC ?

    正文开始之前,我们先来简单了解一下MVC架构。

    MVC全称是Model-View-Controller,是上个世纪80年底Xerox PARC为其编程语言SmallTalk发明的一直软件设计模式。我们可以用一张图来表示MVC架构模型:

    MVC的核心思想是希望通过控制层管理视图呈现,从将逻辑层和视图层进行分离。

    服务端编程其实就是MVC的最佳实践,理解了MVC架构之后,我们马上开始服务端编程。

    服务端编程

    服务端编程也叫后端编程,主要用于为前端提供数据源以及完成必要的业务逻辑处理。

    这个部分我们使用Java语言开发,MVC框架使用最常用的 Spring MVC,完整配置请参考下方表格:

    IDE 编程语言 框架 数据库 服务器
    IntelliJ IDEA Java 1.8 Spring MVC MySQL Tomcat 7.0.57

    为了简化数据库的访问,我们再增加一个轻量级的数据库访问框架 MyBatis

    这里假设你已经正确安装了MySQL数据库和Tomcat服务器,如果你对具体的安装步骤有疑问,请在文章下方评论告诉我。

    在开始编程之前,我们需要完成以下准备工作:

    第一步:创建数据库d_user以及用户表t_user用于保存用户数据

    create database d_server;
    use d_server;
    CREATE TABLE `t_user` (
      `id` int(10) NOT NULL AUTO_INCREMENT,
      `username` varchar(20) NOT NULL,
      `pwd` varchar(32) NOT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `username` (`username`)
    )
    

    第二步:使用IntelliJ IDE创建一个Gradle依赖工程

    最后一个步骤选择工作目录确定即可。

    第三步:在build.gradle脚本文件中添加Spring MVC以及MyBatis依赖

    compile group: 'org.springframework', name: 'spring-webmvc', version: '5.0.4.RELEASE'
    compile group: 'org.mybatis', name: 'mybatis', version: '3.4.6'
    

    第四步:关联本地Tomcat服务器

    a)编辑运行设置,选择本地Tomcat服务器


    b)选择以war包的方式部署到Tomcat


    c)在浏览器中输入http://localhost:8080测试工作是否正常

    如果看到下面这个界面,证明一切工作正常


    第五步:配置Spring MVC

    备注:参考官方文档 Web on Servlet Stack

    a)在webapp目录下面生成WEB-INF/web.xml配置文件
    选择菜单File->Project Structure进入如下界面:



    在弹出的界面中设置路径为.../webapp/WEB-INF即可。

    b)在web.xml文件中添加如下配置信息

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
             version="3.1">
    
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
        </listener>
    
        <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/app-context.xml</param-value>
        </context-param>
    
        <servlet>
            <servlet-name>/</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value></param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
        </servlet>
    
        <servlet-mapping>
            <servlet-name>/</servlet-name>
            <url-pattern>/*</url-pattern>
        </servlet-mapping>
    
    </web-app>
    

    上面这部分配置主要是使用Spring MVC的DispatcherServlet完成请求的拦截分发。配置文件中引用了另外一个配置文件app-context.xml,这个配置文件主要是完成Spring的依赖注入。

    c)在app-context.xml配置文件中添加如下信息

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:p="http://www.springframework.org/schema/p"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/mvc
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">
      
       <!-- 添加扫描注解的包 -->
        <context:component-scan base-package="com.youngfeng.server"/>
        
       <!-- 使用注解完成依赖注入 -->
        <mvc:annotation-driven />
    
    </beans>
    

    d)添加jackson依赖用于Spring实现Json自动解析

    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.4'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.2.3'
    

    PS:不得不承认,Java后端开发的xml文件配置实在是一件繁琐至极的事情,尽管我们只需要配置一次。为了简化配置,Spring官方推出了一个重磅产品 Spring Boot。不过,这不是本文讨论的重点,感兴趣的同学请自行了解。

    虽然我们已经完成了Spring的配置,但MyBatis的配置工作才刚刚开始。

    配置MyBatis

    为了简化Spring中MyBatis的配置,我们再增加一个MyBatis官方的提供的 mybatis-spring 库。

    compile group: 'org.mybatis', name: 'mybatis-spring', version: '1.3.2'
    

    备注:参考官方文档 mybatis-spring

    a)在spring配置文件app-context.xml配置文件中添加如下bean配置:

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
      <property name="dataSource" ref="dataSource" />
    </bean>
    

    b)指定数据源

    b1) 添加Spring JDBC与MySQL Connector依赖

    compile group: 'org.springframework', name: 'spring-jdbc', version: '5.0.4.RELEASE'
    compile group: 'mysql', name: 'mysql-connector-java', version: '6.0.6'
    

    注意:因为部分依赖包只存在于JCenter,需要在build.gradle脚本中添加jcenter maven源

    repositories {
        mavenCentral()
        jcenter()
    }
    

    b2)在app-context.xml文件中添加如下配置:

        <context:property-placeholder location="classpath:db.properties"/>
        
        <bean id="dataSource"
              class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="driverClassName">
                <value>${jdbc.driverClassName}</value>
            </property>
            <property name="url">
                <value>${jdbc.url}</value>
            </property>
            <property name="username">
                <value>${jdbc.username}</value>
            </property>
            <property name="password">
                <value>${jdbc.password}</value>
            </property>
        </bean>
    

    b3)在类路径目录下创建db.properties文件指定MySQL数据库信息

    jdbc.driverClassName = com.mysql.jdbc.Driver
    jdbc.url = jdbc:mysql://localhost:3306/d_server
    jdbc.username = root
    jdbc.password = root
    

    至此,所有的配置工作终于完成了,接下来进入最重要的编码阶段。

    由于控制层需要依赖模型层的代码,因此,我们按照从下往上的原则进行编码。
    a)先完成数据库的访问部分(DAO)

    public interface UserDAO {
        @Select("select * from t_user where username = #{username}")
        User findByUsername(@Param("username") String username);
    
        @Select("select * from t_user where username = #{username} and pwd = #{pwd}")
        User findUser(@Param("username") String username, @Param("pwd") String pwd);
    
        @Insert("insert into t_user(username, pwd) values(#{username}, #{pwd})")
        void insert(@Param("username") String username, @Param("pwd") String pwd);
    }
    

    结合MyBatis,这个部分的工作很简单,甚至DAO的实现都不需要手动编码。

    为了实现DAO的依赖注入,我们在app-context.xml配置文件中添加如下配置:

    <bean id="userDAO" class="org.mybatis.spring.mapper.MapperFactoryBean">
         <property name="mapperInterface" value="com.youngfeng.server.dao.UserDAO"/>
         <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
    </bean>
    

    b)Service层编码(也叫Domain层)
    Service部分是控制层直接调用的接口,从抽象思维来说,也应该使用面向接口的方式编码。这里为了简化,Service部分我们直接使用一个类来实现了。

    @Component("userService")
    public class UserService {
        @Autowired
        UserDAO userDAO;
    
        public boolean isExist(String username) {
            return null != userDAO.findByUsername(username);
        }
    
        public boolean isExist(String username, String pwd) {
            return null != userDAO.findUser(username, pwd);
        }
    
        public void saveUser(String username, String pwd) {
            this.userDAO.insert(username, pwd);
        }
    }
    

    c)控制层编码

    @Controller
    @RequestMapping("/user")
    public class UserController {
        @Autowired
        UserService userService;
    
        @ResponseBody
        @GetMapping("/login")
        public Response login(@RequestParam("username") String username, @RequestParam("pwd") String pwd) {
            Response response = new Response();
      
            // 先判断用户名是否存在,给定不同Code用于区分不同错误
            boolean isExist = userService.isExist(username);
            if(!isExist) {
                response.setCode(Response.CODE_USER_NOT_EXIST);
                response.setMsg("用户不存在或密码错误");
            }
          
            // 判断用户名和密码是否匹配
            isExist = userService.isExist(username, pwd);
    
            if(!isExist) {
                response.setCode(Response.CODE_USER_PWD_ERR);
                response.setMsg("用户不存在或密码错误");
            }
    
            return response;
        }
    
        @ResponseBody
        @GetMapping("/register")
        public Response register(@RequestParam("username") String username, @RequestParam("pwd") String pwd) {
            Response response = new Response();
           
            // 注册之前,判断用户名是否已存在
            boolean isExist = userService.isExist(username);
            if(isExist) {
               response.setCode(Response.CODE_USER_HAS_EXIST);
               response.setMsg("用户名已存在");
            } else {
                userService.saveUser(username, pwd);
            }
    
            return response;
        }
    
    }
    

    想必大家应该已经注意到了,控制层部分请求类型我使用了GET,这是为了方便在浏览器上面测试。测试通过后,要修改为POST请求类型。

    以上代码,我已经在浏览器上测试通过。接下来,我们马上进入iOS客户端编程。

    iOS客户端编程

    iOS部分开发工具我们使用Xcode 9.2,其实你也可以使用AppCode,这是基于IntelliJ IDE开发的一款IDE,使用习惯完全接近IntelliJ IDE。

    为了防止部分同学对Swift语言不熟悉,我们使用最常见的编程语言OC。

    完整配置请参考如下表格:

    IDE 编程语言 网络框架
    Xcode 9.2 Objective C AFNetworking

    打开Xcode,依次选择Create new Xcode Project->Single View App

    下一步填入如下信息,语言选择OC


    第一步:完成UI部分

    这一部分参考苹果官方文档,按照苹果官方推荐,我们使用Storyboard进行布局。由于我们只是完成一个简单的Demo,所有的页面将在同一个Storyboard中完成。实际开发过程中,要根据功能划分Storyboard,方便进行小组开发。


    使用约束布局我们很快完成了UI的构建,接下来进入最重要的编码阶段。约束布局的意思就是为一个控件添加N个约束,使其固定在某个位置。这个部分只要稍加尝试,就能掌握。具体的使用方法,请参考官方文档。

    第二步:创建控制器,并关联UI

    从服务器编程类推,iOS编程模型中应该也有一个叫Controller的东西。果不其然,在iOS新创建的工程中就有一个叫做ViewController的类,其父类是UIViewController。没错,这就是传说中的控制器。

    #import <UIKit/UIKit.h>
    
    @interface ViewController : UIViewController
    
    
    @end
    

    完成登录、注册功能,我们至少需要三个控制器:LoginViewController、RegisterViewController、MainViewController,分别代表登录、注册、首页三个页面。

    接下来,将控制器与UI进行关联。

    UI关联控制器部分,如果你不知道,请先参考苹果官方文档。

    事实上,Xcode的Interface Builder非常好用。按照下图操作即可:


    最后,关联按钮点击事件以及输入框。

    选中控件并按住鼠标右键拖拽到控制器源码中,松开,并选择相应类型即可:


    以登录控制器为例,拖拽完成后的源码如下:

    @interface LoginViewController ()
    @property (weak, nonatomic) IBOutlet UITextField *mUsernameTextField;
    @property (weak, nonatomic) IBOutlet UITextField *mPwdTextField;
    
    @end
    
    @implementation LoginViewController
    
    - (IBAction)login:(id)sender {
    }
    
    - (IBAction)goToRegister:(id)sender {
    }
    

    接下来进入网络部分编程。

    为了简化网络部分编程,我们引入AFNetworking框架。还记得服务端编程是怎么引入依赖的吗?没错,是Gradle。iOS端也有类似的依赖管理工具Cocoapods,这个部分如果不会依然请你参考官方文档。

    使用如下步骤安装依赖(这里假设你已经正确安装了Cocoapod):
    a)在根目录下面创建Podfile文件,并添加如下内容:

    source 'https://github.com/CocoaPods/Specs.git'
    platform :ios, '8.0'
    
    target 'IOSClient' do
    pod 'AFNetworking', '~> 3.0'
    end
    

    b)安装依赖

    pod install
    

    PS:可能有人会问,为什么服务端编程没有安装依赖的步骤。其实,很简单,intelliJ IDE非常智能,它自动检测了build.gradle文件的修改。一旦发现修改,自动安装依赖。因此,看起来就像没有依赖安装这个步骤一样。事实上,Cocoapod并非苹果官方的产品,如果产品来自苹果官方,恐怕Xcode也会支持自动安装依赖。

    依赖安装完成后,为了更好地服务我们的业务。我们对网络请求做一点简单封装,增加HttpClient类,仅提供一个POST请求接口即可。

    //
    //  HttpClient.m
    //  IOSClient
    //
    //  Created by 欧阳锋 on 17/03/2018.
    //  Copyright © 2018 xbdx. All rights reserved.
    //
    
    #import "HttpClient.h"
    #import <AFNetworking.h>
    #import "Response.h"
    
    @implementation HttpClient
    
    static const NSString *BASE_URL = @"http://192.168.31.146:8080";
    
    - (instancetype)init {
        self = [super init];
        if (self) {
            self.baseUrl = BASE_URL;
        }
        return self;
    }
    
    + (HttpClient *)initWithBaseUrl:(NSString *)baseUrl {
        HttpClient *client = [[HttpClient alloc] init];
        client.baseUrl = baseUrl;
        
        return client;
    }
    
    + (HttpClient *)sharedInstance {
        static HttpClient *client = nil;
        static dispatch_once_t once;
        dispatch_once(&once, ^{
            client = [[self alloc] init];
        });
        return client;
    }
    
    - (void)POST:(NSString *)url params:(NSDictionary *)params success:(void (^)(NSString *, id))success error:(void (^)(NSString *, NSInteger, NSInteger, NSString *))error {
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        manager.responseSerializer = [AFJSONResponseSerializer serializer];
        [[AFHTTPSessionManager manager] POST: [_baseUrl stringByAppendingString:url]
                                  parameters: params
                                    progress: nil
                                     success: ^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                                         if(nil != success) {
                                             if(nil != responseObject) {
                                                 if([responseObject isKindOfClass: [NSDictionary class]]) {
                                                     NSInteger code = ((NSDictionary *)responseObject)[@"code"];
                                                     
                                                     if(SUCCESS == code) {
                                                         success(url, responseObject);
                                                     } else {
                                                         if(nil != error) {
                                                             NSString *msg = ((NSDictionary *)responseObject)[@"msg"];
                                                             error(url, SC_OK, code, msg);
                                                         }
                                                     }
                                                 }
                                             }
                                         }
                                     }
                                     failure: ^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull nsError) {
                                         if(nil != nsError) {
                                             error(url, nsError.code, nil, nsError.description);
                                         }
                                     }];
        
    }
    
    @end
    

    为了简化JSON解析,我们增加一个最常见的Json解析库 jsonmodel 库。等待对话框也使用最常见的第三方库 SVProgressHUD

    pod 'JSONModel'
    pod 'SVProgressHUD'
    

    安装依赖使用同样的命令pod install即可。

    接下来,我们添加登录注册逻辑,完成最后部分编码:

    // 登录部分逻辑
    - (IBAction)login:(id)sender {
        [SVProgressHUD show];
        HttpClient *client = [HttpClient sharedInstance];
        [client POST: @"/user/login"
              params: @{@"username" : _mUsernameTextField.text, @"pwd" : _mPwdTextField.text}
             success:^(NSString *url, id data) {
                 [SVProgressHUD dismiss];
                 
                 if([data isKindOfClass: [NSDictionary class]]) {
                     // 例子代码,这里不做严格判断了
                     User *user = [[User alloc] initWithDictionary: data[@"data"] error: nil];
                     [self pushToMainViewController: user];
                 }
             } error:^(NSString *url, NSInteger httpCode, NSInteger bizCode, NSString *error) {
                 [SVProgressHUD dismiss];
                 
                 [self promptError: error];
             }];
    }
    
    - (void)pushToMainViewController: (User *) user {
        UIStoryboard *storyboard = [UIStoryboard storyboardWithName: @"Main" bundle: [NSBundle mainBundle]];
        MainViewController *mainViewController = [storyboard instantiateViewControllerWithIdentifier: @"mainViewController"];
        mainViewController.user = user;
        [self.navigationController presentViewController: mainViewController animated: YES completion: nil];
    }
    
    // 注册部分逻辑
    - (IBAction)register:(id)sender {
        NSString *username = _mUsernameTextField.text;
        NSString *pwd = _mPwdTextField.text;
        NSString *confrimPwd = _mConfirmTextField.text;
        
        if([StringUtil isBlankString: username]) {
            [self promptError: @"请输入用户名"];
            return;
        }
        
        if([StringUtil isBlankString: pwd]) {
            [self promptError: @"请输入用户密码"];
            return;
        }
        
        if([StringUtil isBlankString: confrimPwd]) {
            [self promptError: @"请输入确认密码"];
            return;
        }
        
        if(![pwd isEqualToString: confrimPwd]) {
            [self promptError: @"两次密码输入不一致,请重新输入"];
            return;
        }
        
        HttpClient *client = [HttpClient sharedInstance];
        [client POST: @"/user/register" params: @{@"username" : username, @"pwd" : pwd} success:^(NSString *url, id data) {
            [self promptError: @"注册成功" handler:^(UIAlertAction *action) {
                [self.navigationController popViewControllerAnimated: YES];
            }];
        } error:^(NSString *url, NSInteger httpCode, NSInteger bizCode, NSString *error) {
            [self promptError: error];
        }];
    }
    

    通过上面的步骤,我们已经完成了iOS客户端的开发。苹果官方默认支持的就是经典的MVC模式。因此,我们完全参考服务端开发模式完成了iOS客户端的开发。你唯一需要克服的是对新语言的恐惧,以及适应UI开发的节奏。事实上,大部分服务端程序员都害怕UI编程。

    最后,我们进入Android客户端编程。

    Android客户端编程

    Android部分开发工具,我们使用Android Studio,网络框架使用Retrofit,完整配置参考下方表格:

    IDE 编程语言 网络框架
    Android Studio Java 1.8 Retrofit

    打开Android Studio,选择Start a new Android Studio Project,在打开的页面中填入以下信息:



    剩下步骤全部选择默认。

    按照iOS编码部分类推,Android端应该也有一个类似UIViewController的控制器。果不其然,在模板工程中就有一个MainActivity,其父类是AppCompatActivity,这就是Android的控制器。

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    }
    

    PS:事实上Android早期版本的控制器就叫Activity,由于系统设计不断变更,最终诞生了兼容性子类AppCompatActivity。这都是早期设计不够严谨,导致的问题。相对而言,iOS端的设计就靠谱了许多。

    同样地,在开始编码之前,我们加入所需的第三方依赖。那么,问题来了。Android端如何添加依赖呢?

    碰巧,Android端主要的开发语言就是Java。因此,我们依然可以使用Gradle进行依赖管理。碰巧,Android Studio默认支持的就是使用Gradle进行依赖管理。

    首先,在app模块目录的build.gradle添加 Retrofit 依赖:

    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    

    添加完成后,点击文件右上方Sync now下载依赖:


    相对于AFNetworking,Retrofit设计的更加精妙。参考Retrofit官方文档,我们开始加入登录注册逻辑:

    public interface UserService {
        
        @FormUrlEncoded
        @POST("user/login")
        Call<User> login(@Field("username") String username, @Field("pwd") String pwd);
    
        @FormUrlEncoded
        @POST("user/register")
        Call<User> register(@Field("username") String username, @Field("pwd") String pwd);
    }
    

    Retrofit设计的其中一个巧妙之处在于:你只需要定义好接口,具体的实现交给Retrofit。你可以看到,上面的代码中我们仅仅定义了请求的类型,以及请求所需要的参数就已经完成了网络部分的所有工作。

    不过,操作这个接口实现,需要使用Retrofit实例。接下来,我们参考官方文档生成一个我们需要的Retrofit实例。

    在生成Retrofit实例之前,还需要注意一个事情。还记得iOS端我们是怎么完成JSON解析的吗?是的,我们使用了第三方库jsonmodel。

    在Json解析的设计上,Retrofit也相当巧妙。Retrofit提供了一个转换适配器用于实现Json数据的自动转换。使用它,你可以自定义自己的Json转换适配器;也可以使用官方已经实现好的适配器。一旦添加了这个适配器,所有的Json解析工作Retrofit就会自动帮忙完成。不再需要像AFNetworking一样在回调里面反复进行Json解析操作。

    因此,我们增加一个官方版本的Json转换适配器依赖 converter-json:

    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
    

    加入Json适配器之后,我们使用一个新的Retrofit管理类RetrofitManager用于生成项目所需要的Retrofit实例。完整代码如下:

    public class RetrofitManager {
        private static final String BASE_URL = "http://192.168.31.146:8080";
    
        public static Retrofit create(String baseUrl) {
            return new Retrofit.Builder()
                              .baseUrl(baseUrl)
                              .addConverterFactory(GsonConverterFactory.create())
                              .build();
        }
        
        public static Retrofit createDefault() {
            return create(BASE_URL);
        }
    }
    

    接下来,我们尝试在MainActivity中测试登录接口,确定是否编写正确。我们在MainActivity的onCreate方法中加入如下代码:

    Retrofit retrofit = RetrofitManager.createDefault();
    
    UserService userService = retrofit.create(UserService.class);
    Call < User > call = userService.login("1", "1");
    call.enqueue(new Callback < User > () {
      
        @Override 
        public void onResponse(Call < User > call, Response < User > response) {
            Log.e("MainActivity", call + "" + response);
        }
    
        @Override 
        public void onFailure(Call < User > call, Throwable t) {
            Log.e("MainActivity", call + "" + t);
        }
    });
    

    打开模拟器,运行,你将看到以下错误:

    03-18 04:03:24.546 7277-7277/com.youngfeng.androidclient D/NetworkSecurityConfig: No Network Security Config specified, using platform default
    03-18 04:03:24.574 7277-7277/com.youngfeng.androidclient W/System.err: java.net.SocketException: Permission denied
    03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err:     at java.net.Socket.createImpl(Socket.java:454)
    03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err:     at java.net.Socket.getImpl(Socket.java:517)
    03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err:     at java.net.Socket.setSoTimeout(Socket.java:1108)
    03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err:     at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.java:238)
    03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err:     at okhttp3.internal.connection.RealConnection.connect(RealConnection.java:160)
    03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err:     at okhttp3.internal.connection.StreamAllocation.findConnection(StreamAllocation.java:257)
    03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err:     at okhttp3.internal.connection.StreamAllocation.findHealthyConnection(StreamAllocation.java:135)
    03-18 04:03:24.578 7277-7277/com.youngfeng.androidclient W/System.err:     at okhttp3.internal.connection.StreamAllocation.newStream(StreamAllocation.java:114)
    

    提示我们权限被拒绝,这是和iOS平台不一样的地方。如果你的应用需要使用网络,你需要在清单文件中手动指定使用网络权限。为此,我们在AndroidManifest.xml文件中添加如下配置:

    <uses-permission android:name="android.permission.INTERNET" />
    

    再次运行,一切正常。

    注意:这里的service部分和服务端的service不一样,它只是Retrofit用于将网络接口分模块处理的一种手段,不要混淆。

    上面说到,Android里面的AppCompatActivity就是MVC中的控制器,接下来我们就完成最重要的控制器以及UI部分编码。

    a)创建LoginActivity以及布局文件activity_login.xml,在其onCreate方法中使用setContentView接口进行关联。

    b)UI编程
    你相信吗?一旦你学会了一门新的技术,你的技能就会Double。

    iOS UI部分我们使用了约束布局的方式完成了整体布局,Android是否也可以使用约束布局呢?答案是:当然可以。

    事实上,Android官方也推荐使用这种布局方式进行页面布局。

    切换到可视化布局模式,我们依然使用拖拽UI的方式完成整个布局,完整代码请参考文章最后的附录部分:


    PS:目前,Android端的约束布局相对iOS逊色不少,希望后面官方能够提供更多功能支持。

    按照同样的方式完成注册页面和首页布局,UI部分开发完成后,尝试跳转到指定控制器。你会发现,出错了。这也是和iOS不一样的地方,Android端四大组件必须在清单文件中注册。具体是什么原因,请自行思考,这不是本文研究的重点。

    因此,我们首先在清单文件中对所有控制器进行注册:

        <activity android:name=".login.LoginActivity"
                android:screenOrientation="portrait">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    
            <activity android:name=".MainActivity"
                android:screenOrientation="portrait"/>
    
            <activity android:name=".register.RegisterActivity"
                android:screenOrientation="portrait"/>
    

    然后,以登录为例,我们在控制器中完善登录逻辑:

    public class LoginActivity extends BaseActivity {
        private EditText mUsernameEdit;
        private EditText mPwdEdit;
        private Button mLoginBtn;
        private Button mRegisterBtn;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_login);
    
            mUsernameEdit = findViewById(R.id.edit_username);
            mPwdEdit = findViewById(R.id.edit_pwd);
            mLoginBtn = findViewById(R.id.btn_login);
            mRegisterBtn = findViewById(R.id.btn_register);
    
            mLoginBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    login(mUsernameEdit.getText().toString(), mPwdEdit.getText().toString());
                }
            });
    
            mRegisterBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent intent = new Intent(LoginActivity.this, RegisterActivity.class);
                    startActivity(intent);
                }
            });
        }
    
        private void login(String username, String pwd) {
            Retrofit retrofit = RetrofitManager.createDefault();
            UserService userService = retrofit.create(UserService.class);
            Call<HttpResponse<User>> call = userService.login(username, pwd);
    
            showLoading(true);
            call.enqueue(new Callback<HttpResponse<User>>() {
                @Override
                public void onResponse(Call<HttpResponse<User>> call, Response<HttpResponse<User>> response) {
                    showLoading(false);
    
                    // 例子代码,暂时忽略空值判断
                    if(HttpResponse.CODE_SUCCESS != response.body().getCode()) {
                        promptError(response.body().getMsg() + "");
                    } else {
                        Intent intent = new Intent(LoginActivity.this, MainActivity.class);
                        intent.putExtra(MainActivity.KEY_USER, response.body().getData());
                        startActivity(intent);
                        finish();
                    }
                }
    
                @Override
                public void onFailure(Call<HttpResponse<User>> call, Throwable t) {
                    showLoading(false);
    
                    promptError(t.getMessage() + "");
                }
            });
        }
    }
    

    至此,按照iOS的开发模式,我们完成了Android客户端的开发。与iOS不同的地方是,Android端控制器必须在清单文件中注册。程序员不能主动创建Activity,只能间接使用intent进行通信。而对于布局,两者都可以使用约束管理的方式完成。从这个角度来说,Android端和iOS端开发切换的难度还是比较低的。

    距离全栈还差最后一步

    至此,我们已经完成了文章开头定下的目标。以MVC架构为基础,完成了服务端、iOS客户端、Android客户端编码。

    然而,很多同学希望成为一个全栈工程师。按照现在的主流开发分支来说,成为一个全栈工程师,你还需要掌握Web前端开发。那么,问题来了,Web前端开发是否也是使用MVC架构呢?

    事实上,如果你使用 Angular,你应该早就习惯了MVC。而如果你偏爱React,你恐怕会搭配Redux,使用这种响应式的数据流框架编码。如果你使用Vue,你恐怕也会选择MVC或者MVVM架构。

    如果你选择使用MVC,你依然可以按照类推的方式来学习。由于文章篇幅的原因,这部分就不予展示了。

    编后说

    这篇文章我们以MVC为架构,从服务端编程开始,使用类推的方式依次完成了iOS客户端、Android客户端的开发。

    有人可能会说,文章中的例子太简单,没有实际意义。事实上,在学习一门新技术的时候,就要从最基础的部分出发,建立对这门技术的最初印象。很多同学容易一开始就陷入细节当中无法自拔,产生的最直观的结果就是对新技术产生恐惧。因此,你常常可以看到一个程序员面对新东西骂娘,无怪乎。

    其实,如果你慢慢进入到细节编程中,你会发现技术之间越来越多的相似性。这个时候你的积极性就会越来越高,编码也会更加得心应手。

    我在学习一门新技术的时候,都是先从相似性开始。然后,再去攻克不同的部分。从不同的部分中去提炼相同的思想,这样在面对不同问题的时候,我始终可以使用同样的思想去解决。

    当然,我想,你应该会说。虽然克服了框架问题,可是不同的编程语言千差万别。我们无法从一门语言快速过渡到另外一门语言,这在学习新技术的时候才是最大的拦路虎。

    你说的很对,这恰好是下一个我想和你分享的问题。关注我的简书,下一篇我们一起探讨《技术相对论之编程语言》

    附录

    本篇例子完整代码:https://github.com/yuanhoujun/it-theory-of-relativity
    IntelliJ IDEA下载地址:https://www.jetbrains.com/idea/
    Tomcat下载地址:http://tomcat.apache.org/
    iOS开发者官网:https://developer.apple.com/
    Android开发者官网:https://developer.android.com/index.html

    相关文章

      网友评论

      • IT人故事会:做开发很累,还的学习,之前你这个我也碰到过,但是没记录谢谢了
        欧阳锋:@独白术 LOL,看你怎么理解了
        独白术:@欧阳锋 过去看了看下,咋地说了,更像是鸡汤,不过有句话确实很实在,公司需要的是广度,个人需要的是深度
        欧阳锋:@IT人故事会 👊

      本文标题:技术相对论之软件架构

      本文链接:https://www.haomeiwen.com/subject/ywljfftx.html