分类 戎码一生 下的文章

本篇要点

  • shiro简介及核心组件或功能介绍
  • SpringBoot与shiro快速整合
  • 分析身份认证和授权的流程
  • 介绍shiro的拦截器机制
  • 介绍shiro的权限注解

    一、shiro是什么?用来干什么?

Apache Shiro 是 Java 的一个安全框架。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。

shiro的基本功能点有很多,详细介绍可以参照官方文档,这里分析最常用的两个:认证Authentication与授权Authorization,这俩功能相近,长相也差不太多,但千万注意,不要搞错。

  • 何为认证?即验证用户是否拥有响应的身份。认证是比较好理解的,一般常见的场景就是登录场景,验证你的账号密码是否合理且合法,这就是认证的过程。
  • 何为授权?即验证某个已认证的用户是否拥有某个权限。授权则是发生在认证之后,举个例子,超级管理拥有所有的权限便可以为所欲为,而游客可能只能拥有浏览的权限,不允许操作。

其他比较重要的功能点介绍一下:

  • Subject:代表当前用户,准确来说是与应用代码直接交互的对象,与Subject的交互最终都会委托为SecurityManager。
  • SecurityManager:安全管理器,管理所有的Subject,是shiro的核心,如果学习过SpringMVC,可以看成类似DispatcherServlet的功能。
  • Realm:shiro可以从Realm中获取安全数据,如用户,角色,权限等,提供了认证与授权的主要方法接口。

二、SpringBoot快速整合shiro

导入shiro必要的依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.9.1</version>
        </dependency>

创建数据源,准备加载用户数据的方法

这一步主要是为了省去和数据库交互,直接用HashMap模拟缓存用户数据。

/**
 * 准备实验数据
 * @author Summerday
 */
public class DataSource {

    /**
     * k 用户名
     * v 用户信息
     */
    private static final Map<String, UserBean> data = new HashMap<>();

    static {
        data.put("hyh",new UserBean("hyh","123456","user","view"));
        data.put("sum",new UserBean("sum","123456","admin","view,edit"));
    }

    public static Map<String, UserBean> getData() {
        return data;
    }
}

@Service
public class UserService {
    
    //简单定义加载用户数据的方法,可以考虑加入缓存
    public UserBean getUser(String username) {
        return DataSource.getData().get(username);
    }

}

定义核心组件Realm

/**
 * 核心组件:认证+授权
 * @author Summerday
 */

public class AuthRealm extends AuthorizingRealm {

    private static final Logger log = LoggerFactory.getLogger(AuthRealm.class);

    private static final String SESSION_KEY = "USER_SESSION";

    @Resource
    private UserService userService;

    /**
     * 只有需要验证权限时才会调用, 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 获取user
        Session session = SecurityUtils.getSubject().getSession();
        UserBean user = (UserBean) session.getAttribute(SESSION_KEY);
        // 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        String role = user.getRole();
        String permission = user.getPermission();
        // 定义role和permission
        info.addRole(role);
        info.addStringPermissions(Arrays.asList(permission.split(",")));
        return info;
    }

    /**
     * 登录时调用该方法,返回用户名密码信息,之后将会调用CredentialsMatcher的doCredentialsMatch进       * 行信息验证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        //获取user
        String username = (String) auth.getPrincipal();
        // 如果user为空,抛出UnknownAccountException异常
        UserBean user = Optional
                .ofNullable(userService.getUser(username))
                .orElseThrow(UnknownAccountException::new);
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,user.getPassword(),getName());
        Session session = SecurityUtils.getSubject().getSession();
        session.setAttribute(SESSION_KEY,user);
        return info;
    }
}

用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能验证用户身份:

principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。

credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。

最常见的 principals 和 credentials 组合就是用户名 / 密码了。接下来先进行一个基本的身份认证。

定义shiroConfig,注入核心bean

@Configuration
public class ShiroConfig {

    // shiro的生命周期处理器:用于在实现了 Initializable 接口的 Shiro bean 初始化时
    // 调用 Initializable 接口回调,在实现了 Destroyable 接口的 Shiro bean 销毁时调用 Destroyable 接口回调
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    //提供Realm实例
    @Bean
    public AuthRealm authRealm() {
        return new AuthRealm();
    }

    //权限管理,配置主要是Realm的管理认证
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置Realm
        securityManager.setRealm(authRealm());
        return securityManager;
    }

    //Shiro 提供了相应的注解用于权限控制,配置以开启用于权限注解的解析和验证如 @RequiresRoles
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    //用于开启 Shiro Spring AOP 权限注解的支持;<aop:config proxy-target-class="true"> 表示代理类。使用cglib
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    //配置路径拦截规则
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //指定登录页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        //首页:指定登录成功页面
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/denied");
        loadShiroFilterChain(shiroFilterFactoryBean);
        return shiroFilterFactoryBean;
    }

    /**
     * 定义url-filter规则,这一步可以采取从配置文件中读取,注意拦截的顺序
     */
    private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {

        Map<String, String> map = new HashMap<>();
        //匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤
        map.put("/hello","anon");
        //退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/)
        map.put("/logout", "logout");
        //基于表单的拦截器;如 “`/**=authc`”,如果没有登录会跳到相应的登录页面登录
        map.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
    }
}

