Iterator 设计模式

本篇对应《图解设计模式》第一章。

我觉得 Iterator 设计模式是一个很成功的设计模式,对集合遍历问题进行了抽象。

具体到 Java 语言来讲,Iterator 设计模式是 Collections Framework 的一部分。


Iterator 接口

package java.util;

import java.util.function.Consumer;

/**
 * An iterator over a collection.  {@code Iterator} takes the place of
 * {@link Enumeration} in the Java Collections Framework.  Iterators
 * differ from enumerations in two ways:
 *
 * <ul>
 *      <li> Iterators allow the caller to remove elements from the
 *           underlying collection during the iteration with well-defined
 *           semantics.
 *      <li> Method names have been improved.
 * </ul>
 *
 * <p>This interface is a member of the
 * <a href="{@docRoot}/java.base/java/util/package-summary.html#CollectionsFramework">
 * Java Collections Framework</a>.
 *
 * @apiNote
 * An {@link Enumeration} can be converted into an {@code Iterator} by
 * using the {@link Enumeration#asIterator} method.
 *
 * @param <E> the type of elements returned by this iterator
 *
 * @author  Josh Bloch
 * @see Collection
 * @see ListIterator
 * @see Iterable
 * @since 1.2
 */
public interface Iterator<E> {
    /**
     * Returns {@code true} if the iteration has more elements.
     * (In other words, returns {@code true} if {@link #next} would
     * return an element rather than throwing an exception.)
     *
     * @return {@code true} if the iteration has more elements
     */
    boolean hasNext();

    /**
     * Returns the next element in the iteration.
     *
     * @return the next element in the iteration
     * @throws NoSuchElementException if the iteration has no more elements
     */
    E next();

    /**
     * Removes from the underlying collection the last element returned
     * by this iterator (optional operation).  This method can be called
     * only once per call to {@link #next}.
     * <p>
     * The behavior of an iterator is unspecified if the underlying collection
     * is modified while the iteration is in progress in any way other than by
     * calling this method, unless an overriding class has specified a
     * concurrent modification policy.
     * <p>
     * The behavior of an iterator is unspecified if this method is called
     * after a call to the {@link #forEachRemaining forEachRemaining} method.
     *
     * @implSpec
     * The default implementation throws an instance of
     * {@link UnsupportedOperationException} and performs no other action.
     *
     * @throws UnsupportedOperationException if the {@code remove}
     *         operation is not supported by this iterator
     *
     * @throws IllegalStateException if the {@code next} method has not
     *         yet been called, or the {@code remove} method has already
     *         been called after the last call to the {@code next}
     *         method
     */
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    /**
     * Performs the given action for each remaining element until all elements
     * have been processed or the action throws an exception.  Actions are
     * performed in the order of iteration, if that order is specified.
     * Exceptions thrown by the action are relayed to the caller.
     * <p>
     * The behavior of an iterator is unspecified if the action modifies the
     * collection in any way (even by calling the {@link #remove remove} method
     * or other mutator methods of {@code Iterator} subtypes),
     * unless an overriding class has specified a concurrent modification policy.
     * <p>
     * Subsequent behavior of an iterator is unspecified if the action throws an
     * exception.
     *
     * @implSpec
     * <p>The default implementation behaves as if:
     * <pre>{@code
     *     while (hasNext())
     *         action.accept(next());
     * }</pre>
     *
     * @param action The action to be performed for each element
     * @throws NullPointerException if the specified action is null
     * @since 1.8
     */
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

Iterable 接口

package java.lang;

import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;

/**
 * Implementing this interface allows an object to be the target of the enhanced
 * {@code for} statement (sometimes called the "for-each loop" statement).
 *
 * @param <T> the type of elements returned by the iterator
 *
 * @since 1.5
 * @jls 14.14.2 The enhanced {@code for} statement
 */
public interface Iterable<T> {
    /**
     * Returns an iterator over elements of type {@code T}.
     *
     * @return an Iterator.
     */
    Iterator<T> iterator();

    /**
     * Performs the given action for each element of the {@code Iterable}
     * until all elements have been processed or the action throws an
     * exception.  Actions are performed in the order of iteration, if that
     * order is specified.  Exceptions thrown by the action are relayed to the
     * caller.
     * <p>
     * The behavior of this method is unspecified if the action performs
     * side-effects that modify the underlying source of elements, unless an
     * overriding class has specified a concurrent modification policy.
     *
     * @implSpec
     * <p>The default implementation behaves as if:
     * <pre>{@code
     *     for (T t : this)
     *         action.accept(t);
     * }</pre>
     *
     * @param action The action to be performed for each element
     * @throws NullPointerException if the specified action is null
     * @since 1.8
     */
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

