netty-一行代码引起的思考

本文探讨了对象引用、主动伴生、方法引用及层级引用的概念,详细解析了如何通过这些技术构建业务逻辑,包括资源自处理、多处理器处理和顺序处理等,最终实现了业务规则的灵活组合与高效维护。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0. 开始的地方

Handler(Selector sel, SocketChannel c) throws IOException {
    ...
    sk.attach(this);
    ...
}

new Handler(selector, c);

Scalable IO in Java中看到这代码,总让我感觉有点骚东西。

前面的记录总简单的抒发了一下,现在让我天马行空的思维发散一回。

1. 对象引用问题

gc回收什么垃圾?引用计数为0的对象就是垃圾了。

到底什么是引用呢,怎么算是引用计数为0呢。

什么情况下,我们在代码里面再也找不到一个对象了呢。

1.1 变量名引用

    public static void main(String[] args) {
        Student student = new Student("godme");
        Student godme = student;
        student = new Student("judas");
        student = godme;
    }

仔细一看,哪个Student被丢失了呢。

最后的结果就是,Studnet(godme)被引用了两次,而Student(judas)引用为0

这个时候,通过student或者godme,我都能找到Student(godme)

但是Student(judas)在代码级别,我永远的失去了它的身影。

这种就是要被回收的垃圾,因为它失活了。

1.2 容器引用

    public static void main(String[] args) {
        ArrayList<Student> students = new ArrayList<>();
        for(int id = 1; id < 5; id++){
            students.add(new Student("student" + id));
        }
    }

在没有变量名引用的情况下,使用容器也可以保持对象的活性,防止被回收。

只要还有活性的对象,我们始终能找到它。

如果我们pop之后没有引用继续指向实体,那么,它必然失活。

1.3 其他方式

public class Node {
    private Student value;
    private Node next;
    private Node prev;
}
    public static void main(String[] args) {
        Node head = new Node(null);
        Node cursor = head;
        for(int id = 1; id < 5; id++){
            cursor.setNext(new Node(new Student("student" + id)));
        }
    }

利用链表,或者其他的结构,保持引用,同样能够保证活性

这种方式同质于容器引用,其中关键的地方,是活性的传递性。

通过一个活性的对象,引用到一个对象,这个被引用的对象必然也能够保持活性,也就是能被找到实体。

可以称之为伴生,如果前面的对象失活,伴生的对象们必定失活。

在对象管理的时候,依次用变量名保持活性,快捷但不方便,容易混乱,不易管理。

使用容器,容易管理,但是针对性不强。

如何引用一个对象,并保持其活性,这个就因地制宜了。

2.引用关系的建立

把基本对象排除,我们的任何操作中使用的元素都是对象。

程序的重点就是如何操作这些对象,因此,更好管理这些对象就能让我们更好的组织代码结构。

创建和回收先不论,关键是我们如何找到这些对象,也就是如何去引用,保证活性。

然后去找到实体,开展我们的具体操作。

2.1 被动伴生

    public static void main(String[] args) {
        Student student = new Student("godme");
        ArrayList<Student> students = new ArrayList<>();
        students.add(student);
    }

这是我们最常见也最常用的管理模式。

不论是让实体关联变量名

    Student student = new Student("godme");
    ArrayList<Student> students = new ArrayList<>();

还是把对象添加到容器

    students.add(student);

都是操作对象,去被引用

2.1.1 面向对象扩展

话说回来,我们使用面向对象,但面向对象到底是什么呢?

归根结底,是被动主动的差别。

# 面向过程
sayHello(student);
# 面向对象
student.sayHello();

面向过程中的函数,就好比是工具箱,专门操作信息结构体,信息都是被处理

面向对象中,属性都是主动调用方法进行自处理,是一种主动性的操作。

但是在我们面向对象编程当中,却有太多的过程性被动操作,主动性够不充足。

更多的主动性,是对于程序员而言的主动性,没有贯彻对象的主动性

2.2 主动伴生

class Peron{
    public Person(Family family, name){
        this.name = name;
        family.add(this);
    }
    public static void main(String[] args){
        Family family = new Family();
        new Person(family, "godme")
    }
}

