Java関数の優雅な道(上)

44641 ワード

リード


ソフトウェアプロジェクトコードの蓄積に伴い、システムのメンテナンスコストがますます高くなり、すべてのソフトウェアチームが直面している共通の問題です.コードを持続的に最適化し、コードの品質を高めることは、システムの生命力を向上させる有効な手段の一つである.ソフトウェアシステム思考には「Less coding,more thinking(符号化・多思考)」という言葉があり、「Think more,code less(思考が多ければ多いほど符号化が少ない)」というスラングもある.だから、私たちは符号化の中で多く考えて、自分の符号化レベルを高めるように努力してこそ、より優雅で、より質が高く、より効率的なコードを書くことができます.
本文はJava関数に関連する符号化規則をまとめ、多くのJavaプログラマーにいくつかの符号化提案を提供し、より優雅で、より質が高く、より効率的なコードを作成するのに役立つことを目的としています.このコード規則は、高徳採集部門での実践を通じて、すでに良い効果を得た.

汎用ツール関数の使用


ケース1


現象の説明:
不完全な書き方:
thisName != null && thisName.equals(name);

より完璧な書き方:
(thisName == name) || (thisName != null && thisName.equals(name));

推奨事項:
Objects.equals(name, thisName);

ケース2


現象の説明:
!(list == null || list.isEmpty());

推奨事項:
import org.apache.commons.collections4.CollectionUtils;
CollectionUtils.isNotEmpty(list);