    /**
     * Creates a {@link Spliterator} over the elements described by this
     * {@code Iterable}.
     *
     * @implSpec
     * The default implementation creates an
     * <em><a href="../util/Spliterator.html#binding">early-binding</a></em>
     * spliterator from the iterable's {@code Iterator}.  The spliterator
     * inherits the <em>fail-fast</em> properties of the iterable's iterator.
     *
     * @implNote
     * The default implementation should usually be overridden.  The
     * spliterator returned by the default implementation has poor splitting
     * capabilities, is unsized, and does not report any spliterator
     * characteristics. Implementing classes can nearly always provide a
     * better implementation.
     *
     * @return a {@code Spliterator} over the elements described by this
     * {@code Iterable}.
     * @since 1.8
     */
    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

分别使用几种不同的方法对集合进行遍历(都是基于 Iterator 设计模式)。

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

public class Main
{
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Hello", "World", "GoodBye");

        whileLoop(list);

        forLoop(list);

        forInLoop(list);

        forEachLoop(list);
    }

    // while loop, using iterator design pattern
    public static void whileLoop(Iterable iterable) {
        Iterator it = iterable.iterator();
        while(it.hasNext()) {
            System.out.println(it.next());
        }
    }

    // for loop, using iterator design pattern
    public static void forLoop(Iterable iterable) {
        for(Iterator it = iterable.iterator(); it.hasNext(); ) {
            System.out.println(it.next());
        }
    }

    // for/in, based on iterator design pattern
    public static void forInLoop(Iterable iterable) {
        for(Object object : iterable) {
            System.out.println(object);
        }
    }