定义登录Controller,理解login的流程

@Slf4j
@RestController
public class LoginController {

    @GetMapping(value = "/hello")
    public AjaxResult hello() {
        return AjaxResult.ok("不用登陆,直接访问");
    }

    @GetMapping(value = "/index")
    public AjaxResult index() {
        return AjaxResult.ok("登录成功");
    }

    @GetMapping(value = "/denied")
    public AjaxResult denied() {
        return AjaxResult.error("权限不足");
    }

    @GetMapping("/login")
    public AjaxResult login(String username, String password) {
        // 自动绑定到当前线程
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            //自动委托给 SecurityManager.login
            subject.login(token);
        } catch (UnknownAccountException e) {
            log.error("对用户[{}]进行登录验证,验证未通过,用户不存在", username);
            token.clear();
            return AjaxResult.error(e.getMessage());
        } catch (ExcessiveAttemptsException e) {
            log.error("对用户[{}]进行登录验证,验证未通过,错误次数过多", username);
            token.clear();
            return AjaxResult.error(e.getMessage());
        } catch (AuthenticationException e) {
            log.error("对用户[{}]进行登录验证,验证未通过,堆栈轨迹如下", username, e);
            token.clear();
            return AjaxResult.error(e.getMessage());
        }
        return AjaxResult.ok("登录成功");
    }

}

定义UserController,理解权限注解的使用

@RestController
@RequestMapping("/user")
public class UserController {

    private static final Logger log = LoggerFactory.getLogger(UserController.class);

    @Resource
    private UserService userService;

    /**
     * RequiresRoles 是所需角色 包含 AND 和 OR 两种
     * RequiresPermissions 是所需权限 包含 AND 和 OR 两种
     *
     * @return msg
     */

    @RequiresRoles(value = {"admin"})
    //@RequiresPermissions(value = {"user:list", "user:query"}, logical = Logical.OR)
    @GetMapping("/rq_admin")
    public AjaxResult requireRoles() {
        return AjaxResult.ok("result : admin");
    }

    @RequiresPermissions(value = {"edit", "view"}, logical = Logical.AND)
    @GetMapping("/rq_edit_and_view")
    public AjaxResult requirePermissions() {
        return AjaxResult.ok("result : edit + view");
    }
}

三、认证流程

身份认证是项目安全中比较重要的一环,上面的登录流程中,我们看到了如下代码:

        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            //自动委托给 SecurityManager.login
            subject.login(token);
        } catch (UnknownAccountException e) {
            log.error("对用户[{}]进行登录验证,验证未通过,用户不存在", username);
            token.clear();
        }

我们之前提到过,SecurityManager其实是subject.login的方法的真正执行者,那真正的逻辑是怎样的呢?跟着源码一步步debug我们就能够知道具体的流程如下:

  1. Subject.login(token) 进行登录,其会自动委托给 Security Manager
  2. Security Manager之后委托给Authenticator进行身份验证:this.authenticator.authenticate(token);
  3. 默认使用ModularRealmAuthenticatordoAuthenticate根据配置的Realms数量决定是否采用多Realm身份验证。
  4. AuthenticationInfo info = realm.getAuthenticationInfo(token);获取info,可以实现该接口自定义realm的info获取逻辑。
  5. 由于接口回调,带有用户身份信息的info在一步步往回传,如果出现AuthenticationException,将会认证失败,执行onFailedLogin逻辑。
  6. 如果一路上没有错误,则创建subject,并返回。

四、授权流程