贯彻一下面向对象的主动性,当创建对象时就能够完成对象的自我收归管理了。

对于固定管理流程,我们没必要去外围的去管理他们,可以让他们自我进行管理。

类比于离职,现在都是员工自己收集资料,到处去盖章,然后完成流程。

如果整个流程,都需要外围的去管理,不同的员工,不同的部门,不同的印章,不同的流程,想想都头大。

但是如果让对象去主动,流程就是可复制,对于每个对象而言,更明确,也更便捷。

所以,自主缴费,自主申请之类的主动性操作,越来越多,也越来越容易处理。

2.3 循环引用

class Node{
    Node parent;
    Node child;
    String value;
    public void addChild(Node child){
        this.child = child;
        child.parent = this;
    }
    public void setParent(Node parent){
        this.parent = parent;
        parent.child = this;
    }
}

用链表来说,再明确不过了。

在纯粹的对象管理里面,变量名是多余的废物操作。

在这种纯粹的关系管理,也就是引用传递,或者说伴生族群里面,对象单个依附总比容器挨个管理来的方便快捷。

同时,双向引用这种模式里面,主动性更能够看出效率。

双向链表的互联操作,如果是外部进行关联,一个足够长的链表,就必须重复的进行关联操作。

但是收归个体主动操作,我们却只关心顺序,而不用考虑关系的建立过程。

不过,此小节的关注点,是对象的双向引用,即两个对象的相互伴生

前面的伴生,就像单向链表,只能够单方面的查找。

而双向链表中,可以你来我往,找到其中一个,我们就能找到另一个。

class Person{
    public void girlFriend(Person person){
        this.girlFriend = person;
        person.boyFriend = this;
    }
    public void boyFriend(Person person){
        this.boyFriend = person;
        person.girlFriend = this;
    }
}

这是两个对象之间一种相互确立的伴生关系。

2.4 方法引用

如果是javascript或者python这种语言的话,方法也是一种对象,就能够更好的讲述了。

就可以如此进行使用

handle.handler(handle)

只是,其中更深层的含义,是方法的可替换性

handle.handler = handler

java当中,方法是不可替换的,我们只能借由包含方法的对象替换,来完成方法的替换。

也就是利用接口,来完成一种类似的函数式编程。

2.4.1 接口实现和接口属性

利用接口的话,一般存在两种形式:实现接口接口属性

研究两者的区别,可以让我们更好的利用接口,和进行接下来的讲述。

接口实现

class Person implements Hello{
    public void hello(Person person){
        doSomething;
    }
}

可以说,有一点经验的都能够发现问题:实现接口后方法就固定了

尤其是对于一个类而言,只要是这个类的该方法,已经注定是一致的。

不仅不可替换,而且单一类的所有对象都将统一行为。

接口属性

class Person{
    private Hello hello;
    public void setHello(Hello hello){
        this.hello = hello;
    }
    public void hello(){
        this.hello.hello(this);
    }
}

把接口作为属性,我们就可以自定义的替换该属性。

在替换过程中,我们就可以任意更换具体的操作方法,从而达到同样的方法不同的操作。

对于动作的更改,也就是方法的替换,存在三种方式。

  1. 子类重写
  2. 再次封装
  3. 接口属性替换

对于前面的两种而言,都会派生出新的类别,而且,新类别中的方法依然存在不可替换的性质。

如果想要同一动作的具体内容得到更改,尤其是任意对象都可更改,接口属性更改方案更实用。

3. 资源自处理

总结一下前面的内容

  1. 引用关系建立
  2. 主动性关系建立
  3. 主动方法对象关联

随后,引申出来的也是我们的终极目标:资源的自处理

3.1 一次性处理

资源

public class Ticket {
    private  AtomicInteger total = new AtomicInteger(100);
    private boolean ready = false;
    public  int count(){
        return total.get();
    }
    public void decrease(){
        this.total.decrementAndGet();
    }
    public void increase(){
        this.total.incrementAndGet();
    }
    public boolean hasMore(){
        return total.get() > 0;
    }
    public boolean isReady(){
        return this.ready;
    }
    public void ready(){
        this.ready = true;
    }
}

处理器