主な収益

  • 関数式プログラミング、業務コードが減少し、論理が一目瞭然である.
  • 汎用ツール関数は、論理的考慮が周到で、問題が発生する確率が低い.

  • スーパー関数の分割


    1つの関数が80行を超えると、超大関数に属し、分割する必要があります.

    ケース1:コードブロックごとに1つの関数にカプセル化できます


    各コードブロックには、このコードブロックの機能を説明するためのコメントが必ずあります.
    コードブロックの前にコメントがある場合は、このコードを関数に置き換えることができ、コメントに基づいてこの関数に名前を付けることができます.関数に適切な名前がある場合は、内部コードがどのように実現されているかを見る必要はありません.
    現象の説明:
    //  
    public void liveDaily() {
        //  
        //  
    
        //  
        //  
    
        //  
        //  
    }

    推奨事項:
    //  
    public void liveDaily() {
        //  
        eat();
    
        //  
        code();
    
        //  
        sleep();
    }
    
    //  
    private void eat() {
        //  
    }
    
    //  
    private void code() {
        //  
    }
    
    //  
    private void sleep() {
        //  
    }

    ケース2:各サイクルは1つの関数にカプセル化できます


    現象の説明:
    //  
    public void live() {
        while (isAlive) {
            //  
            eat();
    
            //  
            code();
    
            //  
            sleep();
        }
    }

    推奨事項:
    //  
    public void live() {
        while (isAlive) {
            //  
            liveDaily();
        }
    }
    
    //  
    private void liveDaily() {
        //  
        eat();
    
        //  
        code();
    
        //  
        sleep();
    }

    ケース3:各条件体は1つの関数にカプセル化できます


    現象の説明:
    //  
    public void goOut() {
        //  
        //  :  
        if (isWeekday()) {
            //  
        }
        //  :  
        else {
            //  
        }
    }

    推奨事項:
    //  
    public void goOut() {
        //  
        //  :  
        if (isWeekday()) {
            play();
        }
        //  :  
        else {
            work();
        }
    }
    
    //  
    private void play() {
        //  
    }
    
    //  
    private void work() {
        //  
    }

    主な収益

  • 関数が短いほど精悍で、機能が単一になり、ライフサイクルが長いことが多い.
  • 関数が長ければ長いほど、理解とメンテナンスが容易ではなく、メンテナンススタッフは簡単に修正することができません.
  • は、長すぎる関数において、発見しにくい重複コードを含むことが多い.

  • 同じ関数内のコードブロックレベルができるだけ一致するようにする


    ケース1


    現象の説明:
    //  
    public void liveDaily() {
        //  
        eat();
    
        //  
        code();
    
        //  
        //  
    }

     
    明らかに、寝るというコードブロックはeat(食事)とcode(コード)と同じレベルではなく、突兀に見えます.コードを書くことを文章を書くことにたとえると、eat(食事)とcode(コード)は段落の大意であり、寝るというコードブロックは詳細な段落に属する.liveDaily(毎日の生活)という関数では,主な流れ(段落の大意)を書くだけでよい.
     
    推奨事項:
    public void liveDaily() {
        //  
        eat();
    
        //  
        code();
    
        //  
        sleep();
    }
    
    //  
    private void sleep() {
        //  
    }

    主な収益

  • 関数呼び出しは用途を示し、関数は表現論理を実現し、階層がはっきりして理解しやすい.
  • 階層のコードブロックを1つの関数に置かないと、コードヘッダが軽く感じられやすい.

  • 同じ機能コードを関数としてカプセル化


    ケース1:同じコードを関数としてカプセル化


    現象の説明:
    //  
    public void disableUser() {
        //  
        List userIdList = queryBlackUser();
        for (Long userId : userIdList) {
            User userUpdate = new User();
            userUpdate.setId(userId);
            userUpdate.setEnable(Boolean.FALSE);
            userDAO.update(userUpdate);
        }
    
        //  
        userIdList = queryExpiredUser();
        for (Long userId : userIdList) {
            User userUpdate = new User();
            userUpdate.setId(userId);
            userUpdate.setEnable(Boolean.FALSE);
            userDAO.update(userUpdate);
        }
    }

    推奨事項:
    //  
    public void disableUser() {
        //  
        List userIdList = queryBlackUser();
        for (Long userId : userIdList) {
            disableUser(userId);
        }
    
        //  
        userIdList = queryExpiredUser();
        for (Long userId : userIdList) {
            disableUser(userId);
        }
    }
    
    //  
    private void disableUser(Long userId) {
        User userUpdate = new User();
        userUpdate.setId(userId);
        userUpdate.setEnable(Boolean.FALSE);
        userDAO.update(userUpdate);
    }

    ケース2:類似コードを関数としてカプセル化


    パッケージ類似コードは関数であり,差異は関数パラメータによって制御される.
    現象の説明:
    //  
    public void adoptOrder(Long orderId) {
        Order orderUpdate = new Order();
        orderUpdate.setId(orderId);
        orderUpdate.setStatus(OrderStatus.ADOPTED);
        orderUpdate.setAuditTime(new Date());
        orderDAO.update(orderUpdate);
    }
    
    //  
    public void rejectOrder(Long orderId) {
        Order orderUpdate = new Order();
        orderUpdate.setId(orderId);
        orderUpdate.setStatus(OrderStatus.REJECTED);
        orderUpdate.setAuditTime(new Date());
        orderDAO.update(orderUpdate);
    }

    推奨事項:
    //  
    public void adoptOrder(Long orderId) {
        auditOrder(orderId, OrderStatus.ADOPTED);
    }
    
    //  
    public void rejectOrder(Long orderId) {
        auditOrder(orderId, OrderStatus.REJECTED);
    }
    
    //  
    private void auditOrder(Long orderId, OrderStatus orderStatus) {
        Order orderUpdate = new Order();
        orderUpdate.setId(orderId);
        orderUpdate.setStatus(orderStatus);
        orderUpdate.setAuditTime(new Date());
        orderDAO.update(orderUpdate);
    }

    主な収益

  • 共通関数をカプセル化し、コード行数を減らし、コード品質を高める.
  • は共通関数をカプセル化し、ビジネスコードをより精錬し、可読性とメンテナンス性を向上させる.

  • パッケージ取得パラメータ値関数


    ケース1


    現象の説明:
    //  
    public boolean isPassed(Long userId) {
        //  
        double thisPassThreshold = PASS_THRESHOLD;
        if (Objects.nonNull(passThreshold)) {
            thisPassThreshold = passThreshold;
        }
    
        //  
        double passRate = getPassRate(userId);
    
        //  
        return passRate >= thisPassThreshold;
    }

    推奨事項:
    //  
    public boolean isPassed(Long userId) {
        //  
        double thisPassThreshold = getPassThreshold();
    
        //  
        double passRate = getPassRate(userId);
    
        //  
        return passRate >= thisPassThreshold;
    }
    
    //  
    private double getPassThreshold() {
        if (Objects.nonNull(passThreshold)) {
            return passThreshold;
        }
        return PASS_THRESHOLD;
    }

    主な収益

  • 取得パラメータ値を業務関数から独立させ、業務論理をより明確にする.
  • パッケージの取得パラメータ値は独立関数であり、コードで繰り返し使用することができる.

  • インタフェースパラメータ化による同一論理のカプセル化


    ケース1


    現象の説明:
    //  
    public void sendAuditorSettleData() {
        List settleDataList = auditTaskDAO.statAuditorSettleData();
        for (WorkerSettleData settleData : settleDataList) {
            WorkerPushData pushData = new WorkerPushData();
            pushData.setId(settleData.getWorkerId());
            pushData.setType(WorkerPushDataType.AUDITOR);
            pushData.setData(settleData);
            pushService.push(pushData);
        }
    }
    
    //  
    public void sendCheckerSettleData() {
        List settleDataList = auditTaskDAO.statCheckerSettleData();
        for (WorkerSettleData settleData : settleDataList) {
            WorkerPushData pushData = new WorkerPushData();
            pushData.setId(settleData.getWorkerId());
            pushData.setType(WorkerPushDataType.CHECKER);
            pushData.setData(settleData);
            pushService.push(pushData);
        }
    }

    推奨事項:
    //  
    public void sendAuditorSettleData() {
        sendWorkerSettleData(WorkerPushDataType.AUDITOR, () -> auditTaskDAO.statAuditorSettleData());
    }
    
    //  
    public void sendCheckerSettleData() {
        sendWorkerSettleData(WorkerPushDataType.CHECKER, () -> auditTaskDAO.statCheckerSettleData());
    }
    
    //  
    public void sendWorkerSettleData(WorkerPushDataType dataType, WorkerSettleDataProvider dataProvider) {
        List settleDataList = dataProvider.statWorkerSettleData();
        for (WorkerSettleData settleData : settleDataList) {
            WorkerPushData pushData = new WorkerPushData();
            pushData.setId(settleData.getWorkerId());
            pushData.setType(dataType);
            pushData.setData(settleData);
            pushService.push(pushData);
        }
    }
    
    //  
    private interface WorkerSettleDataProvider {
        //  
        public List statWorkerSettleData();
    }

    主な収益

  • コアロジックを各業務関数から抽出し、業務コードをより明確にし、メンテナンスしやすいようにする.
  • 重複コードの複数回の記述を回避し、重複関数を簡素化すればするほど収益が大きくなる.

  • 関数コードレベルの削減


    関数を美しくするには、関数コードレベルを1~4にすることをお勧めします.インデントが多すぎると、関数が読みにくくなります.

    ケース1:returnによる事前戻り関数


    現象の説明:
    //  
    public Double getUserBalance(Long userId) {
        User user = getUser(userId);
        if (Objects.nonNull(user)) {
            UserAccount account = user.getAccount();
            if (Objects.nonNull(account)) {
                return account.getBalance();
            }
        }
        return null;
    }

    推奨事項:
    //  
    public Double getUserBalance(Long userId) {
        //  
        User user = getUser(userId);
        if (Objects.isNull(user)) {
            return null;
        }
    
        //  
        UserAccount account = user.getAccount();
        if (Objects.isNull(account)) {
            return null;
        }
    
        //  
        return account.getBalance();
    }

    ケース2:continueによる早期終了サイクル


    現象の説明:
    //  
    public double getTotalBalance(List userList) {
        //  
        double totalBalance = 0.0D;
    
        //  
        for (User user : userList) {
            //  
            UserAccount account = user.getAccount();
            if (Objects.nonNull(account)) {
                //  
                Double balance = account.getBalance();
                if (Objects.nonNull(balance)) {
                    totalBalance += balance;
                }
            }
        }
    
        //  
        return totalBalance;
    }

    推奨事項:
    //  
    public double getTotalBalance(List userList) {
        //  
        double totalBalance = 0.0D;
    
        //  
        for (User user : userList) {
            //  
            UserAccount account = user.getAccount();
            if (Objects.isNull(account)) {
                continue;
            }
    
            //  
            Double balance = account.getBalance();
            if (Objects.nonNull(balance)) {
                totalBalance += balance;
            }
        }
    
        //  
        return totalBalance;
    }

    詳細:
    その他の方法:ループでは、ケース1の関数getUserBalance(ユーザー残高の取得)を呼び出してから、残高を加算します.
    ループでは、continueを最大1回使用することを推奨します.複数回continueを使用する必要がある場合は、ループを関数にカプセル化することをお勧めします.

    ケース3:条件式関数による階層の削減


    次の章の「ケース2:複雑な条件式を関数にカプセル化する」を参照してください.

    主な収益

  • コードレベルが減少し、コードインデントが減少した.
  • モジュールの区分がはっきりしていて、読書とメンテナンスが便利です.

  • パッケージング条件式関数


    ケース1:単純条件式を関数にカプセル化


    現象の説明:
    //  
    public double getTicketPrice(Date currDate) {
        if (Objects.nonNull(currDate) && currDate.after(DISCOUNT_BEGIN_DATE)
            && currDate.before(DISCOUNT_END_DATE)) {
            return TICKET_PRICE * DISCOUNT_RATE;
        }
        return TICKET_PRICE;
    }

    推奨事項:
    //  
    public double getTicketPrice(Date currDate) {
        if (isDiscountDate(currDate)) {
            return TICKET_PRICE * DISCOUNT_RATE;
        }
        return TICKET_PRICE;
    }
    
    //  
    private static boolean isDiscountDate(Date currDate) {
        return Objects.nonNull(currDate) && 
    currDate.after(DISCOUNT_BEGIN_DATE)
            && currDate.before(DISCOUNT_END_DATE);
    }

    ケース2:複雑な条件式を関数にカプセル化


    現象の説明:
    //  
    public List getRichUserList(List userList) {
        //  
        List richUserList = new ArrayList<>();
    
        //  
        for (User user : userList) {
            //  
            UserAccount account = user.getAccount();
            if (Objects.nonNull(account)) {
                //  
                Double balance = account.getBalance();
                if (Objects.nonNull(balance) && balance.compareTo(RICH_THRESHOLD) >= 0) {
                    //  
                    richUserList.add(user);
                }
            }
        }
    
        //  
        return richUserList;
    }

    推奨事項:
    //  
    public List getRichUserList(List userList) {
        //  
        List richUserList = new ArrayList<>();
    
        //  
        for (User user : userList) {
            //  
            if (isRichUser(user)) {
                //  
                richUserList.add(user);
            }
        }
    
        //  
        return richUserList;
    }
    
    //  
    private boolean isRichUser(User user) {
        //  
        UserAccount account = user.getAccount();
        if (Objects.isNull(account)) {
            return false;
        }
    
        //  
        Double balance = account.getBalance();
        if (Objects.isNull(balance)) {
            return false;
        }
    
        //  
        return balance.compareTo(RICH_THRESHOLD) >= 0;
    }

    以上のコードはストリーミング(Stream)プログラミングを用いたフィルタリングでも実現できる.

    主な収益

  • 条件式を業務関数から独立し、業務論理をより明確にする.
  • パッケージの条件式は独立関数であり、コードで繰り返し使用できます.

  • 不要な空ポインタ判断はできるだけ避ける


    本章では,プロジェクト内部のコードにのみ適用し,自分が知っているコードであるため,不要な空ポインタ判断をできるだけ避けることができる.サードパーティのミドルウェアとシステムインタフェースについては、コードの堅牢性を保証するために、空のポインタ判断を行う必要があります.

    ケース1:呼び出し関数はパラメータが空でないことを保証し、呼び出された関数は不要な空ポインタ判断をできるだけ避ける


    現象の説明:
    //  
    User user = new User();
    ... //  
    createUser(user);
    
    //  
    private void createUser(User user){
        //  
        if(Objects.isNull(user)) {
            return;
        }
    
        //  
        userDAO.insert(user);
        userRedis.save(user);
    }

    推奨事項:
    //  
    User user = new User();
    ... //  
    createUser(user);
    
    //  
    private void createUser(User user){
        //  
        userDAO.insert(user);
        userRedis.save(user);
    }

    ケース2:呼び出された関数は戻りが空でないことを保証し、呼び出し関数はできるだけ不要な空のポインタの判断を避ける。


    現象の説明:
    //  
    public void saveUser(Long id, String name) {
        //  
        User user = buildUser(id, name);
        if (Objects.isNull(user)) {
            throw new BizRuntimeException(" ");
        }
    
        //  
        userDAO.insert(user);
        userRedis.save(user);
    }
    
    //  
    private User buildUser(Long id, String name) {
        User user = new User();
        user.setId(id);
        user.setName(name);
        return user;
    }

    推奨事項:
    //  
    public void saveUser(Long id, String name) {
        //  
        User user = buildUser(id, name);
    
        //  
        userDAO.insert(user);
        userRedis.save(user);
    }
    
    //  
    private User buildUser(Long id, String name) {
        User user = new User();
        user.setId(id);
        user.setName(name);
        return user;
    }

    ケース3:付与ロジックはリストデータ項目が空でないことを保証し、処理ロジックは不要な空ポインタ判断をできるだけ避ける


    現象の説明:
    //  
    List userList = userDAO.queryAll();
    if (CollectionUtils.isEmpty(userList)) {
        return;
    }
    
    //  
    List userVoList = new ArrayList<>(userList.size());
    for (UserDO user : userList) {
        UserVO userVo = new UserVO();
        userVo.setId(user.getId());
        userVo.setName(user.getName());
        userVoList.add(userVo);
    }
    
    //  
    for (UserVO userVo : userVoList) {
        //  
        if (Objects.isNull(userVo)) {
            continue;
        }
    
        //  
        ...
    }

    推奨事項:
    //  
    List userList = userDAO.queryAll();
    if (CollectionUtils.isEmpty(userList)) {
        return;
    }
    
    //  
    List userVoList = new ArrayList<>(userList.size());
    for (UserDO user : userList) {
        UserVO userVo = new UserVO();
        userVo.setId(user.getId());
        userVo.setName(user.getName());
        userVoList.add(userVo);
    }
    
    //  
    for (UserVO userVo : userVoList) {
        //  
        ...
    }

    ケース4:MyBatisクエリ関数の戻りリストとデータ項目は空ではなく、空のポインタで判断できます。


    MyBatisは優れた永続層フレームワークであり、プロジェクトで最も広く使用されているデータベースミドルウェアの1つです.MyBatisのソースコードを解析することで,クエリ関数が返すリストもデータ項目も空ではなく,コードでは空ポインタで判断しなくてもよい.
    現象の説明:
    この書き方は問題ないが,ただ保守的すぎるだけだ.
    //  
    public List queryUser(Long id, String name) {
        //  
        List userList = userDAO.query(id, name);
        if (Objects.isNull(userList)) {
            return Collections.emptyList();
        }
    
        //  
        List voList = new ArrayList<>(userList.size());
        for (UserDO user : userList) {
            //  
            if (Objects.isNull(user)) {
                continue;
            }
    
            //  
            UserVO vo = new UserVO();
            BeanUtils.copyProperties(user, vo);
            voList.add(vo);
        }
    
        //  
        return voList;
    }

    推奨事項:
    //  
    public List queryUser(Long id, String name) {
        //  
        List userList = userDAO.query(id, name);
    
        //  
        List voList = new ArrayList<>(userList.size());
        for (UserDO user : userList) {
            UserVO vo = new UserVO();
            BeanUtils.copyProperties(user, vo);
            voList.add(vo);
        }
    
        //  
        return voList;
    }

    主な収益

  • 不要な空ポインタ判断を避け、業務コード処理ロジックを簡素化し、業務コードの運行効率を高める.
  • これらの不要な空のポインタは、基本的には永遠に実行されないDeathコードに属し、削除はコードのメンテナンスに役立つと判断した.

  •  


    転載先:https://www.cnblogs.com/gavin-guo/p/11506641.html