    // forEach, based on for/in and functional programming
    public static void forEachLoop(Iterable iterable) {
        iterable.forEach(x -> System.out.println(x));
    }
}

参考:

https://book.douban.com/subject/26933281/

https://www.runoob.com/java/collection-iterator.html

https://www.ibm.com/developerworks/cn/java/j-forin.html

https://docs.oracle.com/javase/tutorial/collections/intro/index.html

https://github.com/tanghengzhi/java-design-pattern/tree/master/src/Iterator

364 total views, 1 views today

Depth-First Search vs Breadth-First Search

Graph

在计算机科学中, 图(graph) 是一种抽象数据类型, 旨在实现数学中的无向图和有向图概念,特别是图论领域。

一个图数据结构是一个(由有限个或者可变数量的)顶点/节点/点和边构成的有限集。

如果顶点对之间是无序的,称为无序图,否则称为有序图;

如果顶点对之间的边是没有方向的,称为无向图,否则称为有向图;

如果顶点对之间的边是有权重的,该图可称为加权图。

Depth-First Search (DFS)

Depth-first search (DFS) is an algorithm for traversing or searching tree or graph data structures. One starts at the root (selecting some arbitrary node as the root in the case of a graph) and explores as far as possible along each branch before backtracking.

Breadth-First Search (BFS)

Breadth-first search (BFS) is an algorithm for traversing or searching tree or graph data structures. It starts at the tree root (or some arbitrary node of a graph, sometimes referred to as a ‘search key’) and explores the neighbor nodes first, before moving to the next level neighbors.

参考:


https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/graph

https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/depth-first-search

https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/breadth-first-search

172 total views, 1 views today

Active Record vs Data Mapper

最近一直在用 Spring Boot 开发,项目中用到的 ORM 框架有:Spring Data JPA,Spring Data MongoDB,Mybatis,MyBatis Plus。

对照 Martin Fowler 的 Catalog of Patterns of Enterprise Application Architecture,我们分别列出 Spring Data,Mybatis,Mybatis Plus 使用的设计模式。

ORM FrameworkDesign Pattern
Spring DataRepository
MybatisData Mapper
Mybatis PlusData Mapper & Active Record

作为对比,我们看一下常用的 PHP ORM 框架使用的设计模式。

ORM Framework Design Pattern
Yii Active RecordActive Record
Laravel EloquentActive Record
Symfony Doctrine Data Mapper

可以看出 Java ORM 框架更喜欢 Data Mapper 设计模式,而 PHP ORM 框架更喜欢 Active Record 模式。

再看一下其他语言的 ORM 框架:

ORM Framework Design Pattern
Ruby on RailsActive Record
DjangoActive Record
Mongoose Active Record

不管是 Ruby on Rails(Ruby), 还是 Django(Python),或者 Mongoose(Node.js),都偏爱 Active Record 模式。


Active Record vs Data Mapper

作为两种非常常见的设计模式,让我们稍微花点时间了解一下 Active Record 和 Data Mapper 的区别。

首先我们还是参考 Martin Fowler 的 Catalog of Patterns of Enterprise Application Architecture 对比一下两种设计模式。

Active Record

Data Mapper

从图上可以看出来,两种设计模式最直观的区别是:Active Record 模式只定义了一个 Person 类,Data Mapper 模式需要定义 Person 类和 PersonMapper 类。

然后我们来对比一下两种模式代码上的区别:

Active Record

Person person = new Person();
person.firstName = "firstName";
person.lastName = "lastName";
person.insert();

Data Mapper

Person person = new Person();
person.firstName = "firstName";
person.lastName = "lastName";
personMapper.insert(person);

我个人更喜欢 Active Record 模式,不知道你们喜欢那种设计模式呢?


参考:

https://www.martinfowler.com/eaaCatalog/index.html

https://www.martinfowler.com/eaaCatalog/dataMapper.html

https://www.martinfowler.com/eaaCatalog/activeRecord.html

https://spring.io/projects/spring-data-jpa

http://www.mybatis.org/mybatis-3/

https://mybatis.plus/

https://www.yiiframework.com/doc/guide/2.0/en/db-active-record

https://laravel.com/docs/5.8/eloquent

https://symfony.com/doc/current/doctrine.html

https://guides.rubyonrails.org/active_record_basics.html

https://docs.djangoproject.com/en/2.2/topics/db/models/

https://mongoosejs.com/

314 total views, no views today

Try Windows Terminal & WSL 2

心血来潮,尝试一下微软新出的 Microsoft Terminal 和 WSL 2。


Windows Terminal

原本准备写一篇如何编译安装 Windows Terminal 的教程,结果下载了 VS 2019 搞了一晚上,第二天早上一醒来就看到 Windows Terminal 在 Microsoft Store 上线了。

啥也不说了,需要的直接去下载吧。

Windows Terminal in Microsoft Store
Windows Terminal Preview, support Windows PowerShell, cmd, wsl

WSL 2

体验完 Windows Terminal 之后,我们再来尝试一下 Windows Insiders 更新的 WSL 2。

首先需要加入 Windows 预览体验计划,选择快速通道,把系统更新到 Windows 10 build 18917 或者更高版本。

然后以管理员身份启动 Windows PowerShell 并输入以下命令。

Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform

完成之后需要重启电脑。

查看一下当前 WSL 版本。

PS C:\Users\tangh> wsl -l -v                                                                                              NAME            STATE           VERSION                                                                               * Ubuntu-18.04    Stopped         1                                                                                                                                                                                                 

把当前安装的 Ubuntu 18.04 的 WSL 1 版本转换成 WSL 2 版本。

PS C:\Users\tangh> wsl --set-version Ubuntu-18.04 2                                                                     正在进行转换,这可能需要几分钟时间...                                                                                   有关与 WSL 2 的主要区别的信息,请访问 https://aka.ms/wsl2                                                               转换完成。  

再查看一下当前 WSL 版本。

PS C:\Users\tangh> wsl -l -v                                                                                              NAME            STATE           VERSION                                                                               * Ubuntu-18.04    Stopped         2 

最后我们来尝试一下在 WSL 2 上安装和启动 Docker 容器。

从 Windows Terminal 进入 WSL 2,然后输入以下命令。

sudo apt update
sudo apt install docker.io
sudo service docker start
* Docker is running

参考:

https://github.com/Microsoft/Terminal

486 total views, 2 views today

Spring Boot Validation

验证表单输入(接口参数)是Web 开发 (API 接口开发)中不可缺少的一个部分。


@NotBlank Validation

首先我们从最简单的不能为空判断开始,介绍 Spring Boot Validation。

假设我们需要提供一个登陆接口,输入用户名密码,返回是否登陆成功。

在去数据库查询用户名密码是否正确之前,我们可以做一些简单的验证。

在 LoginRequest 的 username 和 password 参数上加上 @NotBlank。

package com.example.validation.request;

import javax.validation.constraints.NotBlank;

public class LoginRequest {
    @NotBlank(message = "用户名不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    private String password;

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

在 UserController::login() 方法接收输入的 loginRequest 前面加上 @Valid 。

package com.example.validation.controllers;

import com.example.validation.request.LoginRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
@RequestMapping("/user")
public class UserController {
    @PostMapping("/login")
    public LoginRequest login(@RequestBody @Valid LoginRequest loginRequest) {
        return loginRequest;
    }
}

这样当用户输入的用户名或则密码为空的时候,就不会继续进入到下面的业务逻辑,在验证参数的阶段就可以直接返回 400 错误。

Bean Validation

使用 @NotBlank 完成第一个参数验证的例子的时候,会有这样一个问题:@NotBlank 注解是不是 Spring 框架自带的,这样的注解一共有多少个。

这个时候我们就需要了解一个 Bean Validation。

Bean Validation: JSR 303 – JSR 349 – JSR 380

Bean Validation 是一套通过注解来表示对象模型的约束的 Java 规范,最新版本是 Bean Validation 2.0。

定义的内置约束注解有:

Build-in Constraint definitions

Hibernate Validator 是 Bean Validation 规范的实现,除了Bean Validation 定义的 22 种内置约束注解之外,还实现了另外 25 种内置约束注解。

Build-in Constraint definitions

Creating custom constraints

虽然 Hibernate Validator 已经内置了这么多约束注解,但是需求千变万化,总有不能满足的时候,这时候我们就需要定义并实现自己的一套约束注解。

假设我们需要提供一个注册接口,输入用户名、手机号码、邮箱、密码,返回是否注册成功。

除了需要验证用户名密码是否为空之外,还需要验证手机号码和邮箱至少要有一个是必填的。

MinimumRequiredParameters.java

package com.example.validation;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = MinimumRequiredParametersValidator.class)
@Documented
public @interface MinimumRequiredParameters {