授权操作的本质,其实对subject主体是否具有权限和角色进行判断:

  1. 调用Subject.isPermtted/hasRole接口,其会自动委托给 Security Manager
  2. Security Manager之后委托给Authorizer进行授权操作,假设我们调用isPermitted(“user:view”),其首先会通过 PermissionResolver 把字符串转换成相应的 Permission 实例;
  3. 在进行授权之前,将会调用相应的Realm为Subject授予role和permissions。
  4. 默认使用ModularRealmAuthorizerdoAuthenticate根据配置的Realms数量决定是否采用多Realm身份验证。如果isPermitted/hasRole匹配成功,返回true。

五、拦截器机制

参考:Shiro 拦截器机制

Shiro 内置了很多默认的拦截器,比如身份验证、授权等相关的。默认拦截器可以参考 org.apache.shiro.web.filter.mgt.DefaultFilter 中的枚举拦截器:

身份验证相关:

  1. authc:FormAuthenticationFilter,基于表单的拦截器,/**=authc,如果没有登录会跳到相应的登录页面。
  2. authBasic:BasicHttpAuthenticationFilter,BasicHTTP身份拦截器。
  3. logout:LogoutFilter,退出拦截器,/logout=logout,退出成功后重定向。
  4. user:UserFilter,用户拦截器,/**=user,已经身份验证或记住我登录的用户可以直接访问。
  5. anon:AnonymousFilter,匿名拦截器,/static/**=anon,不需要登录即可访问,一般用于静态资源过滤。

授权相关:

  1. roles:RolesAuthorizationFilter,角色授权拦截器,/admin/**=roles[admin]验证用户是否拥有所有角色。
  2. perms:PermissionsAuthorizationFilter,权限授权拦截器,/user/**=perms["user:create"],验证用户是否拥有所有权限。
  3. port:PortFilter,端口拦截器,/test=port[80],可以通过的端口为80。
  4. rest:HttpMethodPermissionFilter,rest风格拦截器。
  5. ssl:SslFilter,SSL拦截器,只有https协议才能通过,否则跳转会443端口。

其他:

noSessionCreation:NoSessionCreationFilter,不创建会话拦截器,调用 subject.getSession(false) 不会有什么问题,但是如果 subject.getSession(true) 将抛出 DisabledSessionException 异常。

六、权限注解

@RequiresAuthentication:表示当前 Subject 已经通过 login 进行了身份验证;即 Subject.isAuthenticated() 返回 true。

@RequiresUser:表示当前 Subject 已经身份验证或者通过记住我登录的。

@RequiresGuest:表示当前 Subject 没有身份验证或通过记住我登录过,即是游客身份。

@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND):表示当前 Subject 需要角色 admin 和 user。

@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR):表示当前 Subject 需要权限 user:a 或 user:b。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public class ManageAuthorizeAttribute : ActionFilterAttribute, IAuthorizationFilter
    {

        public void OnAuthorization(AuthorizationFilterContext context)
        {
            //先验证是否登录
            //if (!IsLogin(context))
            //{
            //    if (context.HttpContext.Request.IsAjaxRequest())
            //    {
            //        context.Result = new JsonResult() { Data = "您必须先以管理员身份登录下后台,才能继续操作" };
            //        return;
            //    }
            //    else
            //    {

            //        context.Result = new ContentResult() { Content = "<script type=\"text/javascript\">parent.location.href='" + siteConfig.weburl + "/User/Login?returnUrl='+encodeURIComponent(parent.window.location.href);</script>" };
            //        return;
            //    }
            //}

            //此处应该封装成一个方法,test为登录用户名 ,db不能直接写,调用方法可以
            //var u = db.Users.Include(x => x.Roles).ThenInclude(x => x.Permissions).Where(x => x.LoginName == "test").FirstOrDefault();
            //if (u != null)
            //{
            //    List<Permission> plist = new List<Permission>();
            //    foreach (var item in u.Roles)
            //    {
            //        plist.AddRange(item.Permissions);
            //    }

            //    var controllerName = context.RouteData.Values["controller"].ToString();
            //    var actionName = context.RouteData.Values["action"].ToString();


            //    bool HasPermission = plist.Any(p => p.ControllerName.ToLower().Equals(controllerName, StringComparison.CurrentCultureIgnoreCase)
            //        && p.ActionName.ToLower().Equals(actionName, StringComparison.CurrentCultureIgnoreCase));

            //    if (!HasPermission)
            //    {
            //        context.Result = Unauthorized();
            //        return;
            //    }
        }

        protected virtual bool IsLogin(AuthorizationFilterContext filterContext)
        {
            if (filterContext.HttpContext.User.Identity.IsAuthenticated)
            {
                return true;
            }
            return false;
        }
    }

支付宝沙箱支付的Demo,不能用于项目,入门的一个小选择吧

先引入支付宝的SDK

 //应用私钥
            string privateKey = "";   //1
            //支付宝公钥
            string alipayPublicKey = "";  //2
            AlipayConfig alipayConfig = new AlipayConfig
            {
                ServerUrl = "支付宝网关地址",   //3
                AppId = "APPID",            //4
                PrivateKey = privateKey,
                Format = "json",
                AlipayPublicKey = alipayPublicKey,
                Charset = "UTF-8",
                SignType = "RSA2"              
            };
            IAopClient alipayClient = new DefaultAopClient(alipayConfig);
            AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
            request.SetReturnUrl("");//设置同步通知地址
            request.SetNotifyUrl("");//设置异步通知地址
            AlipayTradePagePayModel model = new AlipayTradePagePayModel
            {
                OutTradeNo = orderNo,
                TotalAmount = orderMoney,
                Subject = orderTitle,
                ProductCode = "FAST_INSTANT_TRADE_PAY",        
            };
            request.SetBizModel(model);
            AlipayTradePagePayResponse response = alipayClient.pageExecute(request);

            if (!response.IsError)
            {
                ViewBag.Temp = response.Body;
            }

EF Core迁移 (provider: SSL Provider, error: 0 – 证书链是由不受信任的颁发机构颁发的。)

使用Sql2022,EFCore 7的时候,如果遇到这个错误,只需要在链接字符串中添加

Encrypt=False;TrustServerCertificate=True

即可解决
原因大概如下

MicroSoft.Data.Sqlclient版本如果超过4.0,引入了有关连接字符串的重大更改。如果您的 SQL Server 不使用加密,您必须通过添加在连接字符串中明确指定Encrypt=False。否则连接将失败并显示SqlException.
TrustServerCertificate=True意思就是信任服务器,连接数据库的时候使用的TLS协议,需要CA签名的证书,但是SqlServer2022 自签名的证书不在授信范围内

参考:参考连接

软件设计原则是一组用于设计高质量、可维护、可扩展和可重用软件的指导性原则。以下是一些常见的软件设计原则:

  1. 单一职责原则(SRP):一个类应该只有一个职责。
  2. 开放-关闭原则(OCP):软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
  3. 里氏替换原则(LSP):子类型必须能够替换它们的父类型。
  4. 接口隔离原则(ISP):客户端不应该被强制依赖于它们不使用的接口。
  5. 依赖倒置原则(DIP):高层模块不应该依赖于低层模块,而应该依赖于抽象。
  6. 迪米特法则(LoD):一个对象应该对其他对象有最少的了解。
  7. 合成复用原则(CRP):优先使用对象组合而不是继承来达到复用。

这些原则是软件设计中的基础,它们帮助开发人员设计出高质量、可维护和可扩展的软件系统。

单一职责原则

单一职责(Single Responsibility Principle)(SRP)指的是一个类或模块应该只有一个职责。这意味着一个类或模块应该只负责一个功能或任务,这样可以避免代码的复杂性和耦合性。例如,我们可以定义一个名为“UserService”的类来处理用户的注册、登录和个人资料管理等功能。这个类只有一个职责,即管理用户相关的功能。

开闭原则

开放-关闭原则(Open-Closed Principle)(OCP)指的是软件实体应该对扩展开放,对修改关闭。这意味着当我们需要修改一个模块或类时,应该通过扩展它来实现,而不是直接修改它。这样可以避免对现有代码的破坏,提高代码的可重用性和可扩展性。例如,我们可以定义一个名为“PaymentService”的类来处理支付功能。当我们需要添加新的支付方式时,可以通过扩展该类来实现,而不是直接修改原有代码。

里氏替换原则

里氏替换原则(Liskov Substitution Principle)(LSP)指的是子类型必须能够替换它们的父类型。这意味着当我们使用一个父类的对象时,可以用它的任何子类对象来替换它,而不会影响程序的正确性。例如,我们可以定义一个名为“Animal”的抽象类,它有一个“MakeSound”方法。当我们定义一个“Dog”类时,可以继承“Animal”类,并实现“MakeSound”方法。然后,我们可以使用“Animal”类的对象来调用“Dog”类的“MakeSound”方法,而不会影响程序的正确性。

接口隔离原则

接口隔离原则(Interface Segregation Principle)(ISP)指的是客户端不应该被强制依赖于它们不使用的接口。这意味着当一个接口包含了多个方法时,客户端只需要使用其中的一部分方法时,应该将这些方法拆分成多个小的接口。这样可以减少客户端的依赖,提高代码的灵活性和可重用性。例如,我们可以定义一个名为“ILogger”的接口,它有多个方法,如“LogInfo”、“LogError”、“LogWarning”等。当我们需要使用“LogInfo”方法时,可以定义一个新的接口,例如“IInfoLogger”,它只包含“LogInfo”方法。这样,客户端只需要依赖于“IInfoLogger”接口,而不需要依赖于整个“ILogger”接口。

依赖倒置原则

依赖倒置原则(Dependency Inversion Principle)(DIP)指的是高层模块不应该依赖于低层模块,而应该依赖于抽象。这意味着模块之间的依赖关系应该通过接口或抽象类来实现,而不是直接依赖于具体的实现类。这样可以减少模块之间的耦合性,提高代码的可扩展性和可维护性。例如,我们可以定义一个名为“DataAccess”的抽象类,它有一个“GetData”方法。当我们定义一个“SqlServerDataAccess”类时,可以继承“DataAccess”类,并实现“GetData”方法。然后,我们可以在高层模块中依赖于“DataAccess”抽象类,而不需要直接依赖于“SqlServerDataAccess”类。

迪米特法则

迪米特法则(LoD)指的是一个对象应该对其他对象有最少的了解。这意味着一个对象应该只与其直接的朋友(即与其通信的对象)交互,而不应该与其他对象交互。这样可以减少对象之间的耦合性,提高代码的可维护性和可扩展性。例如,我们可以定义一个名为“Order”的类,它有一个“Customer”属性。当我们需要获取客户的姓名时,可以在“Order”类中调用“Customer”类的“GetName”方法,而不需要直接访问“Customer”类的属性。

合成复用原则

合成复用原则(CRP)指的是优先使用对象组合而不是继承来达到复用。这意味着当我们需要复用代码时,应该通过组合现有的对象来实现,而不是通过继承现有的类来实现。这样可以减少代码的耦合性,提高代码的可重用性和可维护性。例如,我们可以定义一个名为“EmailSender”的类,它有一个“SmtpClient”属性。当我们需要发送邮件时,可以通过“SmtpClient”对象来发送邮件,而不需要在“EmailSender”类中实现邮件发送的具体逻辑

高内聚低耦合

高内聚和低耦合是两个面向对象编程中重要的概念,它们是编写可维护、可扩展和可重用的代码的关键。

高内聚(High Cohesion)

高内聚指的是在一个类、模块或组件中,其包含的各个方法和属性都是高度相关的,并且彼此之间协同工作以实现某个特定的功能。简而言之,高内聚表示一个类或模块的各个元素之间紧密相关,都为实现同一功能而存在。
一个高内聚的模块或类应该具有以下特点:
其各个元素都围绕着一个明确的目标或任务而存在;
其各个元素之间的耦合度较低,相互独立,可以单独修改或测试;
其各个元素之间的交互较少,因此具有更好的可维护性和可重用性;
其各个元素之间的接口简单明了,易于理解和使用。
高内聚是面向对象编程的基本原则之一,它可以帮助我们编写易于维护、易于理解和可重用的代码。

低耦合(Low Coupling)

低耦合指的是在一个类、模块或组件中,其各个元素之间的依赖关系较弱,彼此之间的影响较小。简而言之,低耦合表示一个类或模块的各个元素之间相对独立,修改其中一个元素不会对其他元素造成太大的影响。
一个低耦合的模块或类应该具有以下特点:

  • 其各个元素之间的依赖关系较弱,彼此之间的影响较小;
  • 其各个元素之间的接口简单明了,易于理解和使用;
  • 其各个元素之间的依赖关系可以通过接口或抽象类来定义,从而降低耦合度;
  • 其各个元素之间的依赖关系可以通过依赖注入等技术来解耦。
  • 低耦合是面向对象编程的另一个基本原则,它可以帮助我们编写易于维护、易于扩展和可重用的代码。

总之,高内聚和低耦合是两个相互依存、互相支持的概念。只有同时遵循这两个原则,才能编写出高质量的、易于维护的面向对象代码。