public class Shop extends Thread{
    private Ticket ticket;
    public Shop(Ticket ticket){
        this.ticket = ticket;
        this.start();
    }
    private void sale(){
        while(ticket.hasMore()){
            ticket.decrease();
            System.out.println(Thread.currentThread().getName() + " ticketCount : " + ticket.count());
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    @Override
    public void run() {
        while(!ticket.isReady()){
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        sale();
    }
}

处理

    public static void main(String[] args) {
        Ticket ticket= new Ticket();
        new Shop(ticket);
        new Shop(tiket);
        ticket.ready();
    }

这一下,是不是更清楚所谓主动性了呢。

如果商店本来就注定了卖票,我们只需要让商店自动进货就行了

new Shop(ticket);

贯彻面向对象,充分发挥了对象的主动性,只需要让商店自动拿票就行。

是在没必要知道哪一个商店,然后把票交给它。

3.2 多处理器处理

实际上,前面的例子就是存在多个消费者的例子了,为了省力气,也就一次性完工了。

想要单个例子的,去掉一个new Shop(ticket)就行了。

这里想要说明两点

  1. 本来就注定关联的,主动关联可以更简单的管理对象和组织代码
  2. 多个处理器,做好安全措施,这种添加多个处理器进行护理更加快捷

3.3 顺序处理

3.3.1 异步顺序

一般业务来说,不同的处理逻辑之间,是有顺序关系的。

假设票买完了,需要重新印票。

public class Provider extends Thread {
    private Ticket ticket;
    public Provider(Ticket ticket){
        this.ticket = ticket;
        this.start();
    }
    private void publish(){
       for(int count = 1; count <= 20; count++){
           ticket.increase();
           System.out.println(Thread.currentThread().getName() + " provider : " + ticket.count());
       }
        this.ticket.ready();
    }
    @Override
    public void run() {
        while(true){
            while(ticket.isReady()){
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            publish();
        }
    }
}

当然了,前面的Shop消费完就需要ticket.disable,因为有信号才能够进行控制。

    public void disable(){
        this.ready = false;
    }

不过,这不是我想讲述的重点。

用线程来讲述的话将引入两个话题

  1. 动态
  2. 异步

在这个例子中,涉及到了动态的资源处理,遮掩了我想讲述的主题。

线程一般处理耗操作,顺序控制的话一般会使用Future或者其他对象进行异步控制,同样遮掩了基本的讲述。

这两个方面,算作是此篇内容的进阶,后续慢慢结合加深。

让我们更加深入理解关于引用诞生的更多操纵手段。

3.3.1 同步顺序

3.3.1.1 方法替换

回顾前面的方法引用,也即是针对不同对象可更换不同操作,在讲述同步顺序之前,我们深入的体会一下此效果。

资源

public class Data {
    public Executor executor;
    public Integer value;
    public Data(Integer value){
        this.value = value;
    }
    public Integer calc() {
        return executor.exec(this);
    }
}

接口

public interface Executor {
    Integer exec(Data data);
}

几个实现

public class PlusFive implements Executor {
    @Override
    public Integer exec(Data data) {
        return data.value + 5;
    }
}
public class DivTwo implements Executor {
    @Override
    public Integer exec(Data data) {
        return data.value / 2;
    }
}
public class MultTen implements Executor {
    @Override
    public Integer exec(Data data) {
        return data.value * 10;
    }
}

操作

    public static void main(String[] args) {
        Data data = new Data(3);
        data.executor = new PlusFive();
        System.out.println(data.calc());
        data.executor = new DivTwo();
        System.out.println(data.calc());
        data.executor = new MultTen();
        System.out.println(data.calc());
    }

这里只是为了演示接口属性的可替换性,后续将不再提及。

就此类操作而言,可以利用主动伴生进行代码简化。

public abstract  class ExecutorAdapter implements Executor{
    public ExecutorAdapter(Data data){
        data.executor = this;
    }
}

把其他几个接口直接继承此适配器类。

public class PlusFive implements Executor;
public class DivTwo implements Executor;
public class MultTen implements Executor;
    public static void main(String[] args) {
        Data data = new Data(3);
        new PlusFive(data);
        System.out.println(data.calc());
        new DivTwo(data);
        System.out.println(data.calc());
        new MultTen(data);
        System.out.println(data.calc());
    }

只是需要记得super完成主动伴生

经过如此操作,不难得出如此结论: 接口属性直接伴生操作对象,不具有逻辑组合操作。

这样的直接单层次伴生,能够直接操作对象,却不能够一次处理,最终组装成业务逻辑。

3.3.1.2 顺序管理

我们把原来的操作元素Data中的executor改成容器类。

public class Data {
    public ArrayList<Executor> executors = new ArrayList<>();
    public Integer value;
    public Data(Integer value){
        this.value = value;
    }
    public void calc() {
        Iterator<Executor> it = executors.iterator();
        while(it.hasNext()){
            it.next().exec(this);
        }
    }
}

这样,就完成了逻辑顺序的组装。

伴生的时候,伴生于executors,自动创建关联关系。

public abstract  class ExecutorAdapter implements Executor{
    public ExecutorAdapter(Data data){
        data.executors.add(this);
    }
}

还需要注意一点,也是最终要的点,执行结果必须写回Data,因为它始终贯穿整个流程。

data.value /= 2;
data.value *= 10;
data.value += 5;

操作

    public static void main(String[] args) {
        Data data = new Data(3);
        new PlusFive(data);
        new DivTwo(data);
        new MultTen(data);
        data.calc();
        System.out.println(data.value);
    }

Context

到这里,不知道大家发现了最核心的转变了没有,那就是上下文Context

new Shop(ticket)类型的伴生,让我们发现了伴生,也就是引用传递的效果。

增加伴生的层级,我们完全可以一只根须扯出一座山

然后将这只根须,伴生在一个对象上面,就能够让这个对象贯穿整个的流程。

也就是所谓的context

上面的例子,存在几个缺点

  1. 直接伴生数据,数据未分离
  2. 单链表,有下文没上文

如果我们采用了双向链表,上下文就可以真正的实现。

分离了数据,就可以做到context的流程完备性,完全脱离数据依赖,单纯的流程定制。

而且,在一般的业务处理当中,逻辑都是固定的,只需要这么一个规则树,就可以处理更多的数据。

3.3.1.3 上下文管理

Context

public class Context {
    public ArrayList<Executor> executors = new ArrayList<>();
    public ArrayList<Data> datas = new ArrayList<>();
    public void deal(){
        Iterator<Data> dataIt = datas.iterator();
        Iterator<Executor> executorIt;
        Data data;
        while (dataIt.hasNext()){
            data = dataIt.next();
            executorIt = executors.iterator();
            while (executorIt.hasNext()){
                executorIt.next().exec(data);
            }
        }
    }
}

Data

public class Data {
    public Integer value;
    public Data(Context context, Integer value){
        this.value = value;
        context.datas.add(this);
    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

main

public class OrderMain {
    public static void main(String[] args) {
        Context context = new Context();
        new PlusFive(context);
        new DivTwo(context);
        new MultTen(context);
        new Data(context, 3);
        new Data(context, 5);
        context.deal();
        System.out.println(context.datas.toString());
    }
}

动作的绑定切换到context

这个实现中,有几点问题

  1. 有下文无上文
  2. 上下文和规则树形象重叠

具体怎么做就自己发挥了,暂时就这样了。

说过的循环绑定,之所以没有用上,这个问题值得思考。

因为,上下文贯穿了整个流程,所有的修改都必须写回上下文中或伴生的对象当中去。

这样才能够经历全部的流程操作。

如果方法对象直接绑定了操作元素,必须保证如下几点

  1. 严格控制方法执行顺序
  2. 做严格数据检查
  3. 结果回写对象

防止对象更换和结果丢失。

但是弊端更明显,那就是造成方法对象多实例,并且丢失动态数据功能。

如果把一切都放在context中,生命周期更长,可以随时往context中增加或修改新的变量。

更利于调整后续操作分支和控制。

3.4 分支跳转

前面的context还存在一个弱点,那就是分支跳转,如果只能够单向执行,也就没什么意思了。

容器节点

public abstract  class Node implements NodeExecutor {
    public HashMap<Event,Node> nodes = new HashMap<>();

    public Node nextNode(Event event){
        if (nodes.size() <= 0){
            return null;
        }
        if(nodes.size() == 1){
            return nodes.values().iterator().next();
        }
        if(nodes.containsKey(event)){
            return nodes.get(event);
        }
        return null;
    }
    public void addNode(Event event, Node node){
        nodes.put(event, node);
    }
    public boolean hasNext(){
        return this.nodes.size() > 0;
    }
}

处理器

public interface NodeExecutor {
    Event exec(Data data);
}

NodeContext

public class NodeContext {
    private Node tree;
    public NodeContext(Node node){
        this.tree = node;
    }
    public ArrayList<Data> datas = new ArrayList<>();
    public void addData(){

    }
    public void deal(){
        Iterator<Data> dataIt = datas.iterator();
        Data data;
        while(dataIt.hasNext()){
            data = dataIt.next();
            Node cursor = tree;
            while(cursor != null){
                Event event = cursor.exec(data);
                cursor = cursor.nextNode(event);
            }
        }
    }
}

Event

public enum Event {
    MORETHREE,
    LESSTHREE,
    MINUS,
    PLUS,
    MORETEN,
    LESSTEN;
}

最终实现如下的固定结构

Created with Raphaël 2.2.0 data data < 0 ? data = -data data > 3? data > 10 ? data /= 3 print data += 2 data *= 8 yes no yes no yes no

组合逻辑如此

main

    public static void main(String[] args) {
        Node printNode = new PrintNode();
        Node divThreeNode = new DivThreeNode();
        Node plusTwoNode = new PlusTwoNode();
        Node mulEightNode = new MulEightNode();
        divThreeNode.addNode(null, printNode);
        plusTwoNode.addNode(null, printNode);
        mulEightNode.addNode(null, printNode);
        Node moreTenNode = new MoreTenNode();
        moreTenNode.addNode(Event.MORETEN, divThreeNode);
        moreTenNode.addNode(Event.LESSTEN, plusTwoNode);
        Node moreThreeNode = new MoreThreeNode();
        moreThreeNode.addNode(Event.MORETHREE, moreTenNode);
        moreThreeNode.addNode(Event.LESSTHREE, mulEightNode);
        Node absNode = new AbsNode();
        Node transnode = new TransNode();
        absNode.addNode(Event.MINUS, transnode);
        absNode.addNode(Event.PLUS, moreThreeNode);
        ///////////////////////////////////////////
        NodeContext context = new NodeContext(absNode);
        new Data(context, 4);
        context.deal();
    }

当利用分支节点组装好逻辑业务,后续的业务处理就只是单纯的数据添加。

不用再费心费力的区各种判断,而且业务一旦变更,在对应的节点上面进行局部的修改,整体逻辑不变。

费力只在业务逻辑的节点关系组装,后续的维护和再开发,都十分的简单。

4. 小结

  1. 简单引用
  2. 主动引用
  3. 方法引用
  4. 层级引用
  5. 分支跳转

从简单的一级引用开始发散,引申到一个伴生群

把握单一入口,就可以构建一个贯穿始终的对象,也即是上下文Context

通过层级的引用关系,搭建方法的流程操作逻辑,就可以组装成业务树,宏观上对业务进行分支描述。

让开发更加易于维护和扩展。

这种业务规则的描述,如今普遍的drools等规则引擎,做的不外如此。

通过这次的放飞思想,我有如下收获

  1. 隐晦引用(主动伴生)
  2. 整体逻辑组合描述(方法引用逻辑)
  3. 上下文Context的贯穿方法(贯穿业务树)

当然,这些东西不用这种方式也可以进行编写,但是对应的有如下缺点

  1. 重复引用
  2. 业务不清晰
  3. Context零散注入

尤其你可以看见最后的代码,正是因为Node没有使用伴生的方式,一级一级的注入,太多无用引用。

但是也可以明确的去抽象还原业务逻辑。

抛开业务组装,后续的操作,简直不要太简洁。

而且,在这种模式下的业务改动,仍然可以快速的锁定修改点,快速的添加节点补充业务。

只能说,世上无小事,什么都能玩出花样。

简单的Person a = new Person(),背后也蕴含了无限的可能。

垃圾代码在此

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值