    String message() default "{minimum.required.parameters.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String[] value();

    int minimum() default 1;
}

MinimumRequiredParametersValidator.java

package com.example.validation;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class MinimumRequiredParametersValidator implements ConstraintValidator<MinimumRequiredParameters, Object> {

    private String[] fields;

    private int minimum;

    private int count;

    @Override
    public void initialize(MinimumRequiredParameters constraintAnnotation) {
        this.fields = constraintAnnotation.value();
        this.minimum = constraintAnnotation.minimum();
        this.count = 0;
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(value);

        for (String field: fields) {
            if (wrapper.getPropertyValue(field) != null) { count++; }
        }

        return count >= minimum;
    }
}

resources/ValidationMessages.properties

minimum.required.parameters.message=at least {minimum} of {value} is required

在 RegisterRequest 中使用 @MinimumRequiredParameters 注解判断手机号码和邮箱至少有一个必填。

package com.example.validation.request;

import com.example.validation.MinimumRequiredParameters;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

@MinimumRequiredParameters(value = {"phone", "email"}, message = "手机号码和邮箱至少有一个必填")
public class RegisterRequest {
    @NotBlank(message = "用户名不能为空")
    private String username;

    @Pattern(regexp = "\\d{11}", message = "手机号码格式不正确")
    private String phone;

    @Email(message = "邮箱格式不正确")
    private String email;

    @NotBlank(message = "密码不能为空")
    private String password;

    public String getUsername() {
        return username;
    }

    public String getPhone() {
        return phone;
    }

    public String getEmail() {
        return email;
    }

    public String getPassword() {
        return password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

参考:

https://spring.io/guides/gs/validating-form-input/

https://beanvalidation.org

https://beanvalidation.org/2.0/spec/#builtinconstraints

http://hibernate.org/validator/

https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-customconstraints

https://stackoverflow.com/questions/9284450/jsr-303-validation-if-one-field-equals-something-then-these-other-fields-sho

222 total views, no views today

download.js

fetch('http://api-alpha.heywoof.com/order/waybills/v2/shipping-report', {
    method: "POST",
    headers: {
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        "header": {"authToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NjYyMDA1NjQsInVzZXJJZCI6IjI4ODk4ODM3MDM1OTU0OTk1MiIsImNoYW5uZWxJZCI6MTF9.QE24ZiwMn3NbpH6M6g_6WV2WLQcDdohnaYcJyuJ6_nI"},
        "data": {}
    })
}).then(function(response) {
    return response.blob();
}).then(function(blob) {
    let url = window.URL.createObjectURL(blob);

    let a = document.createElement("a");
    document.body.appendChild(a);
    a.style = "display: none";
    a.href = url;
    a.download = "发货报表.xlsx";
    a.click();
    window.URL.revokeObjectURL(url);
});

参考:

https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

6,812 total views, no views today