- 投稿日:2020-11-14T23:19:57+09:00
プログラミング学習 備忘録11/14
・ローカル変数・・・main()やadd()のようなメソッド内で宣言した変数のこと。
仮引数もその1種・HTMLでのYoutube動画の埋め込み ⇨ 動画下の共有をクリック ⇨ 現れたダイアログの中にある「埋め込む」をクリック ⇨ 埋め込みコードが現れるのでコピーをクリック ⇨ コピーしたコードをvscode等に貼り付け
・HTMLでのGoogle Mapの埋め込み ⇨ 左上のメニューボタンから地図を共有または埋め込むを選択 ⇨ 地図を埋め込む ⇨ HTMLをコピー ⇨ コピーしたコードをvscode等に貼り付け
引用、参考
・スッキリわかるJava入門第3版
・徹底攻略Java silver SE11 問題集
・N予備校
- 投稿日:2020-11-14T22:39:12+09:00
ラズパイZeroとWebSocketとサーバーでCNNカメラ的なものを作ってみる
概要
QiitaのRaspberry Pi Advent Calendar 2020への参加記事です。
今更感はありますが、RaspberryPiZeroのカメラ画像をWebSocketでサーバーに転送し、オブジェクト・ディテクションを行います。処理の流れ
処理は、だいたい以下のような流れで実行します。
サーバーはJavaのマイクロフレームワークの一つ、Sparkを使用し、ラズパイ側はPython3を使用します。
また、オブジェクト・ディテクションは、Yolov3をOpenCV4で使用します。
作成するクラス等
今回は以下のクラス等を作成しました。
- Main.java:SparkFrameWorkの組み込みサーバーを起動するメインクラス
- CameraHandler.java:サーバーにラズパイが接続した時の処理クラス
- WebHandler.java:サーバーにブラウザが接続した時の処理クラス
- SessionList.java:接続したセッションを管理するクラス
- Yolo.java:Yolov3によりオブジェクト・ディテクションを行うクラス
- YoloSolver.java:オブジェクトディテクションを実行するクラス
- SolverThread.java:オブジェクト・ディテクションのスレッドクラス
- index.html:ラズパイカメラ画像を表示するHTML
- main.js/main.css:index.htmlで使用するjs/css
- camera_server.py:ラズパイ側のカメラ画像をサーバーに転送するPythonコード
サーバー側の処理/Webocket通信
ラズパイからの接続時、接続終了時、メッセージ受領時の処理のため、以下のCameraHandlerクラスを作成しました。
Camerahandlerクラスでは、カメラからメッセージ(カメラ画像等)を受け取るとYolo.getInstance().addSolverメソッドでオブジェクト・ディテクションを実行するクラスへ画像を引き渡します。CameraHandler.javaimport java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.util.Base64; import java.util.HashMap; import java.util.Map; import javax.imageio.ImageIO; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.WebSocket; import com.google.gson.Gson; import yolo.Yolo; @WebSocket public class CameraHandler { private static Gson gson=new Gson(); @OnWebSocketConnect public void onConnect(Session session)throws IOException { SessionList.getInstance().addCameraSession(session); Map<String,Object> obj=new HashMap<>(); String key=Integer.toString(session.hashCode()); obj.put("id", key); obj.put("act", "add"); obj.put("data", ""); SessionList.getInstance().broadcastWeb(gson.toJson(obj)); session.getRemote().sendString("O.K."); } @OnWebSocketClose public void onClose(Session session, int statusCode, String reason)throws IOException { SessionList.getInstance().removeCameraSession(session); Map<String,Object> obj=new HashMap<>(); String key=Integer.toString(session.hashCode()); obj.put("id", key); obj.put("act", "remove"); obj.put("data", ""); SessionList.getInstance().broadcastWeb(gson.toJson(obj)); } @OnWebSocketMessage public void onMessage(Session session, String message) throws IOException { String key=Integer.toString(session.hashCode()); if(Main.doYolo()){ Yolo.getInstance().addSolver(key, message,session); }else{ Map<String,Object> obj=new HashMap<>(); obj.put("id", key); obj.put("act", "update"); obj.put("data", message); SessionList.getInstance().broadcastWeb(gson.toJson(obj)); session.getRemote().sendString("O.K."); } } @OnWebSocketMessage public void onBinary(Session session, byte[] buffer, int offset, int length) throws IOException {} public void createImage(String str)throws IOException{ try{ byte[] decodedBytes = Base64.getDecoder().decode(str); BufferedImage img = ImageIO.read(new ByteArrayInputStream(decodedBytes)); ImageIO.write(img, "jpg", new File("test.jpg")); }catch(Exception e){ e.printStackTrace(); } } }ブラウザからの接続時、接続終了時の処理のため、以下のWebHandlerクラスを作成しました。
WebHandler.javaimport java.io.IOException; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.WebSocket; @WebSocket public class WebHandler { @OnWebSocketConnect public void onConnect(Session session){ SessionList.getInstance().addWebSession(session); } @OnWebSocketClose public void onClose(Session session, int statusCode, String reason){ SessionList.getInstance().removeWebSession(session); } @OnWebSocketMessage public void onMessage(Session session, String message) throws IOException { } @OnWebSocketMessage public void onBinary(Session session, byte[] buffer, int offset, int length) throws IOException { // session.getRemote().sendBytes(ByteBuffer.wrap(buffer)); } }サーバー側の処理/オブジェクト・ディテクション
オブジェクト・ディテクションを行うYoloクラスを作成しました。
Yolov3のモデルをOpenCV4.0のDNNに読み込んで解析を行っています。Yolo.javaimport java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Base64; import java.util.List; import javax.imageio.ImageIO; import org.eclipse.jetty.websocket.api.Session; import org.opencv.core.CvType; import org.opencv.core.Mat; import org.opencv.core.MatOfByte; import org.opencv.dnn.Dnn; import org.opencv.dnn.Net; import org.opencv.imgcodecs.Imgcodecs; public class Yolo { private static Yolo yolo=null; private List<String> outputNames; private Net net; public static Yolo getInstance(){ if(yolo==null){ yolo=new Yolo(); return yolo; }else{ return yolo; } } public Yolo(){ nu.pattern.OpenCV.loadShared(); String modelWeights = "yolov3_320.weights"; String modelConfiguration = "yolov3_320.cfg"; net=Dnn.readNetFromDarknet(modelConfiguration, modelWeights); net.setPreferableBackend(Dnn.DNN_BACKEND_CUDA); net.setPreferableTarget(Dnn.DNN_TARGET_CUDA); outputNames=createOutputNames(net); SolverThread st=new SolverThread(net,0.6f); st.start(); } public void setListener(PostProcessingListener l){ SolverThread.setListsner(l); } public List<String> getOutputNames(){ return outputNames; } private List<String> createOutputNames(Net net) { List<String> names = new ArrayList<>(); List<Integer> outLayers = net.getUnconnectedOutLayers().toList(); List<String> layersNames = net.getLayerNames(); outLayers.forEach((item) -> names.add(layersNames.get(item - 1))); return names; } public void addSolver(String name,String base64){ try{ BufferedImage bi=createImage(base64); YoloSolver sol=new YoloSolver(name,bi); SolverThread.add(sol); }catch(Exception e){ e.printStackTrace(); } } public void addSolver(String name,String base64,Session session){ try{ BufferedImage bi=createImage(base64); YoloSolver sol=new YoloSolver(name,bi,session); SolverThread.add(sol); }catch(Exception e){ e.printStackTrace(); } } public void addSolver(String name,BufferedImage bi){ try{ YoloSolver sol=new YoloSolver(name,bi); SolverThread.add(sol); }catch(Exception e){ e.printStackTrace(); } } public void addSolver(String name,BufferedImage bi,Session session){ try{ YoloSolver sol=new YoloSolver(name,bi,session); SolverThread.add(sol); }catch(Exception e){ e.printStackTrace(); } } public void addSolver(String name,Mat mat){ try{ YoloSolver sol=new YoloSolver(name,mat); SolverThread.add(sol); }catch(Exception e){ e.printStackTrace(); } } public void addSolver(String name,Mat mat,Session session){ try{ YoloSolver sol=new YoloSolver(name,mat,session); SolverThread.add(sol); }catch(Exception e){ e.printStackTrace(); } } public static BufferedImage createImage(String str)throws IOException{ try{ byte[] decodedBytes = Base64.getDecoder().decode(str); BufferedImage img = ImageIO.read(new ByteArrayInputStream(decodedBytes)); return img; }catch(Exception e){ e.printStackTrace(); return null; } } public static BufferedImage matToBi(Mat image){ MatOfByte bytemat = new MatOfByte(); Imgcodecs.imencode(".jpg", image, bytemat); byte[] bytes = bytemat.toArray(); InputStream in = new ByteArrayInputStream(bytes); BufferedImage img = null; try { img = ImageIO.read(in); } catch (IOException e) { e.printStackTrace(); } return img; } public static Mat biToMat(BufferedImage image) { image = convertTo3ByteBGRType(image); byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); Mat mat = new Mat(image.getHeight(), image.getWidth(), CvType.CV_8UC3); mat.put(0, 0, data); return mat; } private static BufferedImage convertTo3ByteBGRType(BufferedImage image) { BufferedImage convertedImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_3BYTE_BGR); convertedImage.getGraphics().drawImage(image, 0, 0, null); return convertedImage; }実際のオブジェクト・ディテクションは、以下のYoloSolver.javaで実行しています。
YoloSolver.javaimport java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Observable; import org.eclipse.jetty.websocket.api.Session; import org.opencv.core.Core; import org.opencv.core.Mat; import org.opencv.core.MatOfFloat; import org.opencv.core.MatOfInt; import org.opencv.core.MatOfRect2d; import org.opencv.core.Point; import org.opencv.core.Rect2d; import org.opencv.core.Scalar; import org.opencv.core.Size; import org.opencv.dnn.Dnn; import org.opencv.dnn.Net; import org.opencv.imgproc.Imgproc; import org.opencv.utils.Converters; public class YoloSolver extends Observable{ private BufferedImage bi; private String name; private long time; private BufferedImage dst; private List<Integer> clsIds; private List<Float> confs; private List<Rect2d> rects; private Mat frame; private Session session; public YoloSolver(String name,BufferedImage bi){ time=System.currentTimeMillis(); this.name=name; this.bi=bi; this.frame=Yolo.biToMat(bi); } public YoloSolver(String name,BufferedImage bi,Session session){ this(name,bi); this.session=session; } public YoloSolver(String name,Mat f){ time=System.currentTimeMillis(); this.name=name; this.bi=Yolo.matToBi(f); this.frame=f; } public YoloSolver(String name,Mat f,Session session){ this(name,f); this.session=session; } public void solve(Net net,float confThreshold){ Size sz = new Size(288,288); List<Mat> result = new ArrayList<>(); List<String> outBlobNames = Yolo.getInstance().getOutputNames(); Mat blob = Dnn.blobFromImage(frame, 0.00392, sz, new Scalar(0), true, false); net.setInput(blob); net.forward(result, outBlobNames); clsIds = new ArrayList<>(); confs = new ArrayList<>(); rects = new ArrayList<>(); for (int i=0;i<result.size();++i){ Mat level = result.get(i); for (int j=0;j<level.rows();++j){ Mat row = level.row(j); Mat scores = row.colRange(5, level.cols()); Core.MinMaxLocResult mm = Core.minMaxLoc(scores); float confidence = (float)mm.maxVal; Point classIdPoint = mm.maxLoc; if (confidence > confThreshold){ int centerX = (int)(row.get(0,0)[0] * frame.cols()); int centerY = (int)(row.get(0,1)[0] * frame.rows()); int width = (int)(row.get(0,2)[0] * frame.cols()); int height = (int)(row.get(0,3)[0] * frame.rows()); int left = centerX - width / 2; int top = centerY - height / 2; clsIds.add((int)classIdPoint.x); confs.add((float)confidence); rects.add(new Rect2d(left, top, width, height)); } } } float nmsThresh = 0.5f; if(confs.size()>0){ MatOfFloat confidences = new MatOfFloat(Converters.vector_float_to_Mat(confs)); Rect2d[] boxesArray = rects.toArray(new Rect2d[0]); MatOfRect2d boxes = new MatOfRect2d(boxesArray); MatOfInt indices = new MatOfInt(); Dnn.NMSBoxes(boxes, confidences, confThreshold, nmsThresh, indices); int [] ind = indices.toArray(); for (int i = 0; i < ind.length; ++i){ int idx = ind[i]; Rect2d box = boxesArray[idx]; Imgproc.rectangle(frame, box.tl(), box.br(), new Scalar(0,255,255), 2); } dst=Yolo.matToBi(frame); } } public BufferedImage getDstImage() { return dst; } public List<Integer> getClsIds() { return clsIds; } public List<Float> getConfs() { return confs; } public List<Rect2d> getRects() { return rects; } public BufferedImage getSrcImage(){ return bi; } public Date getDate(){ return new Date(time); } public String getName(){ return name; } public Session getSession() { return session; } }オブジェクト・ディテクションのためのスレッドとして、SolverThreadクラスを作成しました。
SolverTheradクラスは、キューに登録されたYoloSolverインスタンスを順次処理します。
また、解析終了後の処理のため、PostProcessingListenerインターフェイスを定義しました。SolverThread.javaimport java.util.Collections; import java.util.LinkedList; import java.util.List; import org.opencv.dnn.Net; public class SolverThread implements Runnable{ private static List<YoloSolver> queue=Collections.synchronizedList(new LinkedList<YoloSolver>()); private Thread thread; private Net net; private static PostProcessingListener listener; private float threshold=0.6f; public SolverThread(Net net,float confThreshold){ this.net=net; this.threshold=confThreshold; } public static void add(YoloSolver s){ synchronized (queue){ if(queue.contains(s)){ queue.notifyAll(); }else{ queue.add(s); queue.notifyAll(); } } } public static void remove(YoloSolver s){ synchronized (queue){ queue.remove(s); queue.notifyAll(); } } public void start(){ thread=new Thread(this); thread.start(); } public void stop(){ thread=null; } public void run() { while(thread!=null){ YoloSolver solver=null; synchronized(queue){ while(queue.isEmpty()){ try{ queue.wait(); }catch (InterruptedException e){} } if(thread!=null){ solver=(YoloSolver)queue.get(0); queue.remove(0); } } try{ if(solver!=null){ try{ solver.solve(net,threshold); if(listener!=null)listener.postProcessing(solver); }catch(Exception e){ e.printStackTrace(); remove(solver); } } }catch(Exception e){ e.printStackTrace(); } } } public static void setListsner(PostProcessingListener l){ listener=l; } }サーバー側の処理/Mainクラス
サーバー全体の処理を行うMainクラスを作成しました。
また、通信セッションを管理するSessionListクラスを作成しました。
/cameraがラズパイカメラの接続アドレス、/webがブラウザの接続アドレスで、/session/cameraは接続中のカメラセッション一覧を取得するアドレスです。
/cameraに接続したラズパイカメラは逐次を画像をサーバーに送信し、CameraHanderクラスで画像をCNN解析を行うYoloクラスへ渡し、Yoloクラス内でYoloSolverクラスを生成し、SolverThreadのキューへ登録されます。
解析が終わったデータは、Mainクラス内で定義したPostProcessingListenerにより、全てのWebブラウザセッションに更新画像をブロードキャストすると共に、ラズパイカメラセッションに処理終了を通知します。
画像を含むデータのやりとりはJsonで行っています。Main.javaimport static spark.Spark.get; import static spark.Spark.init; import static spark.Spark.port; import static spark.Spark.staticFileLocation; import static spark.Spark.webSocket; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UncheckedIOException; import java.util.HashMap; import java.util.Map; import java.util.Base64; import javax.imageio.ImageIO; import com.google.gson.Gson; import yolo.PostProcessingListener; import yolo.Yolo; import yolo.YoloSolver; public class Main { private static SessionList list=SessionList.getInstance(); private static Gson gson=new Gson(); private static boolean doYolo=false; public static void main(String[] args) { setYolo(true); staticFileLocation("/public"); webSocket("/camera", CameraHandler.class); webSocket("/web", WebHandler.class); port(8080); init(); get("/session/camera", (request, response) -> { try{ String[] ret=list.getCameraId(); response.type("application/json"); response.status(200); return new Gson().toJson(ret); }catch(Exception e){ e.printStackTrace(); response.status(400); response.type("application/json"); return gson.toJson(new String[0]); } }); } public static boolean doYolo(){ return doYolo; } public static void setYolo(boolean flg){ doYolo=flg; if(doYolo){ PostProcessingListener li=new PostProcessingListener(){ @Override public void postProcessing(YoloSolver sol) { try{ Map<String,Object> obj=new HashMap<>(); obj.put("id", sol.getName()); obj.put("act", "update"); if(sol.getDstImage()==null){ String message=jpgToStr(sol.getSrcImage()); obj.put("data", message); }else{ String message=jpgToStr(sol.getDstImage()); obj.put("data", message); } SessionList.getInstance().broadcastWeb(gson.toJson(obj)); }catch(Exception e){ e.printStackTrace(); } try{ sol.getSession().getRemote().sendString("O.K."); }catch(IOException e){ e.printStackTrace(); } } }; Yolo.getInstance().setListener(li); } } private static String jpgToStr(BufferedImage img){ final ByteArrayOutputStream os = new ByteArrayOutputStream(); try{ ImageIO.write(img, "jpg", os); return Base64.getEncoder().encodeToString(os.toByteArray()); } catch (final IOException ioe){ throw new UncheckedIOException(ioe); } } }SessionList.javaimport java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import org.eclipse.jetty.websocket.api.Session; public class SessionList { private Map<String,Session> camera; private Map<String,Session> web; private static SessionList list=null; private SessionList(){ camera=new HashMap<>(); web=new HashMap<>(); } public static SessionList getInstance(){ if(list==null){ list=new SessionList(); return list; }else{ return list; } } public String[] getCameraId(){ String[] keys=camera.keySet().toArray(new String[camera.keySet().size()]); Arrays.sort(keys); return keys; } public void addCameraSession(Session s){ String key=Integer.toString(s.hashCode()); camera.put(key, s); } public void removeCameraSession(Session s){ String key=Integer.toString(s.hashCode()); camera.remove(key); } public void addWebSession(Session s){ String key=Integer.toString(s.hashCode()); web.put(key, s); } public void removeWebSession(Session s){ String key=Integer.toString(s.hashCode()); web.remove(key); } public void broadcastWeb(String mes)throws IOException{ for(Session s : web.values()){ s.getRemote().sendString(mes); } } }ラスパイ側の処理
ラズパイZero側では、逐次カメラ画像をサーバーに転送するCameraServer.pyを作成しました。
CameraServer.pyimport cv2 import websocket import base64 url="ws://アドレス:ポート/camera" WIDTH=640 HEIGHT=480 FPS=6 camera_id=0 cap = cv2.VideoCapture(camera_id) cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT) cap.set(cv2.CAP_PROP_FPS, FPS) encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 85] def on_message(ws, message): if message=="O.K.": try: send_data(ws,cap) except: print('Error') def on_error(ws, error): print(error) def on_close(ws): print("### closed ###") def on_open(ws): print('### open ###') ws = websocket.WebSocketApp(url, on_open=on_open, on_message = on_message, on_error = on_error, on_close = on_close) def send_data(ws,cap): ret, frame = cap.read() res, enc = cv2.imencode('.jpg', frame, encode_param) dst=base64.b64encode(enc).decode('utf-8') ws.send(dst) try: ws.run_forever() except KeyboardInterrupt: ws.close() cap.release()Web接続用HTML
最後にラズパイカメラ画像を表示するHtmlを作成しました。
以下のindex.htmlとmain.js、main.cssが実装です。index.html<!doctype html> <html lang="ja" > <head> <meta charset="UTF-8" /> <title>Iot-Camera-Test</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-dragdata@0.1.0/dist/chartjs-plugin-dragData.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@1.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script> <script src="/js/chartjs-plugin-labels.min.js"></script> <script src="js/main.js"></script> <link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" /> <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.css" /> <link rel="stylesheet" type="text/css" href="css/main.css" /> </head> <body> <header> <div class="collapse bg-dark" id="navbarHeader"> <div class="container"> <div class="row"> <div class="col-sm-8 col-md-7 py-4"> <h4 class="text-white">About</h4> <p class="text-white">テストサイトです。</p> </div> </div> </div> </div> <div class="navbar navbar-dark bg-dark shadow-sm"> <input type="checkbox" class="openSidebarMenu" id="openSidebarMenu"> <label for="openSidebarMenu" class="sidebarIconToggle"> <div class="spinner diagonal part-1"></div> <div class="spinner horizontal"></div> <div class="spinner diagonal part-2"></div> </label> <div id="sidebarMenu"> <ul class="sidebarMenuInner" id="log"> <li><a href="#">サイドパネル</a></li> <hr /> <li>接続状況 <span>Web/CAMERA</span></li> </ul> </div> <div class="container d-flex justify-content-between"> <a href="#" class="navbar-brand d-flex align-items-center"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg> <strong>Bingo-IoT-Camera</strong> </a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarHeader" aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> </div> </div> </header> <main role="main"> <section class="jumbotron text-center"> <div class="container"> <h2 class="jumbotron-heading">IoTカメラ映像(仮)</h2> <p class="lead text-muted">IoTカメラの映像を確認することができます。</p> </div> <a href="#" class="btn btn-primary my-2" onclick="addCardTest();">カード追加テスト</a> <a href="#" class="btn btn-primary my-2" onclick="removeCardTest()">カード削除テスト</a> </section> <div class="container"> <div class="row justify-content-center"> <div class="col-auto mb-3 card-columns" id="card_deck"> </div> </div> <footer class="pt-4 my-md-5 pt-md-5 border-top"> <div class="row"> <div class="col-12 col-md"> <!-- <img class="mb-2" src="./img/ft_img_logo.png" alt="" height="40"> --> <h5>Logo</h5> <ul class="list-unstyled text-small"> <li><a class="text-muted" href="#">Cool stuff</a></li> <li><a class="text-muted" href="#">Random feature</a></li> </ul> </div> <div class="col-6 col-md"> <h5>Features</h5> <ul class="list-unstyled text-small"> <li><a class="text-muted" href="#">Cool stuff</a></li> <li><a class="text-muted" href="#">Random feature</a></li> </ul> </div> <div class="col-6 col-md"> <h5>Resources</h5> <ul class="list-unstyled text-small"> <li><a class="text-muted" href="#">Resource</a></li> <li><a class="text-muted" href="#">Resource name</a></li> </ul> </div> <div class="col-6 col-md"> <h5>About</h5> <ul class="list-unstyled text-small"> <li><a class="text-muted" href="#">Team</a></li> <li><a class="text-muted" href="#">Locations</a></li> </ul> </div> </div> </footer> </div> </main> </body> </html>main.jsconst socket=new WebSocket("ws://"+window.location.host+"/web"); socket.onopen = (e)=>{ $.get("/session/camera", function(data){ data.forEach(id => addCard(id)); }); }; socket.onmessage = (e)=>{ const obj=JSON.parse(e.data); const id=obj.id; const act=obj.act; const data=obj.data; if(act=="update"){ let canvas=document.getElementById(id+'_img'); let ctx=canvas.getContext('2d'); let img=new Image(); img.src='data:image/jepg;base64,'+data; img.onload = function(){ ctx.drawImage(img, 0, 0,640,480,0,0,480,360); } }else if(act=="remove"){ removeCard(id); const li=$("<li />"); li.text("camera("+id+") close"); li.attr("class","text-muted"); li.css("font-size","14"); li.css("line-height","18px"); $("#log").append(li); }else if(act=="add"){ addCard(id); const li=$("<li />"); li.text("camera("+id+") open"); li.attr("class","text-muted"); li.css("font-size","14"); li.css("line-height","18px"); $("#log").append(li); } }; const removeCard=(id_name)=>{ const div=$("#"+id_name); div.remove(); }; let count=0; const addCardTest=()=>{ addCard("Test"+String(count)); count++; }; const removeCardTest=()=>{ if(count>0){ removeCard("Test"+String(count-1)); count--; } }; const addCard =(id_name) =>{ const container=$("<div />"); container.attr("class","card shadow-sm"); container.attr("id",id_name); container.css("width","482px"); container.css("padding","0px"); const header=$("<div />"); header.attr("class","card-header"); container.append(header); const title=$("<h4 />"); title.attr("class","my-0 font-weight-normal"); title.text(id_name); header.append(title); const body=$("<div />"); body.attr("class","card-body"); body.css("margin","0px"); body.css("padding","0px"); container.append(body); canvas=$("<canvas />"); canvas.attr("id",id_name+"_img"); canvas.attr("width","480px"); canvas.attr("height","360px"); body.append(canvas); const footer=$("<div />"); footer.attr("class","card-footer") container.append(footer); const bt=$("<a />"); bt.attr("class","btn btn-primary btn-sm" ); bt.attr("role","button"); bt.attr("onclick","addChart('"+id_name+"');"); bt.text("統計情報"); footer.append(bt); $("#card_deck").append(container); }; const addChart =(id_name) =>{ const container=$("<div />"); container.attr("class","card shadow-sm"); container.attr("id",id_name+"_chartcard"); container.css("width","482px"); container.css("padding","0px"); const header=$("<div />"); header.attr("class","card-header"); container.append(header); const title=$("<h4 />"); title.attr("class","my-0 font-weight-normal"); title.text(" "+id_name); header.append(title); const body=$("<div />"); body.attr("class","card-body"); body.css("margin","0px"); body.css("padding","0px"); const close=$("<a />"); close.attr("class","btn btn-secondary btn-sm" ); close.attr("role","button"); close.attr("onclick","$('#"+id_name+"_chartcard').remove()"); close.text("×"); close.css("padding","0px") close.css("width","24px"); close.css("height","24px"); close.css("font-size","14px"); title.prepend(close); container.append(body); canvas=$("<canvas />"); canvas.attr("id",id_name+"_chart"); canvas.attr("width","480px"); canvas.attr("height","360px"); body.append(canvas); const footer=$("<div />"); footer.attr("class","card-footer") container.append(footer); const bt=$("<a />"); bt.attr("class","btn btn-primary btn-sm" ); bt.attr("role","button"); bt.attr("onclick","alert('Test');"); bt.text("日間変動"); footer.append(bt); // $("#card_deck").append(container); $("#"+id_name).after(container); const ctx = document.getElementById(id_name+"_chart").getContext("2d"); const start=new Date(2020, 12, 1); dates=[]; vals=[]; for(let i=0;i<31;i++){ dates.push(new Date(start.getTime()+i*60*60*24*1000)); let v=Math.round(Math.random()*50); vals.push(v); } var data= { labels: dates, datasets: [ { label: '検出回数', data:vals, backgroundColor: "rgba(219,39,91,0.5)" } ] }; var options={ scales: { xAxes: [{ type: 'time', time: { unit: 'day' } }] } }; var mChart = new Chart(ctx, { type: 'line', data: data, options: options }); };main.cssinput[type=checkbox]:checked ~ .sidebarIconToggle > .horizontal { transition: all 0.3s; box-sizing: border-box; opacity: 0; } input[type=checkbox]:checked ~ .sidebarIconToggle > .diagonal.part-1 { transition: all 0.3s; box-sizing: border-box; transform: rotate(135deg); margin-top: 8px; } input[type=checkbox]:checked ~ .sidebarIconToggle > .diagonal.part-2 { transition: all 0.3s; box-sizing: border-box; transform: rotate(-135deg); margin-top: -9px; } .card-columns { column-count:2; } .spinner { transition: all 0.3s; box-sizing: border-box; position: absolute; height: 3px; width: 100%; background-color: #fff; } .horizontal { transition: all 0.3s; box-sizing: border-box; position: relative; float: left; margin-top: 3px; } .diagonal.part-1 { position: relative; transition: all 0.3s; box-sizing: border-box; float: left; } .diagonal.part-2 { transition: all 0.3s; box-sizing: border-box; position: relative; float: left; margin-top: 3px; } input[type="checkbox"]:checked ~ #sidebarMenu { transform: translateX(0); } input[type=checkbox] { transition: all 0.3s; box-sizing: border-box; display: none; } .sidebarIconToggle { transition: all 0.3s; box-sizing: border-box; cursor: pointer; position: absolute; z-index: 99; height: 100%; width: 100%; top: 22px; left: 15px; height: 22px; width: 22px; } #sidebarMenu { float: left; height: 100%; position: fixed; left: 0; top: 58px; width: 250px; z-index: 50; transform: translateX(-250px); transition: transform 250ms ease-in-out; background: linear-gradient(180deg, #f0ffff 0%, #f0ffff 100%); } .sidebarMenuInner{ margin:0; padding:0; border-top: 1px solid rgba(255, 255, 255, 0.10); } .sidebarMenuInner li{ list-style: none; color: #666; text-transform: uppercase; font-weight: bold; padding: 20px; cursor: pointer; border-bottom: 1px solid rgba(255, 255, 255, 0.10); } .sidebarMenuInner li span{ display: block; font-size: 14px; color: rgba(180, 180, 180, 0.50); } .sidebarMenuInner li a{ color: #666; text-transform: uppercase; font-weight: bold; cursor: pointer; text-decoration: none; }実装結果
ラズパイZeroの性能やシステムの仕様の関係もあり、かなり遅延が生じますが、一応、CNNカメラ的に機能します。
また、サーバー側に余裕があれば、ラズパイZero(カメラ)複数台を同時接続することができます。最後に
もともとは、RaspberryPiZeroを野生動物などの野外調査に利用できないかと思い、実験したコードです。
なかなか、使用する機会もないので、アドベントカレンダーのネタとして消費しました。
そのうち、Raspberry Pi4+Coral USB Acceleratorを使い、カメラ側でのCNNも試してみたいと思います。
- 投稿日:2020-11-14T20:49:16+09:00
Mediator Pattern
Mediatorは、Mediatorの状態を監視するクラスのインスタンスを保持する
Mediatorを監視するクラスはMediatorのインスタンスを保持する
Mediatorは自身の状態が変化したときに、Mediatorを監視する各クラスのインスタンスを通じて、各クラスに状態の変化を通知する以下のクラス構成で確認します
クラス 説明 Mediator.class Mediatorを監視するクラスインスタンスをListに格納
Listにインスタンスがadd()されたら、Listの全要素にListのsizeを通知するabstract
Observer.classMediatorインスタンスを保持する
Mediatorが利用するメソッドを実装ob1.class~ob2.class Observerを実装 user(Main.class) 動作確認 *user 他の開発者がこのパターンを利用する、という意味合いを含みます
Mediator.classclass Mediator{ List list = new ArrayList<Observer>(); void add(Observer obsvr){ list.add(obsvr); notifyTo(list.size()); // Listが更新されたらnotifyTo()を実行 } void notifyTo(int size){ Iterator it = list.iterator(); while(it.hasNext()){ Observer ob = (Observer) it.next(); ob.update(size); // Listに格納した各Observerクラスのupdate()を使って通知 } } }abstract_Oberver.classabstract class Observer{ Mediator mediator = null; int mediatorListSize = 0; Observer(Mediator med){this.mediator=med;} void update(int size){ mediatorListSize=size; System.out.println( mediatorListSize+":"+mediator.list.size() ); } }ob1.class_ob2.classclass ob1 extends Observer{ob1(Mediator med){super(med);}} class ob2 extends Observer{ob2(Mediator med){super(med);}}user(Main.class)public static void main(String[] args){ Mediator md = new Mediator(); md.add(new ob1(md)); md.add(new ob2(md)); }
- 投稿日:2020-11-14T20:49:16+09:00
Observer Pattern
複数のObserverが一つのオブジェクトの状態変化を監視する
監視されるクラスは複数のObserverインスタンスを保持し、Observerクラスのメソッドを使って状態変化を各Observerに通知する
Observerは監視対象のクラスオブジェクトを保持する以下のクラス構成で確認します
クラス 説明 Mediator.class 各Observerクラスが監視するオブジェクト
各ObserverクラスのインスタンスをListに格納
Listにインスタンスがadd()されたら、全ObserverにListのsizeを通知するabstract
Observer.class監視対象のMediatorインスタンスを保持する
Mediatorが利用するメソッドを実装ob1.class~ob2.class Observerを実装 user(Main.class) 動作確認 *user 他の開発者がこのパターンを利用する、という意味合いを含みます
Mediator.classclass Mediator{ List list = new ArrayList<Observer>(); void add(Observer obsvr){ list.add(obsvr); notifyTo(list.size()); // Listが更新されたらnotifyTo()を実行 } void notifyTo(int size){ Iterator it = list.iterator(); while(it.hasNext()){ Observer ob = (Observer) it.next(); ob.update(size); // Listに格納した各Observerクラスのupdate()を使って通知 } } }abstract_Oberver.classabstract class Observer{ Mediator mediator = null; int mediatorListSize = 0; Observer(Mediator med){this.mediator=med;} void update(int size){ mediatorListSize=size; System.out.println( mediatorListSize+":"+mediator.list.size() ); } }ob1.class_ob2.classclass ob1 extends Observer{ob1(Mediator med){super(med);}} class ob2 extends Observer{ob2(Mediator med){super(med);}}user(Main.class)public static void main(String[] args){ Mediator md = new Mediator(); md.add(new ob1(md)); md.add(new ob2(md)); }
- 投稿日:2020-11-14T17:54:57+09:00
Javaの配列とArrayListの違いについて、似た機能のメソッドを比較・対応してみた
はじめに
Javaの問題を解くとき、プログラムの作成条件として、"配列を使うように"とのお達しがあったのですが、
「はて、ArrayListではアカンのか?」
「そもそも配列ってなんだったっけ、めちゃくちゃ曖昧にしか認識してない」
と感じたので色々と調べてみました。
複数の言語を触った経験が悪影響を及ぼし、『リスト』『タプル』『配列』『ArrayList』など、あらゆるキーワードがごちゃまぜになっている状態です。
Javaに限ってもLinkedListなんてものもまだありますし、整理がなかなか大変です。網羅的に、そして深くまで把握するのは今の自分には厳しかったのと、更にそれを記事にまとめるのも厳しいと感じたので、
ふわっとした感じ、「とりあえず間違ってはいない」という内容になるように努めました。
ジェネリクスだとか実装だとか、そういったものはまだ理解&説明しきれる気がしません・・実行環境
Paiza.io : openjdk version "15" 2020-09-15)
この記事でわかること
- 配列とArrayListは大体こんな感じで異なる
- 「配列でこう書く」 vs 「ArrayListでこう書く」
- 宣言
- 要素の代入
- 要素の追加
- 要素の取得
- 要素数(大きさ)の取得
配列とArrayListの違い
配列とArrayListの特徴からみた違い
色々と調べたのですが、とにかく掘れば掘るほど情報がでてくる上、
内容も濃いものまで含まれるため、簡単にとどめます。配列は固定長です。
宣言したときに要素数が固定され、変更することができません。
例えば「こいつは要素を5個入れるように設定しよう!」としたとき、6個目の要素を入れることはできません。これに対して、ArrayListは可変長です。
一応、初期要素数の設定はするのですが(あるいは指定せずともデフォルトで設定される)
その要素数を超過しても、要素を次々に放り込むことが可能です。補足
大きさは変更可能なのですけど、一応初期サイズとして10個の要素が格納できるArrayListを作成してくれます。このサイズは足りなくなれば自動的に拡張されるのであまり気にする必要はありません。
Let's プログラミング 初心者の方でもわかりやすいサイト宣言について
配列の宣言
hogeというint型の配列を作り、そこに1から5までの整数を入れてみます。
ひとまず調べてみたところ、3種類ヒットしたのでそれらを記します。int[] hoge; hoge = new int[5]; hoge[0] = 1; hoge[1] = 2; hoge[2] = 3; hoge[3] = 4; hoge[4] = 5;int[] hoge = new int {1, 2, 3, 4, 5}int [] hoge = {1, 2, 3, 4, 5}ArrayListの宣言
hogeというInteger型のArrayListを作り、1から5までの整数を入れてみます。
ひとまず調べてみたところ、3種類ヒットしたのでそれらを記します。
Integer
?int
じゃないの?と思った方は、
「Java プリミティブ型 ラッパークラス」あたりで検索かけると幸せになれるかもしれません。ArrayList<Integer> hoge = new ArrayList<Integer>(); hoge.add(1); hoge.add(2); hoge.add(3); hoge.add(4); hoge.add(5); // 他の書き方として、 // List<Integer> hoge = new ArrayList<Integer>(); // あるいはJava 10以降なら // var hoge = new ArrayList<Integer>(); // という書き方でもいいよList<Integer> list = new ArrayList<Integer>(){ { add(1); add(2); add(3); add(4); add(5); };List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); // こういう書き方もできるけど、可変長ではなくなるので注意要素の代入について
例として、hogeという配列(ArrayList)の0番目に5を代入します。
要素の代入(配列のとき)
hoge[0] = 5;要素の代入(ArrayListのとき)
hoge.set(0, 5)要素の追加
例としてint型の値 10 を追加します
要素の追加(配列のとき)
固定長なので、そもそもできない。
要素の追加(ArrayListのとき)
hoge.add(10);要素を取得する
例として配列(ArrayList)の0番目の要素を取り出します。
配列とArrayListでカッコの種類が違うので注意!要素を取得する(配列のとき)
int foo = hoge[0];要素を取得する(ArrayListのとき)
int foo hoge.get(0);要素数(大きさ)を取得する
要素数を取得する(配列のとき)
int foo = hoge.length要素数を取得する(ArrayListのとき)
int foo = hoge.size();おわりに
今後ひんぱんに使うだろうなぁと思った操作を挙げて対応してみました。
加筆するかもしれません。参考
- 投稿日:2020-11-14T17:30:24+09:00
Logbackでログメッセージ内の秘密情報をマスク
概要
マスク対象を正規表現で抽出できることが条件だが、Logbackの設定において
%replace
で文字列を置換できる。
- http://logback.qos.ch/manual/layouts_ja.html#cwOptions ← クレジットカード番号をマスクする例
Javaの正規表現の機能や、キャプチャした部分文字列の参照を利用すれば、上の例より難しい条件でのマスクもできる。その例と実際のコードを記す。
例
以下のログには、ユーザーID・トークン・リソースIDがどれも同じ形式(十六進32桁)で出力されている。このうちトークンのみをマスクしたい。
ログの例2020-11-14T09:30:52.774+09:00 [main] INFO com.example.Main - UserID: 35f44b06a3cf8dab8355eb8ba5844c73, Token: b9656056c799ab9ba19cebe12b49992b, ResourceID: 945c4f63c61f1bc7ba632fe0ce25aa0dLogbackの設定<configuration debug="true"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%date{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} [%thread] %level %logger - %message%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="STDOUT" /> </root> </configuration>方法
正規表現でトークン部分を抽出するなら、この例では「直前に
Token
と書かれていること」を利用できそう。(あらゆる状況で厳密に判定しようとすると正規表現だけでは無理)
%message
を次のように書き換える。%replace(%message){'((?i:token).{0,10}?)\b\p{XDigit}{32}\b','$1****'}するとログは以下の通りになる。トークン部分のみが
****
となり、他は変化していない。2020-11-14T09:43:31.724+09:00 [main] INFO com.example.Main - UserID: 5457645aaa75b97eb9e2c7b0aec79ca6, Token: ****, ResourceID: c194b0155ac7ece290092c1ee2a73948
%replace
が丸括弧および2つの引数をとるのは、String#replaceAll()
のレシーバーおよび2引数と同じと考えていい(と思う)。もう少し正規表現を頑張れば、「トークンの前後4桁ずつは残す」といったこともできる。
(補足)正規表現の詳細
- 「十六進32桁」
- 十六進数に使われる文字は
\p{XDigit}
で表せる([0-9A-Fa-f]
と同じ)- 32回繰り返すことを表すため、パターンの後ろに
{32}
を付ける- 32桁より長い場合を排除するため、前後に
\b
(単語境界)を付ける
- ただし
_
が単語の一部扱いなので、これで区切っている場合は使えない- より詳細に境界を定めたいなら、否定[先後]読みを利用できる
- 以上より、正規表現は
\b\p{XDigit}{32}\b
となる- 「直前に
Token
と書かれていること」
- 戦略としては、マスク対象より前の文字列はキャプチャしておき置換文字列内で参照する
- そのためにキャプチャ対象を
()
で囲う- 参照時は
$n
と書く(※ n はキャプチャグループの番号)Token
token
TOKEN
などのバリエーションに対応できるよう、(?i:)
で一時的に大文字小文字を無視するToken
の後に何文字か入るのを許容する
.*
だと最長一致のため、今回の例では正しくマスクできない.*?
もよくない、例えばtoken validation for user 0123... is failed
とか- というわけで字数制限はつけた方が安全(長さは要検討、最長・最短は任意)
- 以上より、正規表現は
((?i:token).{0,10}?)
となるLogstashでJSON形式にする場合
(Logstash内にもマスク処理の設定があるみたいだが、そちらはまだ調べられていない)
Logback内の設定をJSONで書くため、バックスラッシュをエスケープする必要がある。(さもないと、
\b
はバックスペース、\p
は不正なエスケープと認識される)設定(一部){ "timestamp": "%date{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}", "thread": "%thread", "level": "%level", "logger": "%logger", "message": "%replace(%message){'((?i:token).{0,10}?)\\b\\p{XDigit}{32}\\b','$1****'}" }ログ{"timestamp":"2020-11-14T11:31:38.259+09:00","thread":"main","level":"INFO","logger":"com.example.Main","message":"UserID: c610e22e634ed2ff9f1bb27afc81e638, Token: ****, ResourceID: de343ea6405a8c559043c3e3e84f9bcd"}(付録)実験コード
今回の実験に使用したコードは以下の通り。
pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>logback-sample</artifactId> <version>1.0-SNAPSHOT</version> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>6.4</version> </dependency> </dependencies> </project>src/main/resources/logback.xml<configuration debug="true"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%date{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} [%thread] %level %logger - %replace(%message){'((?i:token).{0,10}?)\b\p{XDigit}{32}\b','$1****'}%n</pattern> </encoder> </appender> <appender name="STDOUT_JSON" class="ch.qos.logback.core.ConsoleAppender"> <!-- https://github.com/logstash/logstash-logback-encoder --> <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> <providers> <pattern> <pattern> { "timestamp": "%date{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}", "thread": "%thread", "level": "%level", "logger": "%logger", "message": "%replace(%message){'((?i:token).{0,10}?)\\b\\p{XDigit}{32}\\b','$1****'}" } </pattern> </pattern> </providers> </encoder> </appender> <root level="INFO"> <appender-ref ref="STDOUT" /> <appender-ref ref="STDOUT_JSON" /> </root> </configuration>src/main/java/com/example/Main.javapackage com.example; public class Main { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Main.class); public static void main(String[] args) { log.info("UserID: {}, Token: {}, ResourceID: {}", hex(), hex(), hex()); } private static String hex() { return new java.util.Random().ints(16, 0, 256) .mapToObj(x -> String.format("%02x", x)) .reduce("", (a, b) -> a + b); } }
- 投稿日:2020-11-14T16:35:50+09:00
TDDに取り組むため実践イメージと本質的イメージを押さえる
はじめに
TDDに取り組むため、そして他者にもTDDに対する本質的なところのイメージを共有するために、本記事を記す。
※イメージ重視。一部まとめ終わっていない箇所ありますがご容赦ください。TDDとは
テスト技法ではなく、設計/分析技法
- 価値
- インクリメンタルな設計を促す開発手法
- チームに良いリズムを生む
- 開発に自信と信頼をもたらす
- 原則
- 実装する前にテストを作ること
- 問題を放置せず少しずつ前進させること
- 大事なのは完璧さより自信を持てる状態を保つこと
- ルール
- (自動化された)テストが失敗したときのみ、新しいコードを書く
- 重複を除去する
インクリメンタルな設計とは
- できるだけ無駄なく価値あるものを作るために
- 開発対象への理解が進むほど正しい判断が可能になるため
- 何を作るか正しい判断ができるまでは決めないでおくが
- 最低限必要な機能から少しずつ開発を進め設計自体も少しずつ進化させつつ
- 開発対象への理解度を少しずつ上げることで、正しい判断をできるようにする
適したケース:アジャイル
・作りたいものが決まっていない/分かっていない
・開発チームが開発対象を十分に理解していない
・開発対象が複雑で正しい設計を誰も知らない流れ
- Red :失敗するテストを書く(機能追加/条件追加/バグ再現 etc.)
- Green :テストを成功させる(最小限のテストを書く、重複コードを書く等の罪を犯してよい)
- Refactor :動作を保ったまま設計を洗練する(テストを通すために書いた重複除去)
上記のサイクルを回すことで、リズミカルかつ自信と信頼に基づき、インクリメンタルな設計を行っていく
行ううえでのポイント
- TODOリストに必要なテストを書く
- 着手する前に必要になりそうなテストを書きだす(現時点で認知できるものだけでいい)
- 「すぐにやる」「あとでやる」「まったくやる必要なし」の3つリストに振り分ける
- 実装しなければならない振舞いを考えられるだけ書き出し、まだ実装がない操作をリストに加える
- 今書いたコードを綺麗にするためのリファクタも書きだす
- 新しいテストを思い付いたらリストに書く。リファクタも同様
- 機能は実際に必要となるまでは追加しない方が良い
- きっと後で必要になるだろう→9割無駄になる
- 今必要とするもの以上の機能を追加すると設計が複雑になる
- 必要ない機能を追加すると、他メンバーが読む時間やドキュメントへの記載などリソースが奪われるだけ
- コードを早く・バグを減らす
- 最適な方法は、実装するコードを必要最低限に留めること
- 静的設計と動的設計を相互に行き来きすることで、設計を洗練していく
- 静的設計(責任を設計)
- インターフェース
- 問題を最も単純化できるクラス/メソッド構成
- 名称(クラス/メソッド等)
- 責任内容を端的に表現
- 動的設計(仕事の正しさを設計)
- 振舞い
- 処理内容:どういう手順でどのクラス/メソッドを使い処理を行うか
- 事前条件/事後条件
- In/Out:事前条件に対して結果(事後条件)は何か
実践イメージのサンプル
題材:ボウリング点数計算
使用環境:
- IntelliJ Community版
- Java 11 ※古くて申し訳ない
- Gradle ※下2つのライブラリ取得/管理とビルド実行のために
- JUnit5
- AssertJまずは簡単かつ先に進む道筋足り得るものから
全てガーターのケース
スタートライン
最初に追加するテストは、「何もしないテスト」を書く
Red
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; public class BowlingGameTest { @Test public void test_all_Garter() { var game = new BowlingGame(); assertThat(game).isNotNull(); } }Green
public class BowlingGame { }最小のメソッドを追加
Red
全投球ガーターのテストケースにする
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; public class BowlingGameTest { @Test public void test_all_Garter() { var game = new BowlingGame(); + for(int count = 0; count < 20; count++) { + game.recordOneShot(0); + } - assertThat(game).isNotNull(); + assertThat(game.getTotalScore()).isEqualTo(0); } }Green
public class BowlingGame { + public void recordOneShot(int pins) { + } + public int score() { + return 0; + } }全部1ピンだけ倒したケース
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; public class BowlingGameTest { // ry + @Test + public void test_all_one_pin() { + var game = new BowlingGame(); + for (int count = 0; count < 20; count++) { + game.recordOneShot(1); + } + assertThat(game.score()).isEqualTo(20); + } }Green
public class BowlingGame { private int totalScore = 0; + public void recordOneShot(int pins) { + totalScore += pins; + } + public int getTotalScore() { + return totalScore; + } }不吉なにおいがしたらリファクタ
コードの「不吉なにおい」をきっかけに行う
- 重複コード
- 行数が長いメソッド
- 多すぎる引数 etc.
※テストコードも対象
// テストコードリファクタ(重複コード除去:メソッド抽出) import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; public class BowlingGameTest { + private BowlingGame game; + + @BeforeEach + public void setup() { + game = new BowlingGame(); + } @Test public void test_test_all_Garter() { - var game = new BowlingGame(); - for(int count = 0; count < 20; count++) { - game.recordOneShot(0); - } + recordSamePinsManyShot(20, 0); assertThat(game.getTotalScore()).isEqualTo(0); } @Test public void test_all_one_pin() { - var game = new BowlingGame(); - for (int count = 0; count < 20; count++) { - game.recordOneShot(1); - } + recordSamePinsManyShot(20, 1); assertThat(game.getTotalScore()).isEqualTo(20); } + private void recordSamePinsManyShot(int shotCount, int pins) { + for (int count = 0; count < shotCount; count++) { + game.recordOneShot(pins); + } + } }少し複雑なケース:スペア(次の回のピン数が加算される)
Red
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class BowlingGameTest { private BowlingGame game; @BeforeEach public void setup() { game = new BowlingGame(); } // ry + @Test + public void test_spare() { + game.recordOneShot(4); + game.recordOneShot(6); // スペア 10+5=15 + game.recordOneShot(5); + recordSamePinsManyShot(17, 0); + assertThat(game.getTotalScore()).isEqualTo(20); + } private void recordSamePinsManyShot(int shotCount, int pins) { for (int count = 0; count < shotCount; count++) { game.recordOneShot(pins); } } }修正する際の考え方
修正方法を考える
- 原因を考える → 前の投球状態を覚えてないから
- 回避策を考える → スペアフラグを用意
- ※この時、他の問題は考えない(ストライク、10フレーム目など)インクリメンタルにやる
手順を考える
- スペアフラグ追加
- 合計点が10点→フラグON、フラグがONなら加算
↓ 修正してみる
public class BowlingGame { private int totalScore = 0; + private boolean isSpare = false; public void recordOneShot(int pins) { totalScore += pins; + if (isSpare) { + totalScore += pins; + isSpare = false; + } + if (totalScore == 10) { + isSpare = true; + } } public int getTotalScore() { return totalScore; } }
- 既存テスト失敗したら通るように修正
- 原因 → 合計点が10の時にフラグONにしたから
- 回避策 → 条件変更:1回前のピン数との合計が10になったらフラグON
package bowling; public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; + private int beforePins = 0; public void recordOneShot(int pins) { totalScore += pins; if (isSpare) { totalScore += pins; isSpare = false; } - if (totalScore == 10) { + if (pins + beforePins == 10) { isSpare = true; } + beforePins = pins; } public int getTotalScore() { return totalScore; } }今の実装の死角をテストしてみる
Red
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class BowlingGameTest { private BowlingGame game; @BeforeEach public void setup() { game = new BowlingGame(); } // ry + @Test + public void test_not_spare_when_before_and_current_total_10() { + game.recordOneShot(2); + game.recordOneShot(6); + game.recordOneShot(4); + game.recordOneShot(7); + recordSamePinsManyShot(16, 0); + assertThat(game.getTotalScore()).isEqualTo(19); + } private void recordSamePinsManyShot(int shotCount, int pins) { for (int count = 0; count < shotCount; count++) { game.recordOneShot(pins); } } }Green
public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; private int beforePins = 0; private int shotCount = 1; public void recordOneShot(int pins) { totalScore += pins; if (isSpare) { totalScore += pins; isSpare = false; } - if (pins + beforePins == 10) { + if (shotCount == 2 && pins + beforePins == 10) { isSpare = true; } beforePins = pins; + shotCount = shotCount == 1 ? 2 : 1; } public int getTotalScore() { return totalScore; } }5.複雑なテストケース:ストライク/連続ストライク
ストライク
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class BowlingGameTest { private BowlingGame game; @BeforeEach public void setup() { game = new BowlingGame(); } // ry + @Test + public void test_strike() { + game.recordOneShot(10); + game.recordOneShot(3); + game.recordOneShot(4); + game.recordOneShot(2); + recordSamePinsManyShot(15, 0); + assertThat(game.getTotalScore()).isEqualTo(26); + } private void recordSamePinsManyShot(int shotCount, int pins) { for (int count = 0; count < shotCount; count++) { game.recordOneShot(pins); } } }
- 修正
- 原因:ストライクの判断ができない
- 回避策:現ショットより後の得点加算枠数を設ける
public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; private int beforePins = 0; private int shotCount = 1; + private int strikeAddScoreCount = 0; public void recordOneShot(int pins) { totalScore += pins; if (isSpare) { totalScore += pins; isSpare = false; } if (shotCount == 2 && pins + beforePins == 10) { isSpare = true; } + if (strikeAddScoreCount > 0) { + totalScore += pins; + strikeAddScoreCount -= 1; + } + if (pins == 10) { + strikeAddScoreCount = 2; + } beforePins = pins; shotCount = shotCount == 1 ? 2 : 1; } public int getTotalScore() { return totalScore; } }定期的にリファクタリング
メソッド抽出
package bowling; public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; private int beforePins = 0; private int shotCount = 1; private int strikeAddScoreCount = 0; public void recordOneShot(int pins) { totalScore += pins; - if (isSpare) { - totalScore += pins; - isSpare = false; - } - if (shotCount == 2 && pins + beforePins == 10) { - isSpare = true; - } - - if (strikeAddScoreCount > 0) { - totalScore += pins; - strikeAddScoreCount -= 1; - } - if (pins == 10) { - strikeAddScoreCount = 2; - } + calcSpareAddScore(pins); + calcStrikeAddScore(pins); beforePins = pins; shotCount = shotCount == 1 ? 2 : 1; } + public int getTotalScore() { + return totalScore; + } + + private void calcSpareAddScore(int pins) { + if (isSpare) { + totalScore += pins; + isSpare = false; + } + if (shotCount == 2 && pins + beforePins == 10) { + isSpare = true; + } + } + + private void calcStrikeAddScore(int pins) { + if (strikeAddScoreCount > 0) { + totalScore += pins; + strikeAddScoreCount -= 1; + } + if (pins == 10) { + strikeAddScoreCount = 2; + } + } }ストライク2連続
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class BowlingGameTest { private BowlingGame game; @BeforeEach public void setup() { game = new BowlingGame(); } // ry + @Test + public void test_strike_double() { + game.recordOneShot(10); + game.recordOneShot(10); + game.recordOneShot(4); + game.recordOneShot(2); + recordSamePinsManyShot(14, 0); + assertThat(game.getTotalScore()).isEqualTo(46); + } private void recordSamePinsManyShot(int shotCount, int pins) { for (int count = 0; count < shotCount; count++) { game.recordOneShot(pins); } } }public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; private int beforePins = 0; private int shotCount = 1; private int strikeAddScoreCount = 0; + private int doubleAddScoreCount = 0; public void recordOneShot(int pins) { totalScore += pins; calcSpareAddScore(pins); calcStrikeAddScore(pins); beforePins = pins; shotCount = shotCount == 1 ? 2 : 1; } public int getTotalScore() { return totalScore; } private void calcSpareAddScore(int pins) { if (isSpare) { totalScore += pins; isSpare = false; } if (shotCount == 2 && pins + beforePins == 10) { isSpare = true; } } private void calcStrikeAddScore(int pins) { if (strikeAddScoreCount > 0) { totalScore += pins; strikeAddScoreCount -= 1; } + if (doubleAddScoreCount > 0) { + totalScore += pins; + doubleAddScoreCount -= 1; + } if (pins == 10) { + if (strikeAddScoreCount == 0) { strikeAddScoreCount = 2; + } else { + doubleAddScoreCount = 2; + } } } }ストライク3連続のテストケース追加
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class BowlingGameTest { private BowlingGame game; @BeforeEach public void setup() { game = new BowlingGame(); } // ry + @Test + public void test_strike_turkey() { + game.recordOneShot(10); + game.recordOneShot(10); + game.recordOneShot(10); + game.recordOneShot(4); + game.recordOneShot(2); + recordSamePinsManyShot(12, 0); + assertThat(game.getTotalScore()).isEqualTo(76); + } private void recordSamePinsManyShot(int shotCount, int pins) { for (int count = 0; count < shotCount; count++) { game.recordOneShot(pins); } } }複雑なケース(続):ストライクとスペアの複合
ストライク1回&スペア1回
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class BowlingGameTest { private BowlingGame game; @BeforeEach public void setup() { game = new BowlingGame(); } // ry + @Test + public void test_strike_and_spare() { + game.recordOneShot(10); + game.recordOneShot(6); + game.recordOneShot(4); + game.recordOneShot(2); + recordSamePinsManyShot(15, 0); + assertThat(game.getTotalScore()).isEqualTo(34); + } private void recordSamePinsManyShot(int shotCount, int pins) { for (int count = 0; count < shotCount; count++) { game.recordOneShot(pins); } } }
- 原因:ストライクの後、shotCountが1に戻らない
- 回避策:shotCountの条件追加
public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; private int beforePins = 0; private int shotCount = 1; private int strikeAddScoreCount = 0; private int doubleAddScoreCount = 0; public void recordOneShot(int pins) { totalScore += pins; calcSpareAddScore(pins); calcStrikeAddScore(pins); beforePins = pins; - shotCount = shotCount == 1 ? 2 : 1; + shotCount = shotCount == 1 && strikeAddScoreCount < 2 ? 2 : 1; } public int getTotalScore() { return totalScore; } private void calcSpareAddScore(int pins) { if (isSpare) { totalScore += pins; isSpare = false; } if (shotCount == 2 && pins + beforePins == 10) { isSpare = true; } } private void calcStrikeAddScore(int pins) { if (strikeAddScoreCount > 0) { totalScore += pins; strikeAddScoreCount -= 1; } if (doubleAddScoreCount > 0) { totalScore += pins; doubleAddScoreCount -= 1; } if (pins == 10) { if (strikeAddScoreCount == 0) { strikeAddScoreCount = 2; } else { doubleAddScoreCount = 2; } } } }ストライク2連続+スペア
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class BowlingGameTest { private BowlingGame game; @BeforeEach public void setup() { game = new BowlingGame(); } // ry + @Test + public void test_strike_double_and_spare() { + game.recordOneShot(10); + game.recordOneShot(10); + game.recordOneShot(6); + game.recordOneShot(4); + game.recordOneShot(2); + recordSamePinsManyShot(13, 0); + assertThat(game.getTotalScore()).isEqualTo(26+20+12+2); + } private void recordSamePinsManyShot(int shotCount, int pins) { for (int count = 0; count < shotCount; count++) { game.recordOneShot(pins); } } }
- 原因:ストライク2連続の時も shotCountが1に戻らない
- 回避策:同様にshotCountの条件追加
public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; private int beforePins = 0; private int shotCount = 1; private int strikeAddScoreCount = 0; private int doubleAddScoreCount = 0; public void recordOneShot(int pins) { totalScore += pins; calcSpareAddScore(pins); calcStrikeAddScore(pins); beforePins = pins; - shotCount = shotCount == 1 && strikeAddScoreCount < 2 ? 2 : 1; + shotCount = shotCount == 1 && strikeAddScoreCount < 2 && doubleAddScoreCount < 2 ? 2 : 1; } public int getTotalScore() { return totalScore; } private void calcSpareAddScore(int pins) { if (isSpare) { totalScore += pins; isSpare = false; } if (shotCount == 2 && pins + beforePins == 10) { isSpare = true; } } private void calcStrikeAddScore(int pins) { if (strikeAddScoreCount > 0) { totalScore += pins; strikeAddScoreCount -= 1; } if (doubleAddScoreCount > 0) { totalScore += pins; doubleAddScoreCount -= 1; } if (pins == 10) { if (strikeAddScoreCount == 0) { strikeAddScoreCount = 2; } else { doubleAddScoreCount = 2; } } } }リファクタ(メソッド抽出)
public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; private int beforePins = 0; private int shotCount = 1; private int strikeAddScoreCount = 0; private int doubleAddScoreCount = 0; public void recordOneShot(int pins) { totalScore += pins; calcSpareAddScore(pins); calcStrikeAddScore(pins); beforePins = pins; shotCount = shotCount == 1 && strikeAddScoreCount < 2 && doubleAddScoreCount < 2 ? 2 : 1; } public int getTotalScore() { return totalScore; } private void calcSpareAddScore(int pins) { if (isSpare) { totalScore += pins; isSpare = false; } checkSpare(pins); } private void checkSpare(int pins) { if (shotCount == 2 && pins + beforePins == 10) { isSpare = true; } } private void calcStrikeAddScore(int pins) { - if (strikeAddScoreCount > 0) { - totalScore += pins; - strikeAddScoreCount -= 1; - } - if (doubleAddScoreCount > 0) { - totalScore += pins; - doubleAddScoreCount -= 1; - } - if (pins == 10) { - if (strikeAddScoreCount == 0) { - strikeAddScoreCount = 2; - } else { - doubleAddScoreCount = 2; - } - } + addStrikeScore(pins); + addDoubleScore(pins); + if (isStrike(pins)) { + recognizeStrikeAddCount(); + } } + private void addStrikeScore(int pins) { + if (strikeAddScoreCount > 0) { + totalScore += pins; + strikeAddScoreCount -= 1; + } + } + + private void addDoubleScore(int pins) { + if (doubleAddScoreCount > 0) { + totalScore += pins; + doubleAddScoreCount -= 1; + } + } + + private boolean isStrike(int pins) { + return pins == 10; + } + + private void recognizeStrikeAddCount() { + if (strikeAddScoreCount == 0) { + strikeAddScoreCount = 2; + } else { + doubleAddScoreCount = 2; + } + } }フレーム毎の点数取得
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class BowlingGameTest { private BowlingGame game; @BeforeEach public void setup() { game = new BowlingGame(); } // ry + @Test + public void test_one_frame_score_when_all_garter() { + recordSamePinsManyShot(20, 0); + assertThat(game.getFrameScore(1)).isEqualTo(0); + } private void recordSamePinsManyShot(int shotCount, int pins) { for (int count = 0; count < shotCount; count++) { game.recordOneShot(pins); } } }public BowlingGame { // ry + public int getFrameScore(int frameNum) { + return 0; + } }手詰まり感が出てきたら静的設計を見直し
- 計算が必要なテストケースを追加しようとすると…これまでの繰り返しになる?
- 手詰まり感が出てきた場合は、静的設計を見直す
※動的設計と静的設計を相互に行うことで、設計が徐々に洗練される
Frameクラス導入
public class Frame { public void recordOneShot(int pins) { } public int getScore(int frameNum) { return 0; } }全投球1ピンだけ倒した場合
Red
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class FrameTest { private Frame frame; @BeforeEach public void setup() { frame = new Frame(); } @Test public void test_one_frame_score_when_all_one_pin() { frame.recordOneShot(1); assertThat(frame.getScore(1)).isEqualTo(1); } }public class Frame { private int score = 0; public void recordOneShot(int pins) { + score += pins; } public int getScore(int frameNum) { - return 0; + return score; } }BowlingGameにFrame組み込み
public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; private int beforePins = 0; private int shotCount = 1; private int strikeAddScoreCount = 0; private int doubleAddScoreCount = 0; + private Frame frame = new Frame(); // ry + public int getFrameScore(int frameNum) { + return frame.getScore(frameNum); + } // ry }import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class BowlingGameTest { private BowlingGame game; @BeforeEach public void setup() { game = new BowlingGame(); } // ry + @Test + public void test_one_frame_score_when_all_garter() { + recordSamePinsManyShot(20, 0); + assertThat(game.getFrameScore(1)).isEqualTo(0); + } private void recordSamePinsManyShot(int shotCount, int pins) { for (int count = 0; count < shotCount; count++) { game.recordOneShot(pins); } } }BowlingGameにFrame組み込み2(責務の分離)
- 静的設計見直し:Frameクラスに以下を委譲する
- 投球記録
- フレーム完了判定
- 投球結果判定(スペア/ストライク etc.)
- ストライク/スペア後の得点加算判定/記録
- 点数返却
※ MECE (Mutually Exclusive and Collectively Exhaustive):重複なく漏れなく という視点でクラス設計俯瞰視
※現時点で明確になっている機能だけに着目してリファクタを行う
フレーム完了判定
Red
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class FrameTest { private Frame frame; @BeforeEach public void setup() { frame = new Frame(); } // ry + @Test + public void test_frame_finish_two_shot() { + frame.recordOneShot(1); + assertThat(frame.isFinished()).isFalse(); + frame.recordOneShot(1); + assertThat(frame.isFinished()).isTrue(); + } }package bowling; public class Frame { private int score = 0; + private int shotCount = 0; public void recordOneShot(int pins) { score += pins; + shotCount++; } public int getScore(int frameNum) { return score; } + public boolean isFinished() { + return shotCount >= 2; + } }フレーム完了判定(ストライク)
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class FrameTest { private Frame frame; @BeforeEach public void setup() { frame = new Frame(); } // ry + @Test + public void test_frame_finish_strike() { + var frame = new Frame(); + frame.recordOneShot(10); + assertThat(frame.isFinished()).isTrue(); + } }public class Frame { private int score = 0; private int shotCount = 0; public void recordOneShot(int pins) { score += pins; shotCount++; } public int getScore(int frameNum) { return score; } public boolean isFinished() { - return shotCount >= 2; + return shotCount >= 2 || score >= 10; } }BowlingGameにフレーム判定組み込み
全投球1ピンだと全フレーム2点
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class BowlingGameTest { private BowlingGame game; @BeforeEach public void setup() { game = new BowlingGame(); } // ry + @Test + public void test_one_frame_score_is_2_when_all_one_pin() { + recordSamePinsManyShot(20, 1); + for (int frameNum = 0; frameNum < 10; frameNum++) { + assertThat(game.getFrameScore(frameNum + 1)).isEqualTo(2); + } + } private void recordSamePinsManyShot(int shotCount, int pins) { for (int count = 0; count < shotCount; count++) { game.recordOneShot(pins); } } }import java.util.LinkedList; public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; private int beforePins = 0; private int shotCount = 1; private int strikeAddScoreCount = 0; private int doubleAddScoreCount = 0; - private Frame frame = new Frame(); + private final LinkedList<Frame> frames = new LinkedList<>() { + { + add(new Frame()); + } + }; public void recordOneShot(int pins) { + var frame = frames.getLast(); + frame.recordOneShot(pins); totalScore += pins; calcSpareAddScore(pins); calcStrikeAddScore(pins); beforePins = pins; shotCount = shotCount == 1 && strikeAddScoreCount < 2 && doubleAddScoreCount < 2 ? 2 : 1; + if (frame.isFinished()) { + frames.add(new Frame()); + } } public int getTotalScore() { return totalScore; } + public int getFrameScore(int frameNum) { + return frames.get(frameNum - 1).getScore(); + } // ry }投球結果判定
スペア
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; public class FrameTest { private Frame frame; @BeforeEach public void setup() { frame = new Frame(); } // ry + @Test + public void test_spare_when_defect_10_pins_in_second_shot_of_frame() { + frame.recordOneShot(5); + assertThat(frame.isSpare()).isFalse(); + frame.recordOneShot(5); + assertThat(frame.isSpare()).isTrue(); + } }public class Frame { private int score = 0; private int shotCount = 0; public void recordOneShot(int pins) { score += pins; shotCount++; } public int getScore() { return score; } public boolean isFinished() { return shotCount >= 2 || score >= 10; } + public boolean isSpare() { + return score == 10 && shotCount >= 2; + } }ストライク
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class FrameTest { private Frame frame; @BeforeEach public void setup() { frame = new Frame(); } //ry + @Test + public void test_strike_when_defect_10_pins_in_first_shot_of_frame() { + assertThat(frame.isStrike()).isFalse(); + frame.recordOneShot(10); + assertThat(frame.isStrike()).isTrue(); + } }public class Frame { private int score = 0; private int shotCount = 0; public void recordOneShot(int pins) { score += pins; shotCount++; } public int getScore() { return score; } public boolean isFinished() { return shotCount >= 2 || score >= 10; } public boolean isSpare() { return score == 10 && shotCount >= 2; } + public boolean isStrike() { + return score == 10 && shotCount == 1; + } }BowlingGameに投球判定を組み込む
スペア
import java.util.LinkedList; public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; private int strikeAddScoreCount = 0; private int doubleAddScoreCount = 0; private final LinkedList<Frame> frames = new LinkedList<>() { { add(new Frame()); } }; public void recordOneShot(int pins) { var frame = frames.getLast(); frame.recordOneShot(pins); totalScore += pins; calcSpareAddScore(pins); calcStrikeAddScore(pins); if (frame.isFinished()) { frames.add(new Frame()); } } public int getTotalScore() { return totalScore; } public int getFrameScore(int frameNum) { return frames.get(frameNum - 1).getScore(); } private void calcSpareAddScore(int pins) { if (isSpare) { totalScore += pins; isSpare = false; } - checkSpare(pins); + if (frames.getLast().isSpare()) { + isSpare = true; + } } - private void checkSpare(int pins) { - if (shotCount == 2 && pins + beforePins == 10) { - isSpare = true; - } - } private void calcStrikeAddScore(int pins) { addStrikeScore(pins); addDoubleScore(pins); if (isStrike(pins)) { recognizeStrikeAddCount(); } } private void addStrikeScore(int pins) { if (strikeAddScoreCount > 0) { totalScore += pins; strikeAddScoreCount -= 1; } } private void addDoubleScore(int pins) { if (doubleAddScoreCount > 0) { totalScore += pins; doubleAddScoreCount -= 1; } } private boolean isStrike(int pins) { return pins == 10; } private void recognizeStrikeAddCount() { if (strikeAddScoreCount == 0) { strikeAddScoreCount = 2; } else { doubleAddScoreCount = 2; } } }ストライク
import java.util.LinkedList; public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; private int strikeAddScoreCount = 0; private int doubleAddScoreCount = 0; private final LinkedList<Frame> frames = new LinkedList<>() { { add(new Frame()); } }; public void recordOneShot(int pins) { var frame = frames.getLast(); frame.recordOneShot(pins); totalScore += pins; calcSpareAddScore(pins); calcStrikeAddScore(pins); if (frame.isFinished()) { frames.add(new Frame()); } } public int getTotalScore() { return totalScore; } public int getFrameScore(int frameNum) { return frames.get(frameNum - 1).getScore(); } private void calcSpareAddScore(int pins) { if (isSpare) { totalScore += pins; isSpare = false; } if (frames.getLast().isSpare()) { isSpare = true; } } private void calcStrikeAddScore(int pins) { addStrikeScore(pins); addDoubleScore(pins); - if (isStrike(pins)) { + if (frames.getLast().isStrike()) { recognizeStrikeAddCount(); } } private void addStrikeScore(int pins) { if (strikeAddScoreCount > 0) { totalScore += pins; strikeAddScoreCount -= 1; } } private void addDoubleScore(int pins) { if (doubleAddScoreCount > 0) { totalScore += pins; doubleAddScoreCount -= 1; } } private void recognizeStrikeAddCount() { if (strikeAddScoreCount == 0) { strikeAddScoreCount = 2; } else { doubleAddScoreCount = 2; } } }ストライク/スペア後の(ボーナス)得点加算判定/記録
Frameに移す
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class FrameTest { private Frame frame; @BeforeEach public void setup() { frame = new Frame(); } // ry + @Test + public void test_spare_add_bonus_score() { + frame.recordOneShot(5); + frame.recordOneShot(5); + frame.addBonusScore(5); + assertThat(frame.getScore()).isEqualTo(15); + } }package bowling; public class Frame { private int score = 0; private int shotCount = 0; public void recordOneShot(int pins) { score += pins; shotCount++; } public int getScore() { return score; } public boolean isFinished() { return shotCount >= 2 || score >= 10; } public boolean isSpare() { return score == 10 && shotCount >= 2; } public boolean isStrike() { return score == 10 && shotCount == 1; } + public void addBonusScore(int bonusScore) { + score += bonusScore; + } }BowlingGameにボーナス得点加算判定/記録を組み込む
スペア
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class FrameTest { private Frame frame; @BeforeEach public void setup() { frame = new Frame(); } // ry + @Test + public void test_spare_frame_score_add_next_pins() { + game.recordOneShot(4); + game.recordOneShot(6); + game.recordOneShot(5); + assertThat(game.getFrameScore(1)).isEqualTo(15); + assertThat(game.getTotalScore()).isEqualTo(20); + } }import java.util.LinkedList; public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; + private Frame spareFrame = null; private int strikeAddScoreCount = 0; private int doubleAddScoreCount = 0; private final LinkedList<Frame> frames = new LinkedList<>() { { add(new Frame()); } }; // ry private void calcSpareAddScore(int pins) { if (isSpare) { totalScore += pins; isSpare = false; + spareFrame.addBonusScore(pins); + spareFrame = null; } if (frames.getLast().isSpare()) { isSpare = true; + spareFrame = frames.getLast(); } } // ry }ストライク
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class FrameTest { private Frame frame; @BeforeEach public void setup() { frame = new Frame(); } // ry @Test public void test_strike_frame_score_add_twice_next_pins() { game.recordOneShot(10); game.recordOneShot(3); game.recordOneShot(4); game.recordOneShot(2); recordSamePinsManyShot(15, 0); assertThat(game.getTotalScore()).isEqualTo(26); assertThat(game.getFrameScore(1)).isEqualTo(17); } }import java.util.LinkedList; public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; private Frame spareFrame = null; + private Frame strikeFrame = null; private int strikeAddScoreCount = 0; private int doubleAddScoreCount = 0; private final LinkedList<Frame> frames = new LinkedList<>() { { add(new Frame()); } }; // ry private void addStrikeScore(int pins) { if (strikeAddScoreCount > 0) { totalScore += pins; strikeAddScoreCount -= 1; + strikeFrame.addBonusScore(pins); } } private void addDoubleScore(int pins) { if (doubleAddScoreCount > 0) { totalScore += pins; doubleAddScoreCount -= 1; } } private void recognizeStrikeAddCount() { if (strikeAddScoreCount == 0) { strikeAddScoreCount = 2; + strikeFrame = frames.getLast(); } else { doubleAddScoreCount = 2; } } }ストライク(2連続)
import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class FrameTest { private Frame frame; @BeforeEach public void setup() { frame = new Frame(); } // ry + @Test + public void test_strike_double_frame_score() { + game.recordOneShot(10); + game.recordOneShot(10); + game.recordOneShot(4); + game.recordOneShot(2); + recordSamePinsManyShot(14, 0); + assertThat(game.getTotalScore()).isEqualTo(46); + assertThat(game.getFrameScore(1)).isEqualTo(24); + assertThat(game.getFrameScore(2)).isEqualTo(16); + } }import java.util.LinkedList; public class BowlingGame { private int totalScore = 0; private boolean isSpare = false; private Frame spareFrame = null; private Frame strikeFrame = null; + private Frame doubleFrame = null; private int strikeAddScoreCount = 0; private int doubleAddScoreCount = 0; private final LinkedList<Frame> frames = new LinkedList<>() { { add(new Frame()); } }; // ry private void addDoubleScore(int pins) { if (doubleAddScoreCount > 0) { totalScore += pins; doubleAddScoreCount -= 1; + doubleFrame.addBonusScore(pins); } } private void recognizeStrikeAddCount() { if (strikeAddScoreCount == 0) { strikeAddScoreCount = 2; strikeFrame = frames.getLast(); } else { doubleAddScoreCount = 2; + doubleFrame = frames.getLast(); } } }続くが保留
次は、strikeAddScoreCount、doubleAddScoreCount による判定も Frameに移す
ここまででも、ある程度イメージできるかと思われるため一旦ここまで(気力尽き)
TDDの本質的イメージ
あくまで私の理解のためあしからず。
抽象的なイメージ
まず(雑な画像で申し訳ないが)画像から以下のようなイメージをして欲しい。
- 以下の一番外の円が開発対象全体であり、その中にはいくつもの問題/課題(以下画像のグレーの〇)が存在している = 開発対象は問題/課題の集合体(塊)
- 円の中央に進めば進むほど対象の核心となるような問題が存在している
- 対象への理解が乏しく、開発者は外側から見える問題/課題しか認識できていない
上記を踏まえ、実際に開発を進めていくとなると、対象への理解が乏しい場合、この塊に対して、
- 何から手を付けていけばいいか分からない
- どう進めていけばいいか分からない
となると思う。そこに対してTDDは、
外側から1つずつ解けるものから順番に解いていく。それが一見回り道だったしても。
最短ルートで問題達を解決していけるのが理想だが、まだ見えてない内側の部分がある状態で、そもそも最短ルートで解いていくというのは現実的に不可能である。また、最短だと考えて複数の問題を同時に解こうとするとかえってややこしくなり余計に時間がかかるし、複雑な作り込みをしてしまうことは往々にして起きるだろう。だからこそ、小さい粒度に分解し、1つずつ問題を解いていく。
1ずつ解きほどいていくことで、少しずつ核心に近づくとともに対象への理解を進めていく。核心もしくはそれに近しい問題に到達する頃には対象への理解が深まり、その問題に対して正しい判断をすることができる状態になる(だからこそ、正しい判断ができるまでは決めないでおく。正しい判断がまだできない状態で仮決めてしてもそれは概ね余分な作り込みになってしまうから)。
実際に実装する時には、どの問題を解くかというのをテストケースを書くことで定義/宣言するのだと思う。
TDDのアプローチのイメージ
上記を踏まえて、TDDのアプローチに紐づけて考えると、TDDは、
小さい粒度の目標(目指すべきゴールや解決したい問題)をテストケースを書くことで定め、目の前の1つの目標だけに注力する。これを繰り返し行っていく。
それを、静的設計と動的設計さえも分割して行うことで、
- 静的設計 ≒ ロジック/アルゴリズムだけを考える作業 ( Red → Green の時 )
- 動的設計 ≒ クラス/インターフェース設計だけを考える作業 ( Refactor の時 )
片方をやるときは片方に注力でき、Red/Green/Refactorのサイクルを回すことで(開発者の力量によるが)ある程度設計の質を高い状態に保ちつつ、設計/実装を洗練していけるのだと考える。
また、Refactor時は以下を担保に得られる安心と信頼に基づいているため、動的設計を考える作業に注力できる。
- 目標を達成するために必要なロジックを理解/実装したという実績
- テストというOK/NGを判定してくれる絶対的指標
と私は理解した。以上
参考
- 投稿日:2020-11-14T15:52:27+09:00
Mediator Pattern
MediatorはMediatorが管理するクラスインスタンスをフィールドに保持し、Mediatoに管理されるクラスはMediatorのインスタンスを保持し、Mediatorのメソッドを使わせる
Mediatorは管理するクラスのインスタンスからクラスの状態を取得し、状態に応じた処理を行う以下のクラス構成で確認します
クラス 説明 Mediator.class 管理するクラスのインスタンスを保持 menber.class men1.class~men2.classのsuper
Mediatorクラスインタンスを持つuser(Main.class) 動作確認 *user 他の開発者がこのパターンを利用する、という意味合いを含みます
Mediator.classclass Mediator{ menber men1 = new men1(), men2 = new men2(); String check(menber menber){ if(menber.str == men1.str){return "men1";} else{return "men2";} } }menber.classclass menber{ Mediator mediator; String str; menber(String str){this.str =str;} void set(){this.mediator=new Mediator();} }men.classclass men1 extends menber{ men1(){super("men1");}} class men2 extends menber{ men2(){super("men2");}user(Main.class)public static void main(String[] args){ menber m1 = new men1(); m1.set(); String res = m1.mediator.check(m1); System.out.println(res); }
- 投稿日:2020-11-14T13:36:01+09:00
Facade Pattern
多数のFacade配下のクラスの利用をコントロールする。userはFacadeを通じてFacade配下のクラスを利用する
以下のクラス構成で確認します
package アクセス修飾子 クラス 説明 sample public facade.class facadeが管理する各クラスの呼び出し、利用をコントロール sample default sam0.class~sam2.class int値を戻す default public user(Main.class) facadeを利用してsam0.class~sam2.classを利用する *user 他の開発者がこのパターンを利用する、という意味合いを含みます
facade.classpackage sample; public class facade{ int res; public facade(int condition){ switch(condition){ case 0 : res = new sam0().get();break; case 1 : res = new sam1().get();break; default : res = new sam2().get();break; } this.res = res; } public int get(){return this.res;} }sam01.classpackage sample; class sam0{ int get(){return 0;} } class sam1{ int get(){return 1;} } class sam2{ int get(){return 2;} }user(Main.class)import sample.facade; class Main { public static void main(String[] args){ facade fd = new facade(8); System.out.println(fd.get()); } }
- 投稿日:2020-11-14T12:35:16+09:00
Chain of Responsibility Pattern
オブジェクトを順次よび出す
以下のクラス構成で確認します
クラス 説明 abstract
chain.classchain機能を定義
フィールド next は次のオブジェクトインスタンスを格納
set():次のオブジェクトをセット
doChain()を定義chai1.class~chain3.class chainを実装 user(Main.class) 動作確認 *user 他の開発者がこのパターンを利用する、という意味合いを含みます
abstract_class_chainabstract class chain{ String name = this.getClass().getName(); chain next; // 次のオブジェクトを格納 chain set(chain next){ this.next=next; return this.next; // 次のオブジェクトを返す } abstract void doChain(); void print(){ System.out.println(name);} }chain1.classclass chain1 extends chain{ void doChain(){ print(); if(next != null){next.doChain();} // 次のオブジェクトのdoChain()を実行 } }chain2.classclass chain2 extends chain{ void doChain(){ print(); if(next != null){next.doChain();} } }chain3.classclass chain3 extends chain{ void doChain(){ print(); if(next != null){next.doChain();} } }user(Main.class)public static void main(String[] args){ chain1 ch1 = new chain1(); chain2 ch2 = new chain2(); chain3 ch3 = new chain3(); ch1.set(ch2).set(ch3); ch1.doChain(); }
- 投稿日:2020-11-14T12:30:55+09:00
PostgreSQLでUPSERTやってみた。
はじめに
DB操作を行うときに、「このデータがINSERT済み、もしくはUNIQUE制約にひっかかったら更新、
そうでなければINSERTしたいな」
ってときにJavaで分岐させたりしてました。
でもPostgreSQLを使用していれば、その処理が同時にできるらしい。
ということで試してみました。ちなみに、SQLのみでの実装は結構他のサイトにも情報があるので、今回はMyBatisを使用した方法をとっていきます。
といってもやることはあまり変わりませんが。環境は以下
IDE:Eclipse
</>:Java8,SpringBoot
<DB関係(本筋)>:PostgreSQL,MyBatis環境構築
SpringBootとMyBatisそのものとか、PostgreSQLとの連携については本筋ではないのである程度省略します。
今回はGradleです。
Gradle(抜粋)
dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2' compileOnly 'org.projectlombok:lombok' runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } }
application.properties(抜粋)
<!-- Postgres property --> spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:5432/hrm spring.datasource.username=***** spring.datasource.password=*****テーブルを用意
PostgreSQLにテーブルを用意します。
今回はSchemaはpublicで行う。
PostgreSQL
create table tableName ( id serial primary key, user_id integer not null, date_id integer not null, is_working smallint, ); alter table tableName add constraint user_date unique(user_id, date_id);今回のアプリケーションの例を説明します。
例えばユーザーのToDoリストを管理するアプリケーションで、ユーザーがタスクをこなしたかどうかをチェックするアプリがあるとします。
タスクがこなせていればis_workingには1、そうでなければ0を挿入し、ひと月分まとめて更新ができるようにします。
そのタスク管理を過去にも遡って編集したい場合、9月2日になって前日のタスクがこなせていないことが判明した場合は、
9月1日のis_workingを更新しなければなりません。
このとき、9月1日のように更新したい値や、9月2日のように新規登録したい値がまとめて入ってきた場合や、ユーザーが複数いる場合を想定して上記のSQLではuser_idとdate_idが一致した場合に限り制約を設けています。あとは、htmlからController、Mapperへと値を受け渡し、Mapperにupsertの構文を記述するだけです。
Mapper(抜粋)
<insert id="upsert"> insert into ${tableName} ( user_id, date_id, is_working ) values ( #{user_id}, #{date_id}, #{is_working} ) ON CONFLICT (user_id, date_id) do update set is_working = #{is_working} </insert>これだけです。
注意点
Mapperに構文を書く際、upsertという単語はMyBatisでは存在しない?ため、
insertもしくはupdateを記述することになります。
また、conflict名も正しく記述しないと動作しないので、間違えないようにしましょう。その他の方法?
他にもDONOTHINGやINDEXを利用した指定方法もあるようです。
DONOTHINGであればこちら
その他詳細な使い方についてはこちらのサイトが役に立つかと思います。まとめ
SQL直打ちするような方法は多々見かけましたが、実際にコーディングしているようなものはあまりなかったので
参考程度にまとめてみました。
ですがMyBatisに限らずどんな方法であっても、SQLの構文が変わるわけではないのであまり関係ないかもしれないですね。
Daoにパラメータさえ渡せてしまえば、記述は同じです。if文で記述するよりまとまっていると思いますので、PostgreSQLの環境であれば積極的に活用していきたいですね。
- 投稿日:2020-11-14T12:00:48+09:00
【Android / Java】DataBinding について学んだ
はじめに
開発案件でDataBindingを使っており、学習したことを記事にする。
今回はDataBindingを使って、データクラスオブジェクトのプロパティの変更を監視してView表示に反映させるまでを学習した。
今回作成した学習用アプリ
①と②のテキスト表示を「チェンジ」ボタンを押す毎に動的に行ったりきたり切り替えるというもの
「ドラえもん」⇄「のびた」
①
② 実装ファイル
build.gradle(:app)
activity_main.xml
Character.java
(モデルclass)EventHandlers.java
(インターフェース)MainActivity.java
実装していく
1. DataBinding の導入
build.gradle(:app)
にdataBinding { enabled = true }
を追加build.gradleapply plugin: 'com.android.application' android { compileSdkVersion 30 buildToolsVersion "30.0.2" defaultConfig { applicationId "com.android.databindingjava" minSdkVersion 24 targetSdkVersion 30 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } // 記述を追加 dataBinding { enabled = true } } dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' }2. 変更を監視するモデルクラスを定義
Character.java// BaseObservableを継承 public class Character extends BaseObservable { private String name; public Character(String name) { this.name = name; } // getNameに@Bindableを付与することにより監視用の定数BR.nameが生成される @Bindable public String getName() { return name; } // setNameにnotifyPropertyChanged(BR.name)を付与することで // レイアウト側からBR.nameに対応するgetName()が呼ばれる(setNameされるタイミングでgetNameがレイアウト側から呼ばれる) public void setName(String name) { this.name = name; notifyPropertyChanged(BR.name); } }viewに変更を反映させるために、
BaseObservable
を継承getName
に@Bindable
をつけ監視用の定数であるBR.name
を生成するsetName
にnotifyPropertyChanged(BR.name);
を記述するこうすることで
setName
が実行されるタイミングでgetName
がレイアウト側から呼ばれ、name
の値を変更した際にviewに変更が反映されるようになる。3. データを変更するためのインターフェースを定義
EventHandlers.javapublic interface EventHandlers { // クリックイベントに対応させたいため、引数はView.OnClickListenerのonClickと同じ(View view)にする void onChangeClick(View view); }レイアウトにセットするイベントハンドラーをインターフェースとして定義
4. 変更を反映させるレイアウトファイルを作成
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <!-- ルートをlayoutにすることでDataBindingに対応したレイアウトとして認識される --> <!-- activity_main.xml => ActivityMainBinding このような形で自動的にxmlファイル名に応じたBindingクラスが作られる--> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <!-- Binding オブジェクト --> <data> <!-- この記述によりcharacterという名前(任意)で、Userクラスオブジェクトとの結びつけがされる --> <variable name="character" type="com.android.databindingjava.Character" /> <!-- この記述によりeventHandlersという名前(任意)で、ハンドラー(インターフェース)が設定される --> <variable name="eventHandlers" type="com.android.databindingjava.EventHandlers" /> </data> <!-- Views--> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/text_view_user_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="30dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" android:text="@{character.name}" /> <Button android:id="@+id/button_change" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="30dp" android:text="チェンジ" android:onClick="@{eventHandlers.onChangeClick}" app:layout_constraintBottom_toTopOf="@id/text_view_user_name" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>レイアウトからモデルclassへのアクセス
<variable name="character" type="com.android.databindingjava.Character" />
により、レイアウトファイル内でCharacterクラスオブジェクトをcharacterという名前で定義しており、レイアウトファイル内でオブジェクトを使用できるようになる。
@{character.name}
この記述でCharacterクラスのnameプロパティにアクセスでき、android:text="@{character.name}"
によりtextにプロパティの値がセットされる。※
@{}
の中身はnullを許容するようになっており、nullの場合でもNullPointerExceptionが発生することはない。レイアウト要素にイベントハンドラーをセット
<variable name="eventHandlers" type="com.android.databindingjava.EventHandlers" />
により、レイアウトファイル内でEventHandlersインターフェースをeventHandlersという名前で定義しており、レイアウトファイル内でインターフェースにアクセスできるようになる。
@{eventHandlers.onChangeClick}
この記述でEventHandlersインターフェースのonChangeClickにアクセスでき、Button要素の中でandroid:onClick="@{eventHandlers.onChangeClick}"
を記述することによりクリックした際にonChangeClickが呼ばれるようになる。
(※ 後述するMainActivity.javaへの記述も必要)5. MainActivityでDataBinding処理を定義
MainActivity.java// EventHandlers(インターフェース) を実装 public class MainActivity extends AppCompatActivity implements EventHandlers { private Character chara = new Character("ドラえもん"); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // activity_main.xml に対応したクラスの bindingインスタンスを作成 ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main); // activity_main.xmlのcharacterにcharaをセット binding.setCharacter(chara); // activity_main.xmlのeventHandlersにMainActivityをセット binding.setEventHandlers(this); } // button_changeのクリックイベント処理(インターフェース) @Override public void onChangeClick(View view) { // charaのnameの文字列によって、セットする文字列を変える if (chara.getName().equals("ドラえもん")) { chara.setName("のびた"); } else { chara.setName("ドラえもん"); } } }ルートを
<layout>
にしたレイアウトファイルを作成することで自動的にxmlファイル名に応じたBindingクラスが作成される。
今回であれば、 activity_main.xml => ActivityMainBinding(.java)
onCreate
の中では以下のような処理を行っている
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
でインスタンスを作成binding.setCharacter(chara);
でレイアウトファイルのcharacterにcharaをセットbinding.setEventHandlers(this);
でレイアウトファイルのeventHandlersにMainActivityをセットそしてインターフェースonChangeClickを実装し、メソッドないでTextView文字列値に応じてデータを変更する処理を書いている。
参考サイト
私のこの記事はこちらの記事をめちゃくちゃ参考にさせていただいております。
本当にわかりやすかったです!ありがとうございました!最後に
今回は簡単なアプリですが、実際の案件は規模が大きくコードを読み解くのが大変なのが現状です。
さらに学習継続していきます。誤り、ご指摘などあればコメントいただければ幸いです。
- 投稿日:2020-11-14T10:03:03+09:00
【Java】Scanner・三項演算子・配列操作 (AOJ①)
AIZU ONLINE JUDGE の教材を使って勉強していきますAIZU ONLINE JUDGE とは
- AOJ(Aizu Online Judge:onlinejudge.u-aizu.ac.jp)は誰でも無料で利用できるプログラミング問題のオンライン採点システムです
- http://judge.u-aizu.ac.jp/onlinejudge/
- 開発者の渡部有隆さんによる機能・使いかた解説
https://book.mynavi.jp/manatee/series/detail/id=88273- JavadではClass名をMainにしないと通らないので注意
オンラインジャッジとは、多くの演習問題にチャレンジすることができ、オンラインでコードを採点してくれるサービスです。各問題には、テストデータが準備されており、提出されたコードの正誤とその効率の判定を即座に行ってくれます
入力を受け取る方法(Scanner)
- nextLineメソッド:改行までの1行分の入力を取得
- 単語間のスペースを含む入力を読み取ります(\n行の終わりまで)
- 入力が読み込まれると、カーソルを次の行に移動
- nextメソッド:スペースまで入力を読み込む
- 入力を読んだ後にカーソルを同じ行に置く
- * nextIntメソッド:スペースまでの入力をintとして取得
- エスケープシーケンス"\n"は読み取らない
import java.util.Scanner; public class Main { public static void main(String[] args) throws Exception { Scanner scan = new Scanner(System.in); // String str = scan.nextLine(); // System.out.println(str); //neko inu String str1 = scan.next(); String str2 = scan.next(); System.out.println(str1); //neko System.out.println(str2); //inu scan.close(); } }長方形の面積と周の長さ
たて a cm よこ b cm の長方形の面積と周の長さを求めるプログラムを作成して下さい。
import java.util.Scanner; public class Main { public static void main(String[] args){ Scanner scan = new Scanner(System.in); int a = scan.nextInt(); int b = scan.nextInt(); System.out.println( (a*b) + " " + (a*2+b*2) ); //System.out.printf("%d %d\n",a*b,2*(a+b)); } }時計
秒単位の時間Sが与えられるので h: m :s の形式へ変換して出力してください。ここで、hは時間、mは60未満の分、sは60未満の秒とします。H、m、sを :(コロン)区切りで1行に出力してください。
import java.util.Scanner; public class Main { public static void main(String[] args){ Scanner sc = new Scanner(System.in); int sec, min, hour; sec = sc.nextInt(); hour = sec / 3600; min = (sec%3600) / 60; sec = sec % 60; System.out.println(hour+":"+min+":"+sec); } }条件演算子(三項演算子)
(条件式) ? 式A : 式B
- 条件式の値がtrueだった場合に式1を処理、falseだった場合に式2を処理
- AとBを比較するとき(式A:式B) 式A、Bは何らかの値を返す必要がある
public class Main { public static void main(String[] args) throws Exception{ var age = 20; System.out.println(age >= 20 ? "大人" : "子供"); //大人 } }大小関係
2つの整数 a, b を読み込んで、a と b の大小関係を出力するプログラムを作成して下さい。
import java.util.Scanner; public class Main{ public static void main(String[] args){ Scanner sc = new Scanner(System.in); int a = sc.nextInt(); int b = sc.nextInt(); String result = (a == b) ? "a == b" : (a > b) ? "a > b" : "a < b"; System.out.println(result); } }配列操作
- Arraysクラス
- 配列は後からサイズ変更できない
- →copyOfメソッドでサイズの異なる配列に値を複製
- copyOf、copyOfRangeはシャローコピー
- シャローコピーは参照型の場合、コピー元が変わるとコピー先も変わってしまう
- →ディープコピー
var list2 = new StringBuilder[list1.length];
//Shallow copy import java.util.Arrays; public class Main { public static void main(String[] args) { var array1 = new String[] { "dog", "cat", "mouse", "fox", "lion" }; //配列をソート Arrays.sort(array1); //配列を文字列化 System.out.println(Arrays.toString(array1)); //[cat, dog, fox, lion, mouse] //ソート済の配列から値を検索 System.out.println(Arrays.binarySearch(array1, "mouse")); //4 var array2 = new String[] { "あ", "い", "う", "え", "お" }; //配列コピー、引数に長さ、不足分は0/nullで埋める var array3 = Arrays.copyOf(array2, 2); System.out.println(Arrays.toString(array3)); //[あ, い] //配列を引数で範囲指定してコピー var array4 = Arrays.copyOfRange(array2, 1, 7); System.out.println(Arrays.toString(array4)); //[い, う, え, お, null, null] //配列に値を設定 Arrays.fill(array4, 4, 6, "―"); System.out.println(Arrays.toString(array4)); //[い, う, え, お, ―, ―] } }//Deep copy import java.util.Arrays; public class Main { public static void main(String[] args) { var list1 = new StringBuilder[] { new StringBuilder("ドレミファドーナツ"), new StringBuilder("ARAMA"), new StringBuilder("ハニホヘト") }; var list2 = new StringBuilder[list1.length]; for (var i = 0; i < list1.length; i++) { list2[i] = new StringBuilder(list1[i].toString()); } list1[2].append("ハロー"); System.out.println(Arrays.toString(list1)); //[ドレミファドーナツ, ARAMA, ハニホヘトハロー] System.out.println(Arrays.toString(list2)); //[ドレミファドーナツ, ARAMA, ハニホヘト] } }3つの数の整列
3つの整数を読み込み、それらを値が小さい順に並べて出力するプログラムを作成して下さい。
import java.util.Arrays; import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int num1 = scanner.nextInt(); int num2 = scanner.nextInt(); int num3 = scanner.nextInt(); int[] nums = {num1,num2,num3}; Arrays.sort(nums); System.out.println(String.format("%s %s %s", nums[0],nums[1],nums[2])); //46 50 80 } }
- 投稿日:2020-11-14T10:03:03+09:00
【Java】Scanner・三項演算子・配列操作 (AOJ①大小整列)
AIZU ONLINE JUDGE の教材を使って勉強していきますAIZU ONLINE JUDGE とは
- AOJ(Aizu Online Judge:onlinejudge.u-aizu.ac.jp)は誰でも無料で利用できるプログラミング問題のオンライン採点システムです
- http://judge.u-aizu.ac.jp/onlinejudge/
- 開発者の渡部有隆さんによる機能・使いかた解説
https://book.mynavi.jp/manatee/series/detail/id=88273- JavadではClass名をMainにしないと通らないので注意
オンラインジャッジとは、多くの演習問題にチャレンジすることができ、オンラインでコードを採点してくれるサービスです。各問題には、テストデータが準備されており、提出されたコードの正誤とその効率の判定を即座に行ってくれます
入力を受け取る方法(Scanner)
- nextLineメソッド:改行までの1行分の入力を取得
- 単語間のスペースを含む入力を読み取ります(\n行の終わりまで)
- 入力が読み込まれると、カーソルを次の行に移動
- nextメソッド:スペースまで入力を読み込む
- 入力を読んだ後にカーソルを同じ行に置く
- * nextIntメソッド:スペースまでの入力をintとして取得
- エスケープシーケンス"\n"は読み取らない
import java.util.Scanner; public class Main { public static void main(String[] args) throws Exception { Scanner scan = new Scanner(System.in); // String str = scan.nextLine(); // System.out.println(str); //neko inu String str1 = scan.next(); String str2 = scan.next(); System.out.println(str1); //neko System.out.println(str2); //inu scan.close(); } }長方形の面積と周の長さ
たて a cm よこ b cm の長方形の面積と周の長さを求めるプログラムを作成して下さい。
import java.util.Scanner; public class Main { public static void main(String[] args){ Scanner scan = new Scanner(System.in); int a = scan.nextInt(); int b = scan.nextInt(); System.out.println( (a*b) + " " + (a*2+b*2) ); //System.out.printf("%d %d\n",a*b,2*(a+b)); } }時計
秒単位の時間Sが与えられるので h: m :s の形式へ変換して出力してください。ここで、hは時間、mは60未満の分、sは60未満の秒とします。H、m、sを :(コロン)区切りで1行に出力してください。
import java.util.Scanner; public class Main { public static void main(String[] args){ Scanner sc = new Scanner(System.in); int sec, min, hour; sec = sc.nextInt(); hour = sec / 3600; min = (sec%3600) / 60; sec = sec % 60; System.out.println(hour+":"+min+":"+sec); } }条件演算子(三項演算子)
(条件式) ? 式A : 式B
- 条件式の値がtrueだった場合に式1を処理、falseだった場合に式2を処理
- AとBを比較するとき(式A:式B) 式A、Bは何らかの値を返す必要がある
public class Main { public static void main(String[] args) throws Exception{ var age = 20; System.out.println(age >= 20 ? "大人" : "子供"); //大人 } }大小関係
2つの整数 a, b を読み込んで、a と b の大小関係を出力するプログラムを作成して下さい。
import java.util.Scanner; public class Main{ public static void main(String[] args){ Scanner sc = new Scanner(System.in); int a = sc.nextInt(); int b = sc.nextInt(); String result = (a == b) ? "a == b" : (a > b) ? "a > b" : "a < b"; System.out.println(result); } }配列操作
- Arraysクラス
- 配列は後からサイズ変更できない
- →copyOfメソッドでサイズの異なる配列に値を複製
- copyOf、copyOfRangeはシャローコピー
- シャローコピーは参照型の場合、コピー元が変わるとコピー先も変わってしまう
- →ディープコピー
var list2 = new StringBuilder[list1.length];
//Shallow copy import java.util.Arrays; public class Main { public static void main(String[] args) { var array1 = new String[] { "dog", "cat", "mouse", "fox", "lion" }; //配列をソート Arrays.sort(array1); //配列を文字列化 System.out.println(Arrays.toString(array1)); //[cat, dog, fox, lion, mouse] //ソート済の配列から値を検索 System.out.println(Arrays.binarySearch(array1, "mouse")); //4 var array2 = new String[] { "あ", "い", "う", "え", "お" }; //配列コピー、引数に長さ、不足分は0/nullで埋める var array3 = Arrays.copyOf(array2, 2); System.out.println(Arrays.toString(array3)); //[あ, い] //配列を引数で範囲指定してコピー var array4 = Arrays.copyOfRange(array2, 1, 7); System.out.println(Arrays.toString(array4)); //[い, う, え, お, null, null] //配列に値を設定 Arrays.fill(array4, 4, 6, "―"); System.out.println(Arrays.toString(array4)); //[い, う, え, お, ―, ―] } }//Deep copy import java.util.Arrays; public class Main { public static void main(String[] args) { var list1 = new StringBuilder[] { new StringBuilder("ドレミファドーナツ"), new StringBuilder("ARAMA"), new StringBuilder("ハニホヘト") }; var list2 = new StringBuilder[list1.length]; for (var i = 0; i < list1.length; i++) { list2[i] = new StringBuilder(list1[i].toString()); } list1[2].append("ハロー"); System.out.println(Arrays.toString(list1)); //[ドレミファドーナツ, ARAMA, ハニホヘトハロー] System.out.println(Arrays.toString(list2)); //[ドレミファドーナツ, ARAMA, ハニホヘト] } }3つの数の整列
3つの整数を読み込み、それらを値が小さい順に並べて出力するプログラムを作成して下さい。
import java.util.Arrays; import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int num1 = scanner.nextInt(); int num2 = scanner.nextInt(); int num3 = scanner.nextInt(); int[] nums = {num1,num2,num3}; Arrays.sort(nums); System.out.println(String.format("%s %s %s", nums[0],nums[1],nums[2])); //46 50 80 } }